mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
# Backport This will backport the following commits from `main` to `8.x`: - [[SecuritySolution] Asset Criticality ECS compatibility (#194109)](https://github.com/elastic/kibana/pull/194109) <!--- Backport version: 9.4.3 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Pablo Machado","email":"pablo.nevesmachado@elastic.co"},"sourceCommit":{"committedDate":"2024-10-02T13:36:44Z","message":"[SecuritySolution] Asset Criticality ECS compatibility (#194109)\n\n## Summary\r\n* New asset criticality ECS fields in mappings\r\n* Schemas update\r\n* Data client update\r\n* Add check and throw an error if data migration is required\r\n* Create a mappings and data migration\r\n * When kibana starts\r\n * Check if a mappings update is required\r\n * Update mappings\r\n * Check if data migration is required\r\n * Schedule a kibana task that runs the migration\r\n\r\n\r\nNew asset criticality fields: asset, host, user\r\nTs type definition:\r\nhttps://github.com/elastic/kibana/pull/194109/files#diff-61d0a28910f5cc972f65e47ff8ba189a0b34bae0d7a0c492b88676d8059bc87dR88-R122\r\n\r\n\r\nBlocked by: https://github.com/elastic/elasticsearch/pull/113588\r\n\r\n### Checklist\r\n\r\n\r\n[x] [Unit or functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere updated or added to match the most common scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>","sha":"40eb9b279f3ad33ae7205287cec7a493e7193727","branchLabelMapping":{"^v9.0.0$":"main","^v8.16.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","v9.0.0","backport:prev-minor","Theme: entity_analytics","Team:Entity Analytics"],"title":"[SecuritySolution] Asset Criticality ECS compatibility","number":194109,"url":"https://github.com/elastic/kibana/pull/194109","mergeCommit":{"message":"[SecuritySolution] Asset Criticality ECS compatibility (#194109)\n\n## Summary\r\n* New asset criticality ECS fields in mappings\r\n* Schemas update\r\n* Data client update\r\n* Add check and throw an error if data migration is required\r\n* Create a mappings and data migration\r\n * When kibana starts\r\n * Check if a mappings update is required\r\n * Update mappings\r\n * Check if data migration is required\r\n * Schedule a kibana task that runs the migration\r\n\r\n\r\nNew asset criticality fields: asset, host, user\r\nTs type definition:\r\nhttps://github.com/elastic/kibana/pull/194109/files#diff-61d0a28910f5cc972f65e47ff8ba189a0b34bae0d7a0c492b88676d8059bc87dR88-R122\r\n\r\n\r\nBlocked by: https://github.com/elastic/elasticsearch/pull/113588\r\n\r\n### Checklist\r\n\r\n\r\n[x] [Unit or functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere updated or added to match the most common scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>","sha":"40eb9b279f3ad33ae7205287cec7a493e7193727"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/194109","number":194109,"mergeCommit":{"message":"[SecuritySolution] Asset Criticality ECS compatibility (#194109)\n\n## Summary\r\n* New asset criticality ECS fields in mappings\r\n* Schemas update\r\n* Data client update\r\n* Add check and throw an error if data migration is required\r\n* Create a mappings and data migration\r\n * When kibana starts\r\n * Check if a mappings update is required\r\n * Update mappings\r\n * Check if data migration is required\r\n * Schedule a kibana task that runs the migration\r\n\r\n\r\nNew asset criticality fields: asset, host, user\r\nTs type definition:\r\nhttps://github.com/elastic/kibana/pull/194109/files#diff-61d0a28910f5cc972f65e47ff8ba189a0b34bae0d7a0c492b88676d8059bc87dR88-R122\r\n\r\n\r\nBlocked by: https://github.com/elastic/elasticsearch/pull/113588\r\n\r\n### Checklist\r\n\r\n\r\n[x] [Unit or functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere updated or added to match the most common scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>","sha":"40eb9b279f3ad33ae7205287cec7a493e7193727"}}]}] BACKPORT--> Co-authored-by: Pablo Machado <pablo.nevesmachado@elastic.co>
This commit is contained in:
parent
eeee8f7d4d
commit
d8f01e8d43
26 changed files with 1151 additions and 50 deletions
|
@ -29582,6 +29582,8 @@ components:
|
|||
allOf:
|
||||
- $ref: >-
|
||||
#/components/schemas/Security_Entity_Analytics_API_CreateAssetCriticalityRecord
|
||||
- $ref: >-
|
||||
#/components/schemas/Security_Entity_Analytics_API_AssetCriticalityRecordEcsParts
|
||||
- type: object
|
||||
properties:
|
||||
'@timestamp':
|
||||
|
@ -29591,6 +29593,49 @@ components:
|
|||
type: string
|
||||
required:
|
||||
- '@timestamp'
|
||||
Security_Entity_Analytics_API_AssetCriticalityRecordEcsParts:
|
||||
type: object
|
||||
properties:
|
||||
asset:
|
||||
type: object
|
||||
properties:
|
||||
criticality:
|
||||
$ref: >-
|
||||
#/components/schemas/Security_Entity_Analytics_API_AssetCriticalityLevel
|
||||
required:
|
||||
- asset
|
||||
host:
|
||||
type: object
|
||||
properties:
|
||||
asset:
|
||||
type: object
|
||||
properties:
|
||||
criticality:
|
||||
$ref: >-
|
||||
#/components/schemas/Security_Entity_Analytics_API_AssetCriticalityLevel
|
||||
required:
|
||||
- criticality
|
||||
name:
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
user:
|
||||
type: object
|
||||
properties:
|
||||
asset:
|
||||
type: object
|
||||
properties:
|
||||
criticality:
|
||||
$ref: >-
|
||||
#/components/schemas/Security_Entity_Analytics_API_AssetCriticalityLevel
|
||||
required:
|
||||
- criticality
|
||||
name:
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
required:
|
||||
- asset
|
||||
Security_Entity_Analytics_API_AssetCriticalityRecordIdParts:
|
||||
type: object
|
||||
properties:
|
||||
|
|
|
@ -37591,6 +37591,8 @@ components:
|
|||
allOf:
|
||||
- $ref: >-
|
||||
#/components/schemas/Security_Entity_Analytics_API_CreateAssetCriticalityRecord
|
||||
- $ref: >-
|
||||
#/components/schemas/Security_Entity_Analytics_API_AssetCriticalityRecordEcsParts
|
||||
- type: object
|
||||
properties:
|
||||
'@timestamp':
|
||||
|
@ -37600,6 +37602,49 @@ components:
|
|||
type: string
|
||||
required:
|
||||
- '@timestamp'
|
||||
Security_Entity_Analytics_API_AssetCriticalityRecordEcsParts:
|
||||
type: object
|
||||
properties:
|
||||
asset:
|
||||
type: object
|
||||
properties:
|
||||
criticality:
|
||||
$ref: >-
|
||||
#/components/schemas/Security_Entity_Analytics_API_AssetCriticalityLevel
|
||||
required:
|
||||
- asset
|
||||
host:
|
||||
type: object
|
||||
properties:
|
||||
asset:
|
||||
type: object
|
||||
properties:
|
||||
criticality:
|
||||
$ref: >-
|
||||
#/components/schemas/Security_Entity_Analytics_API_AssetCriticalityLevel
|
||||
required:
|
||||
- criticality
|
||||
name:
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
user:
|
||||
type: object
|
||||
properties:
|
||||
asset:
|
||||
type: object
|
||||
properties:
|
||||
criticality:
|
||||
$ref: >-
|
||||
#/components/schemas/Security_Entity_Analytics_API_AssetCriticalityLevel
|
||||
required:
|
||||
- criticality
|
||||
name:
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
required:
|
||||
- asset
|
||||
Security_Entity_Analytics_API_AssetCriticalityRecordIdParts:
|
||||
type: object
|
||||
properties:
|
||||
|
|
|
@ -53,8 +53,37 @@ export const CreateAssetCriticalityRecord = AssetCriticalityRecordIdParts.merge(
|
|||
})
|
||||
);
|
||||
|
||||
export type AssetCriticalityRecordEcsParts = z.infer<typeof AssetCriticalityRecordEcsParts>;
|
||||
export const AssetCriticalityRecordEcsParts = z.object({
|
||||
asset: z.object({
|
||||
criticality: AssetCriticalityLevel.optional(),
|
||||
}),
|
||||
host: z
|
||||
.object({
|
||||
name: z.string(),
|
||||
asset: z
|
||||
.object({
|
||||
criticality: AssetCriticalityLevel,
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
user: z
|
||||
.object({
|
||||
name: z.string(),
|
||||
asset: z
|
||||
.object({
|
||||
criticality: AssetCriticalityLevel,
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export type AssetCriticalityRecord = z.infer<typeof AssetCriticalityRecord>;
|
||||
export const AssetCriticalityRecord = CreateAssetCriticalityRecord.merge(
|
||||
AssetCriticalityRecordEcsParts
|
||||
).merge(
|
||||
z.object({
|
||||
/**
|
||||
* The time the record was created or updated.
|
||||
|
|
|
@ -61,6 +61,7 @@ components:
|
|||
AssetCriticalityRecord:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/CreateAssetCriticalityRecord'
|
||||
- $ref: '#/components/schemas/AssetCriticalityRecordEcsParts'
|
||||
- type: object
|
||||
properties:
|
||||
'@timestamp':
|
||||
|
@ -70,3 +71,43 @@ components:
|
|||
description: The time the record was created or updated.
|
||||
required:
|
||||
- '@timestamp'
|
||||
AssetCriticalityRecordEcsParts:
|
||||
type: object
|
||||
properties:
|
||||
'asset':
|
||||
type: object
|
||||
properties:
|
||||
'criticality':
|
||||
$ref: '#/components/schemas/AssetCriticalityLevel'
|
||||
required:
|
||||
- 'asset'
|
||||
'host':
|
||||
type: object
|
||||
properties:
|
||||
'name':
|
||||
type: string
|
||||
'asset':
|
||||
type: object
|
||||
properties:
|
||||
'criticality':
|
||||
$ref: '#/components/schemas/AssetCriticalityLevel'
|
||||
required:
|
||||
- 'criticality'
|
||||
required:
|
||||
- 'name'
|
||||
'user':
|
||||
type: object
|
||||
properties:
|
||||
'name':
|
||||
type: string
|
||||
'asset':
|
||||
type: object
|
||||
properties:
|
||||
'criticality':
|
||||
$ref: '#/components/schemas/AssetCriticalityLevel'
|
||||
required:
|
||||
- 'criticality'
|
||||
required:
|
||||
- 'name'
|
||||
required:
|
||||
- 'asset'
|
||||
|
|
|
@ -577,6 +577,7 @@ components:
|
|||
AssetCriticalityRecord:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/CreateAssetCriticalityRecord'
|
||||
- $ref: '#/components/schemas/AssetCriticalityRecordEcsParts'
|
||||
- type: object
|
||||
properties:
|
||||
'@timestamp':
|
||||
|
@ -586,6 +587,46 @@ components:
|
|||
type: string
|
||||
required:
|
||||
- '@timestamp'
|
||||
AssetCriticalityRecordEcsParts:
|
||||
type: object
|
||||
properties:
|
||||
asset:
|
||||
type: object
|
||||
properties:
|
||||
criticality:
|
||||
$ref: '#/components/schemas/AssetCriticalityLevel'
|
||||
required:
|
||||
- asset
|
||||
host:
|
||||
type: object
|
||||
properties:
|
||||
asset:
|
||||
type: object
|
||||
properties:
|
||||
criticality:
|
||||
$ref: '#/components/schemas/AssetCriticalityLevel'
|
||||
required:
|
||||
- criticality
|
||||
name:
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
user:
|
||||
type: object
|
||||
properties:
|
||||
asset:
|
||||
type: object
|
||||
properties:
|
||||
criticality:
|
||||
$ref: '#/components/schemas/AssetCriticalityLevel'
|
||||
required:
|
||||
- criticality
|
||||
name:
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
required:
|
||||
- asset
|
||||
AssetCriticalityRecordIdParts:
|
||||
type: object
|
||||
properties:
|
||||
|
|
|
@ -577,6 +577,7 @@ components:
|
|||
AssetCriticalityRecord:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/CreateAssetCriticalityRecord'
|
||||
- $ref: '#/components/schemas/AssetCriticalityRecordEcsParts'
|
||||
- type: object
|
||||
properties:
|
||||
'@timestamp':
|
||||
|
@ -586,6 +587,46 @@ components:
|
|||
type: string
|
||||
required:
|
||||
- '@timestamp'
|
||||
AssetCriticalityRecordEcsParts:
|
||||
type: object
|
||||
properties:
|
||||
asset:
|
||||
type: object
|
||||
properties:
|
||||
criticality:
|
||||
$ref: '#/components/schemas/AssetCriticalityLevel'
|
||||
required:
|
||||
- asset
|
||||
host:
|
||||
type: object
|
||||
properties:
|
||||
asset:
|
||||
type: object
|
||||
properties:
|
||||
criticality:
|
||||
$ref: '#/components/schemas/AssetCriticalityLevel'
|
||||
required:
|
||||
- criticality
|
||||
name:
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
user:
|
||||
type: object
|
||||
properties:
|
||||
asset:
|
||||
type: object
|
||||
properties:
|
||||
criticality:
|
||||
$ref: '#/components/schemas/AssetCriticalityLevel'
|
||||
required:
|
||||
- criticality
|
||||
name:
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
required:
|
||||
- asset
|
||||
AssetCriticalityRecordIdParts:
|
||||
type: object
|
||||
properties:
|
||||
|
|
|
@ -9,6 +9,7 @@ import { loggingSystemMock, elasticsearchServiceMock } from '@kbn/core/server/mo
|
|||
import { AssetCriticalityDataClient } from './asset_criticality_data_client';
|
||||
import { createOrUpdateIndex } from '../utils/create_or_update_index';
|
||||
import { auditLoggerMock } from '@kbn/security-plugin/server/audit/mocks';
|
||||
import type { AssetCriticalityUpsert } from '../../../../common/entity_analytics/asset_criticality/types';
|
||||
|
||||
type MockInternalEsClient = ReturnType<
|
||||
typeof elasticsearchServiceMock.createScopedClusterClient
|
||||
|
@ -57,6 +58,41 @@ describe('AssetCriticalityDataClient', () => {
|
|||
updated_at: {
|
||||
type: 'date',
|
||||
},
|
||||
asset: {
|
||||
properties: {
|
||||
criticality: {
|
||||
type: 'keyword',
|
||||
},
|
||||
},
|
||||
},
|
||||
host: {
|
||||
properties: {
|
||||
asset: {
|
||||
properties: {
|
||||
criticality: {
|
||||
type: 'keyword',
|
||||
},
|
||||
},
|
||||
},
|
||||
name: {
|
||||
type: 'keyword',
|
||||
},
|
||||
},
|
||||
},
|
||||
user: {
|
||||
properties: {
|
||||
asset: {
|
||||
properties: {
|
||||
criticality: {
|
||||
type: 'keyword',
|
||||
},
|
||||
},
|
||||
},
|
||||
name: {
|
||||
type: 'keyword',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -141,4 +177,91 @@ describe('AssetCriticalityDataClient', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#upsert()', () => {
|
||||
let esClientMock: MockInternalEsClient;
|
||||
let loggerMock: ReturnType<typeof loggingSystemMock.createLogger>;
|
||||
let subject: AssetCriticalityDataClient;
|
||||
|
||||
beforeEach(() => {
|
||||
esClientMock = elasticsearchServiceMock.createScopedClusterClient().asInternalUser;
|
||||
loggerMock = loggingSystemMock.createLogger();
|
||||
subject = new AssetCriticalityDataClient({
|
||||
esClient: esClientMock,
|
||||
logger: loggerMock,
|
||||
namespace: 'default',
|
||||
auditLogger: mockAuditLogger,
|
||||
});
|
||||
});
|
||||
|
||||
it('created "host" records in the asset criticality index', async () => {
|
||||
const record: AssetCriticalityUpsert = {
|
||||
idField: 'host.name',
|
||||
idValue: 'host1',
|
||||
criticalityLevel: 'high_impact',
|
||||
};
|
||||
|
||||
await subject.upsert(record);
|
||||
|
||||
expect(esClientMock.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: 'host.name:host1',
|
||||
index: '.asset-criticality.asset-criticality-default',
|
||||
body: {
|
||||
doc: {
|
||||
id_field: 'host.name',
|
||||
id_value: 'host1',
|
||||
criticality_level: 'high_impact',
|
||||
'@timestamp': expect.any(String),
|
||||
asset: {
|
||||
criticality: 'high_impact',
|
||||
},
|
||||
host: {
|
||||
name: 'host1',
|
||||
asset: {
|
||||
criticality: 'high_impact',
|
||||
},
|
||||
},
|
||||
},
|
||||
doc_as_upsert: true,
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('created "user" records in the asset criticality index', async () => {
|
||||
const record: AssetCriticalityUpsert = {
|
||||
idField: 'user.name',
|
||||
idValue: 'user1',
|
||||
criticalityLevel: 'medium_impact',
|
||||
};
|
||||
|
||||
await subject.upsert(record);
|
||||
|
||||
expect(esClientMock.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: 'user.name:user1',
|
||||
index: '.asset-criticality.asset-criticality-default',
|
||||
body: {
|
||||
doc: {
|
||||
id_field: 'user.name',
|
||||
id_value: 'user1',
|
||||
criticality_level: 'medium_impact',
|
||||
'@timestamp': expect.any(String),
|
||||
asset: {
|
||||
criticality: 'medium_impact',
|
||||
},
|
||||
user: {
|
||||
name: 'user1',
|
||||
asset: {
|
||||
criticality: 'medium_impact',
|
||||
},
|
||||
},
|
||||
},
|
||||
doc_as_upsert: true,
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -22,6 +22,7 @@ import type { CriticalityValues } from './constants';
|
|||
import { CRITICALITY_VALUES, assetCriticalityFieldMap } from './constants';
|
||||
import { AssetCriticalityAuditActions } from './audit';
|
||||
import { AUDIT_CATEGORY, AUDIT_OUTCOME, AUDIT_TYPE } from '../audit';
|
||||
import { getImplicitEntityFields } from './helpers';
|
||||
|
||||
interface AssetCriticalityClientOpts {
|
||||
logger: Logger;
|
||||
|
@ -49,19 +50,12 @@ const createId = ({ idField, idValue }: AssetCriticalityIdParts) => `${idField}:
|
|||
|
||||
export class AssetCriticalityDataClient {
|
||||
constructor(private readonly options: AssetCriticalityClientOpts) {}
|
||||
|
||||
/**
|
||||
* It will create idex for asset criticality,
|
||||
* or update mappings if index exists
|
||||
* Initialize asset criticality resources.
|
||||
*/
|
||||
public async init() {
|
||||
await createOrUpdateIndex({
|
||||
esClient: this.options.esClient,
|
||||
logger: this.options.logger,
|
||||
options: {
|
||||
index: this.getIndex(),
|
||||
mappings: mappingFromFieldMap(assetCriticalityFieldMap, 'strict'),
|
||||
},
|
||||
});
|
||||
await this.createOrUpdateIndex();
|
||||
|
||||
this.options.auditLogger?.log({
|
||||
message: 'User installed asset criticality Elasticsearch resources',
|
||||
|
@ -74,6 +68,20 @@ export class AssetCriticalityDataClient {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* It will create idex for asset criticality or update mappings if index exists
|
||||
*/
|
||||
public async createOrUpdateIndex() {
|
||||
await createOrUpdateIndex({
|
||||
esClient: this.options.esClient,
|
||||
logger: this.options.logger,
|
||||
options: {
|
||||
index: this.getIndex(),
|
||||
mappings: mappingFromFieldMap(assetCriticalityFieldMap, 'strict'),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* A general method for searching asset criticality records.
|
||||
|
@ -131,7 +139,7 @@ export class AssetCriticalityDataClient {
|
|||
});
|
||||
}
|
||||
|
||||
private getIndex() {
|
||||
public getIndex() {
|
||||
return getAssetCriticalityIndex(this.options.namespace);
|
||||
}
|
||||
|
||||
|
@ -165,6 +173,12 @@ export class AssetCriticalityDataClient {
|
|||
};
|
||||
}
|
||||
|
||||
public getIndexMappings() {
|
||||
return this.options.esClient.indices.getMapping({
|
||||
index: this.getIndex(),
|
||||
});
|
||||
}
|
||||
|
||||
public async get(idParts: AssetCriticalityIdParts): Promise<AssetCriticalityRecord | undefined> {
|
||||
const id = createId(idParts);
|
||||
|
||||
|
@ -193,11 +207,15 @@ export class AssetCriticalityDataClient {
|
|||
refresh = 'wait_for' as const
|
||||
): Promise<AssetCriticalityRecord> {
|
||||
const id = createId(record);
|
||||
const doc = {
|
||||
const doc: AssetCriticalityRecord = {
|
||||
id_field: record.idField,
|
||||
id_value: record.idValue,
|
||||
criticality_level: record.criticalityLevel,
|
||||
'@timestamp': new Date().toISOString(),
|
||||
asset: {
|
||||
criticality: record.criticalityLevel,
|
||||
},
|
||||
...getImplicitEntityFields(record),
|
||||
};
|
||||
|
||||
await this.options.esClient.update({
|
||||
|
@ -272,6 +290,10 @@ export class AssetCriticalityDataClient {
|
|||
id_field: record.idField,
|
||||
id_value: record.idValue,
|
||||
criticality_level: record.criticalityLevel,
|
||||
asset: {
|
||||
criticality: record.criticalityLevel,
|
||||
},
|
||||
...getImplicitEntityFields(record),
|
||||
'@timestamp': new Date().toISOString(),
|
||||
},
|
||||
doc_as_upsert: true,
|
||||
|
@ -316,7 +338,14 @@ export class AssetCriticalityDataClient {
|
|||
index: this.getIndex(),
|
||||
refresh: refresh ?? false,
|
||||
doc: {
|
||||
criticality_level: 'deleted',
|
||||
criticality_level: CRITICALITY_VALUES.DELETED,
|
||||
asset: {
|
||||
criticality: CRITICALITY_VALUES.DELETED,
|
||||
},
|
||||
...getImplicitEntityFields({
|
||||
...idParts,
|
||||
criticalityLevel: CRITICALITY_VALUES.DELETED,
|
||||
}),
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
|
|
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* 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 { AssetCriticalityEcsMigrationClient } from './asset_criticality_migration_client';
|
||||
import { AssetCriticalityDataClient } from './asset_criticality_data_client';
|
||||
import type { Logger, ElasticsearchClient } from '@kbn/core/server';
|
||||
import type { AuditLogger } from '@kbn/security-plugin-types-server';
|
||||
|
||||
jest.mock('./asset_criticality_data_client');
|
||||
|
||||
const emptySearchResponse = {
|
||||
took: 1,
|
||||
timed_out: false,
|
||||
_shards: { total: 1, successful: 1, skipped: 0, failed: 0 },
|
||||
hits: { hits: [] },
|
||||
};
|
||||
|
||||
describe('AssetCriticalityEcsMigrationClient', () => {
|
||||
let logger: Logger;
|
||||
let auditLogger: AuditLogger | undefined;
|
||||
let esClient: ElasticsearchClient;
|
||||
let assetCriticalityDataClient: jest.Mocked<AssetCriticalityDataClient>;
|
||||
let migrationClient: AssetCriticalityEcsMigrationClient;
|
||||
|
||||
beforeEach(() => {
|
||||
logger = { info: jest.fn(), error: jest.fn() } as unknown as Logger;
|
||||
auditLogger = undefined;
|
||||
esClient = { updateByQuery: jest.fn() } as unknown as ElasticsearchClient;
|
||||
assetCriticalityDataClient = new AssetCriticalityDataClient({
|
||||
logger,
|
||||
auditLogger,
|
||||
esClient,
|
||||
namespace: '*',
|
||||
}) as jest.Mocked<AssetCriticalityDataClient>;
|
||||
|
||||
(AssetCriticalityDataClient as jest.Mock).mockImplementation(() => assetCriticalityDataClient);
|
||||
|
||||
migrationClient = new AssetCriticalityEcsMigrationClient({ logger, auditLogger, esClient });
|
||||
});
|
||||
|
||||
describe('isEcsMappingsMigrationRequired', () => {
|
||||
it('should return true if any index mappings do not have asset property', async () => {
|
||||
assetCriticalityDataClient.getIndexMappings.mockResolvedValue({
|
||||
index1: { mappings: { properties: {} } },
|
||||
index2: { mappings: { properties: { asset: {} } } },
|
||||
});
|
||||
|
||||
const result = await migrationClient.isEcsMappingsMigrationRequired();
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if all index mappings have asset property', async () => {
|
||||
assetCriticalityDataClient.getIndexMappings.mockResolvedValue({
|
||||
index1: { mappings: { properties: { asset: {} } } },
|
||||
index2: { mappings: { properties: { asset: {} } } },
|
||||
});
|
||||
|
||||
const result = await migrationClient.isEcsMappingsMigrationRequired();
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isEcsDataMigrationRequired', () => {
|
||||
it('should return true if there are documents without asset.criticality field', async () => {
|
||||
assetCriticalityDataClient.search.mockResolvedValue({
|
||||
...emptySearchResponse,
|
||||
hits: { hits: [{ _index: 'test-index' }] },
|
||||
});
|
||||
|
||||
const result = await migrationClient.isEcsDataMigrationRequired();
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if all documents have asset.criticality field', async () => {
|
||||
assetCriticalityDataClient.search.mockResolvedValue(emptySearchResponse);
|
||||
|
||||
const result = await migrationClient.isEcsDataMigrationRequired();
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('migrateEcsMappings', () => {
|
||||
it('should call createOrUpdateIndex on assetCriticalityDataClient', async () => {
|
||||
await migrationClient.migrateEcsMappings();
|
||||
expect(assetCriticalityDataClient.createOrUpdateIndex).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('migrateEcsData', () => {
|
||||
it('should call updateByQuery on esClient with correct parameters', async () => {
|
||||
await migrationClient.migrateEcsData();
|
||||
expect(esClient.updateByQuery).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
index: assetCriticalityDataClient.getIndex(),
|
||||
body: expect.objectContaining({
|
||||
query: expect.any(Object),
|
||||
script: expect.any(Object),
|
||||
}),
|
||||
}),
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import type { Logger, ElasticsearchClient } from '@kbn/core/server';
|
||||
import type { AuditLogger } from '@kbn/security-plugin-types-server';
|
||||
import { AssetCriticalityDataClient } from './asset_criticality_data_client';
|
||||
|
||||
interface AssetCriticalityEcsMigrationClientOpts {
|
||||
logger: Logger;
|
||||
auditLogger: AuditLogger | undefined;
|
||||
esClient: ElasticsearchClient;
|
||||
}
|
||||
|
||||
const ECS_MAPPINGS_MIGRATION_QUERY = {
|
||||
bool: {
|
||||
must_not: {
|
||||
exists: {
|
||||
field: 'asset.criticality',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const PAINLESS_SCRIPT = `
|
||||
Map asset = new HashMap();
|
||||
asset.put('criticality', ctx._source.criticality_level);
|
||||
ctx._source.asset = asset;
|
||||
if (ctx._source.id_field == 'user.name') {
|
||||
Map user = new HashMap();
|
||||
user.put('name', ctx._source.id_value);
|
||||
user.put('asset', asset);
|
||||
ctx._source.user = user;
|
||||
} else {
|
||||
Map host = new HashMap();
|
||||
host.put('name', ctx._source.id_value);
|
||||
host.put('asset', asset);
|
||||
ctx._source.host = host;
|
||||
}`;
|
||||
|
||||
export class AssetCriticalityEcsMigrationClient {
|
||||
private readonly assetCriticalityDataClient: AssetCriticalityDataClient;
|
||||
constructor(private readonly options: AssetCriticalityEcsMigrationClientOpts) {
|
||||
this.assetCriticalityDataClient = new AssetCriticalityDataClient({
|
||||
...options,
|
||||
namespace: '*', // The migration is applied to all spaces
|
||||
});
|
||||
}
|
||||
|
||||
public isEcsMappingsMigrationRequired = async () => {
|
||||
const indicesMappings = await this.assetCriticalityDataClient.getIndexMappings();
|
||||
|
||||
return Object.values(indicesMappings).some(
|
||||
({ mappings }) => mappings?.properties?.asset === undefined
|
||||
);
|
||||
};
|
||||
|
||||
public isEcsDataMigrationRequired = async () => {
|
||||
const resp = await this.assetCriticalityDataClient.search({
|
||||
query: ECS_MAPPINGS_MIGRATION_QUERY,
|
||||
size: 1,
|
||||
});
|
||||
|
||||
return resp.hits.hits.length > 0;
|
||||
};
|
||||
|
||||
public migrateEcsMappings = () => {
|
||||
return this.assetCriticalityDataClient.createOrUpdateIndex();
|
||||
};
|
||||
|
||||
public migrateEcsData = (abortSignal?: AbortSignal) => {
|
||||
return this.options.esClient.updateByQuery(
|
||||
{
|
||||
index: this.assetCriticalityDataClient.getIndex(),
|
||||
conflicts: 'proceed',
|
||||
ignore_unavailable: true,
|
||||
allow_no_indices: true,
|
||||
scroll_size: 1000,
|
||||
body: {
|
||||
query: ECS_MAPPINGS_MIGRATION_QUERY,
|
||||
script: {
|
||||
source: PAINLESS_SCRIPT,
|
||||
lang: 'painless',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
retryOnTimeout: true,
|
||||
maxRetries: 2,
|
||||
signal: abortSignal,
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
|
@ -25,6 +25,15 @@ const buildMockCriticalityHit = (
|
|||
id_field: 'host.name',
|
||||
id_value: 'hostname',
|
||||
criticality_level: 'medium_impact',
|
||||
asset: {
|
||||
criticality: 'medium_impact',
|
||||
},
|
||||
host: {
|
||||
name: 'hostname',
|
||||
asset: {
|
||||
criticality: 'medium_impact',
|
||||
},
|
||||
},
|
||||
...overrides,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -9,7 +9,7 @@ import { AssetCriticalityDataClient } from './asset_criticality_data_client';
|
|||
import type { SecuritySolutionRequestHandlerContext } from '../../../types';
|
||||
|
||||
/**
|
||||
* As internal user we check for existence of asset crititcality resources
|
||||
* As internal user we check for existence of asset criticality resources
|
||||
* and initialise it if it does not exist
|
||||
* @param context
|
||||
* @param logger
|
||||
|
|
|
@ -10,6 +10,12 @@ import type { AssetCriticalityRecord } from '../../../../common/api/entity_analy
|
|||
|
||||
export type CriticalityValues = AssetCriticalityRecord['criticality_level'] | 'deleted';
|
||||
|
||||
const assetCriticalityMapping = {
|
||||
type: 'keyword',
|
||||
array: false,
|
||||
required: false,
|
||||
};
|
||||
|
||||
export const assetCriticalityFieldMap: FieldMap = {
|
||||
'@timestamp': {
|
||||
type: 'date',
|
||||
|
@ -26,16 +32,29 @@ export const assetCriticalityFieldMap: FieldMap = {
|
|||
array: false,
|
||||
required: false,
|
||||
},
|
||||
criticality_level: {
|
||||
type: 'keyword',
|
||||
array: false,
|
||||
required: false,
|
||||
},
|
||||
criticality_level: assetCriticalityMapping,
|
||||
updated_at: {
|
||||
type: 'date',
|
||||
array: false,
|
||||
required: false,
|
||||
},
|
||||
'asset.criticality': {
|
||||
type: 'keyword',
|
||||
array: false,
|
||||
required: true,
|
||||
},
|
||||
'host.name': {
|
||||
type: 'keyword',
|
||||
array: false,
|
||||
required: false,
|
||||
},
|
||||
'host.asset.criticality': assetCriticalityMapping,
|
||||
'user.name': {
|
||||
type: 'keyword',
|
||||
array: false,
|
||||
required: false,
|
||||
},
|
||||
'user.asset.criticality': assetCriticalityMapping,
|
||||
} as const;
|
||||
|
||||
export const CRITICALITY_VALUES: { readonly [K in CriticalityValues as Uppercase<K>]: K } = {
|
||||
|
|
|
@ -6,8 +6,12 @@
|
|||
*/
|
||||
|
||||
import { CriticalityModifiers } from '../../../../common/entity_analytics/asset_criticality';
|
||||
import type { CriticalityLevel } from '../../../../common/entity_analytics/asset_criticality/types';
|
||||
import type {
|
||||
AssetCriticalityUpsert,
|
||||
CriticalityLevel,
|
||||
} from '../../../../common/entity_analytics/asset_criticality/types';
|
||||
import { RISK_SCORING_NORMALIZATION_MAX } from '../risk_score/constants';
|
||||
import type { CriticalityValues } from './constants';
|
||||
|
||||
/**
|
||||
* Retrieves the criticality modifier for a given criticality level.
|
||||
|
@ -65,3 +69,19 @@ export const bayesianUpdate = ({
|
|||
const newProbability = priorProbability * modifier;
|
||||
return (max * newProbability) / (1 + newProbability);
|
||||
};
|
||||
|
||||
type AssetCriticalityUpsertWithDeleted = {
|
||||
[K in keyof AssetCriticalityUpsert]: K extends 'criticalityLevel'
|
||||
? CriticalityValues
|
||||
: AssetCriticalityUpsert[K];
|
||||
};
|
||||
|
||||
export const getImplicitEntityFields = (record: AssetCriticalityUpsertWithDeleted) => {
|
||||
const entityType = record.idField === 'host.name' ? 'host' : 'user';
|
||||
return {
|
||||
[entityType]: {
|
||||
asset: { criticality: record.criticalityLevel },
|
||||
name: record.idValue,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -0,0 +1,212 @@
|
|||
/*
|
||||
* 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 {
|
||||
createMigrationTask,
|
||||
scheduleAssetCriticalityEcsCompliancyMigration,
|
||||
} from './schedule_ecs_compliancy_migration';
|
||||
|
||||
import { loggerMock } from '@kbn/logging-mocks';
|
||||
import { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks';
|
||||
import { elasticsearchServiceMock } from '@kbn/core-elasticsearch-server-mocks';
|
||||
import { auditLoggerMock } from '@kbn/core-security-server-mocks';
|
||||
|
||||
const mockMigrateEcsMappings = jest.fn().mockResolvedValue(false);
|
||||
const mockIsEcsMappingsMigrationRequired = jest.fn().mockResolvedValue(false);
|
||||
const mockIsEcsDataMigrationRequired = jest.fn().mockResolvedValue(false);
|
||||
const mockMigrateEcsData = jest.fn().mockResolvedValue({
|
||||
updated: 100,
|
||||
failures: [],
|
||||
});
|
||||
jest.mock('../asset_criticality_migration_client', () => ({
|
||||
AssetCriticalityEcsMigrationClient: jest.fn().mockImplementation(() => ({
|
||||
isEcsMappingsMigrationRequired: mockIsEcsMappingsMigrationRequired,
|
||||
isEcsDataMigrationRequired: mockIsEcsDataMigrationRequired,
|
||||
migrateEcsMappings: mockMigrateEcsMappings,
|
||||
migrateEcsData: mockMigrateEcsData,
|
||||
})),
|
||||
}));
|
||||
const mockTaskManagerStart = taskManagerMock.createStart();
|
||||
const logger = loggerMock.create();
|
||||
const auditLogger = auditLoggerMock.create();
|
||||
|
||||
const getStartServices = jest.fn().mockResolvedValue([
|
||||
{
|
||||
elasticsearch: {
|
||||
client: elasticsearchServiceMock.createClusterClient(),
|
||||
},
|
||||
},
|
||||
{ taskManager: mockTaskManagerStart },
|
||||
]);
|
||||
const mockAbortController = {
|
||||
abort: jest.fn(),
|
||||
signal: {},
|
||||
};
|
||||
|
||||
global.AbortController = jest.fn().mockImplementation(() => mockAbortController);
|
||||
|
||||
describe('scheduleAssetCriticalityEcsCompliancyMigration', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should register the task if taskManager is available', async () => {
|
||||
const taskManager = taskManagerMock.createSetup();
|
||||
await scheduleAssetCriticalityEcsCompliancyMigration({
|
||||
auditLogger,
|
||||
taskManager,
|
||||
logger,
|
||||
getStartServices,
|
||||
});
|
||||
|
||||
expect(taskManager.registerTaskDefinitions).toHaveBeenCalledWith({
|
||||
'security-solution-ea-asset-criticality-ecs-migration': expect.any(Object),
|
||||
});
|
||||
});
|
||||
|
||||
it('should not register the task if taskManager is not available', async () => {
|
||||
await expect(
|
||||
scheduleAssetCriticalityEcsCompliancyMigration({
|
||||
auditLogger,
|
||||
taskManager: undefined,
|
||||
logger,
|
||||
getStartServices,
|
||||
})
|
||||
).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it('should migrate mappings if required', async () => {
|
||||
const taskManager = taskManagerMock.createSetup();
|
||||
|
||||
mockIsEcsMappingsMigrationRequired.mockResolvedValue(true);
|
||||
|
||||
await scheduleAssetCriticalityEcsCompliancyMigration({
|
||||
auditLogger,
|
||||
taskManager,
|
||||
logger,
|
||||
getStartServices,
|
||||
});
|
||||
|
||||
expect(mockMigrateEcsMappings).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not migrate mappings if not required', async () => {
|
||||
const taskManager = taskManagerMock.createSetup();
|
||||
|
||||
mockIsEcsMappingsMigrationRequired.mockResolvedValue(false);
|
||||
|
||||
await scheduleAssetCriticalityEcsCompliancyMigration({
|
||||
auditLogger,
|
||||
taskManager,
|
||||
logger,
|
||||
getStartServices,
|
||||
});
|
||||
|
||||
expect(mockMigrateEcsMappings).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should schedule the task if data migration is required', async () => {
|
||||
mockIsEcsDataMigrationRequired.mockResolvedValue(true);
|
||||
|
||||
await scheduleAssetCriticalityEcsCompliancyMigration({
|
||||
auditLogger,
|
||||
taskManager: taskManagerMock.createSetup(),
|
||||
logger,
|
||||
getStartServices,
|
||||
});
|
||||
|
||||
expect(mockTaskManagerStart.ensureScheduled).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: 'security-solution-ea-asset-criticality-ecs-migration-task-id',
|
||||
taskType: 'security-solution-ea-asset-criticality-ecs-migration',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should log an error if scheduling the task fails', async () => {
|
||||
mockIsEcsDataMigrationRequired.mockResolvedValue(true);
|
||||
mockTaskManagerStart.ensureScheduled.mockRejectedValue(new Error('Failed to schedule task'));
|
||||
|
||||
await scheduleAssetCriticalityEcsCompliancyMigration({
|
||||
auditLogger,
|
||||
taskManager: taskManagerMock.createSetup(),
|
||||
logger,
|
||||
getStartServices,
|
||||
});
|
||||
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
'Error scheduling security-solution-ea-asset-criticality-ecs-migration-task-id, received Failed to schedule task'
|
||||
);
|
||||
});
|
||||
|
||||
it('should not schedule the task if data migration is not required', async () => {
|
||||
mockIsEcsDataMigrationRequired.mockResolvedValue(false);
|
||||
|
||||
await scheduleAssetCriticalityEcsCompliancyMigration({
|
||||
auditLogger,
|
||||
taskManager: taskManagerMock.createSetup(),
|
||||
logger,
|
||||
getStartServices,
|
||||
});
|
||||
|
||||
expect(mockTaskManagerStart.ensureScheduled).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('#createMigrationTask', () => {
|
||||
it('should run the migration task and log the result', async () => {
|
||||
const migrationTask = createMigrationTask({
|
||||
getStartServices,
|
||||
logger,
|
||||
auditLogger,
|
||||
})();
|
||||
|
||||
await migrationTask.run();
|
||||
|
||||
expect(mockMigrateEcsData).toHaveBeenCalled();
|
||||
expect(logger.info).toHaveBeenCalledWith(
|
||||
'Task "security-solution-ea-asset-criticality-ecs-migration" finished. Updated documents: 100, failures: 0'
|
||||
);
|
||||
});
|
||||
|
||||
it('should log failures if there are any', async () => {
|
||||
mockMigrateEcsData.mockResolvedValueOnce({
|
||||
updated: 50,
|
||||
failures: [{ cause: 'Error 1' }, { cause: 'Error 2' }],
|
||||
});
|
||||
|
||||
const migrationTask = createMigrationTask({
|
||||
getStartServices,
|
||||
logger,
|
||||
auditLogger,
|
||||
})();
|
||||
|
||||
await migrationTask.run();
|
||||
|
||||
expect(mockMigrateEcsData).toHaveBeenCalled();
|
||||
expect(logger.info).toHaveBeenCalledWith(
|
||||
'Task "security-solution-ea-asset-criticality-ecs-migration" finished. Updated documents: 50, failures: Error 1\nError 2'
|
||||
);
|
||||
});
|
||||
|
||||
it('should abort request and log when the task is cancelled', async () => {
|
||||
const migrationTask = createMigrationTask({
|
||||
getStartServices,
|
||||
logger,
|
||||
auditLogger,
|
||||
})();
|
||||
|
||||
await migrationTask.run();
|
||||
await migrationTask.cancel();
|
||||
|
||||
expect(mockAbortController.abort).toHaveBeenCalled();
|
||||
|
||||
expect(logger.debug).toHaveBeenCalledWith(
|
||||
'Task cancelled: "security-solution-ea-asset-criticality-ecs-migration"'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* 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 { EntityAnalyticsMigrationsParams } from '../../migrations';
|
||||
import { AssetCriticalityEcsMigrationClient } from '../asset_criticality_migration_client';
|
||||
|
||||
const TASK_TYPE = 'security-solution-ea-asset-criticality-ecs-migration';
|
||||
const TASK_ID = `${TASK_TYPE}-task-id`;
|
||||
const TASK_TIMEOUT = '15m';
|
||||
const TASK_SCOPE = ['securitySolution'];
|
||||
|
||||
export const scheduleAssetCriticalityEcsCompliancyMigration = async ({
|
||||
auditLogger,
|
||||
taskManager,
|
||||
logger,
|
||||
getStartServices,
|
||||
}: EntityAnalyticsMigrationsParams) => {
|
||||
if (!taskManager) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug(`Register task "${TASK_TYPE}"`);
|
||||
|
||||
taskManager.registerTaskDefinitions({
|
||||
[TASK_TYPE]: {
|
||||
title: `Migrate Asset Criticality index data to be ECS compliant`,
|
||||
timeout: TASK_TIMEOUT,
|
||||
createTaskRunner: createMigrationTask({ auditLogger, logger, getStartServices }),
|
||||
},
|
||||
});
|
||||
|
||||
const [coreStart, depsStart] = await getStartServices();
|
||||
const taskManagerStart = depsStart.taskManager;
|
||||
const esClient = coreStart.elasticsearch.client.asInternalUser;
|
||||
|
||||
const migrationClient = new AssetCriticalityEcsMigrationClient({
|
||||
esClient,
|
||||
logger,
|
||||
auditLogger,
|
||||
});
|
||||
|
||||
const shouldMigrateMappings = await migrationClient.isEcsMappingsMigrationRequired();
|
||||
if (shouldMigrateMappings) {
|
||||
logger.debug('Migrating Asset Criticality mappings');
|
||||
await migrationClient.migrateEcsMappings();
|
||||
}
|
||||
|
||||
const shouldMigrateData = await migrationClient.isEcsDataMigrationRequired();
|
||||
if (shouldMigrateData && taskManagerStart) {
|
||||
logger.debug(`Task scheduled: "${TASK_TYPE}"`);
|
||||
|
||||
const now = new Date();
|
||||
try {
|
||||
await taskManagerStart.ensureScheduled({
|
||||
id: TASK_ID,
|
||||
taskType: TASK_TYPE,
|
||||
scheduledAt: now,
|
||||
runAt: now,
|
||||
scope: TASK_SCOPE,
|
||||
params: {},
|
||||
state: {},
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error(`Error scheduling ${TASK_ID}, received ${e.message}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const createMigrationTask =
|
||||
({
|
||||
getStartServices,
|
||||
logger,
|
||||
auditLogger,
|
||||
}: Pick<EntityAnalyticsMigrationsParams, 'getStartServices' | 'logger' | 'auditLogger'>) =>
|
||||
() => {
|
||||
let abortController: AbortController;
|
||||
return {
|
||||
run: async () => {
|
||||
abortController = new AbortController();
|
||||
const [coreStart] = await getStartServices();
|
||||
const esClient = coreStart.elasticsearch.client.asInternalUser;
|
||||
const migrationClient = new AssetCriticalityEcsMigrationClient({
|
||||
esClient,
|
||||
logger,
|
||||
auditLogger,
|
||||
});
|
||||
|
||||
const response = await migrationClient.migrateEcsData(abortController.signal);
|
||||
const failures = response.failures?.map((failure) => failure.cause);
|
||||
const hasFailures = failures && failures?.length > 0;
|
||||
|
||||
logger.info(
|
||||
`Task "${TASK_TYPE}" finished. Updated documents: ${response.updated}, failures: ${
|
||||
hasFailures ? failures.join('\n') : 0
|
||||
}`
|
||||
);
|
||||
},
|
||||
|
||||
cancel: async () => {
|
||||
abortController.abort();
|
||||
logger.debug(`Task cancelled: "${TASK_TYPE}"`);
|
||||
},
|
||||
};
|
||||
};
|
|
@ -8,6 +8,7 @@ import type { IKibanaResponse, Logger } from '@kbn/core/server';
|
|||
import { buildSiemResponse } from '@kbn/lists-plugin/server/routes/utils';
|
||||
import { transformError } from '@kbn/securitysolution-es-utils';
|
||||
import { buildRouteValidationWithZod } from '@kbn/zod-helpers';
|
||||
import type { AssetCriticalityUpsert } from '../../../../../common/entity_analytics/asset_criticality/types';
|
||||
import {
|
||||
CreateAssetCriticalityRecordRequestBody,
|
||||
type CreateAssetCriticalityRecordResponse,
|
||||
|
@ -58,7 +59,7 @@ export const assetCriticalityPublicUpsertRoute = (
|
|||
const securitySolution = await context.securitySolution;
|
||||
const assetCriticalityClient = securitySolution.getAssetCriticalityDataClient();
|
||||
|
||||
const assetCriticalityRecord = {
|
||||
const assetCriticalityRecord: AssetCriticalityUpsert = {
|
||||
idField: request.body.id_field,
|
||||
idValue: request.body.id_value,
|
||||
criticalityLevel: request.body.criticality_level,
|
||||
|
|
|
@ -14,6 +14,7 @@ import { EntityStoreDataClient } from './entity_store_data_client';
|
|||
import { EntityClient } from '@kbn/entityManager-plugin/server/lib/entity_client';
|
||||
import type { SortOrder } from '@elastic/elasticsearch/lib/api/types';
|
||||
import type { EntityType } from '../../../../common/api/entity_analytics/entity_store/common.gen';
|
||||
import { AssetCriticalityEcsMigrationClient } from '../asset_criticality/asset_criticality_migration_client';
|
||||
|
||||
describe('EntityStoreDataClient', () => {
|
||||
const logger = loggingSystemMock.createLogger();
|
||||
|
@ -30,6 +31,11 @@ describe('EntityStoreDataClient', () => {
|
|||
soClient: mockSavedObjectClient,
|
||||
logger,
|
||||
}),
|
||||
assetCriticalityMigrationClient: new AssetCriticalityEcsMigrationClient({
|
||||
esClient: esClientMock,
|
||||
logger: loggerMock,
|
||||
auditLogger: undefined,
|
||||
}),
|
||||
});
|
||||
|
||||
const defaultSearchParams = {
|
||||
|
|
|
@ -23,11 +23,13 @@ import type {
|
|||
import { EngineDescriptorClient } from './saved_object/engine_descriptor';
|
||||
import { getEntitiesIndexName, getEntityDefinition } from './utils/utils';
|
||||
import { ENGINE_STATUS, MAX_SEARCH_RESPONSE_SIZE } from './constants';
|
||||
import type { AssetCriticalityEcsMigrationClient } from '../asset_criticality/asset_criticality_migration_client';
|
||||
|
||||
interface EntityStoreClientOpts {
|
||||
logger: Logger;
|
||||
esClient: ElasticsearchClient;
|
||||
entityClient: EntityClient;
|
||||
assetCriticalityMigrationClient: AssetCriticalityEcsMigrationClient;
|
||||
namespace: string;
|
||||
soClient: SavedObjectsClientContract;
|
||||
}
|
||||
|
@ -43,6 +45,7 @@ interface SearchEntitiesParams {
|
|||
|
||||
export class EntityStoreDataClient {
|
||||
private engineClient: EngineDescriptorClient;
|
||||
|
||||
constructor(private readonly options: EntityStoreClientOpts) {
|
||||
this.engineClient = new EngineDescriptorClient({
|
||||
soClient: options.soClient,
|
||||
|
@ -54,12 +57,21 @@ export class EntityStoreDataClient {
|
|||
entityType: EntityType,
|
||||
{ indexPattern = '', filter = '' }: InitEntityEngineRequestBody
|
||||
): Promise<InitEntityEngineResponse> {
|
||||
const { entityClient, assetCriticalityMigrationClient, logger } = this.options;
|
||||
const requiresMigration = await assetCriticalityMigrationClient.isEcsDataMigrationRequired();
|
||||
|
||||
if (requiresMigration) {
|
||||
throw new Error(
|
||||
'Asset criticality data migration is required before initializing entity store. If this error persists, please restart Kibana.'
|
||||
);
|
||||
}
|
||||
|
||||
const definition = getEntityDefinition(entityType, this.options.namespace);
|
||||
|
||||
this.options.logger.info(`Initializing entity store for ${entityType}`);
|
||||
logger.info(`Initializing entity store for ${entityType}`);
|
||||
|
||||
const descriptor = await this.engineClient.init(entityType, definition, filter);
|
||||
await this.options.entityClient.createEntityDefinition({
|
||||
await entityClient.createEntityDefinition({
|
||||
definition: {
|
||||
...definition,
|
||||
filter,
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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 { AuditLogger, Logger, StartServicesAccessor } from '@kbn/core/server';
|
||||
import type { TaskManagerSetupContract } from '@kbn/task-manager-plugin/server';
|
||||
import type { StartPlugins } from '../../../plugin';
|
||||
import { scheduleAssetCriticalityEcsCompliancyMigration } from '../asset_criticality/migrations/schedule_ecs_compliancy_migration';
|
||||
|
||||
export interface EntityAnalyticsMigrationsParams {
|
||||
taskManager?: TaskManagerSetupContract;
|
||||
logger: Logger;
|
||||
getStartServices: StartServicesAccessor<StartPlugins>;
|
||||
auditLogger: AuditLogger | undefined;
|
||||
}
|
||||
|
||||
export const scheduleEntityAnalyticsMigration = async (params: EntityAnalyticsMigrationsParams) => {
|
||||
const scopedLogger = params.logger.get('entityAnalytics.migration');
|
||||
|
||||
await scheduleAssetCriticalityEcsCompliancyMigration({ ...params, logger: scopedLogger });
|
||||
};
|
|
@ -124,6 +124,7 @@ import { isEndpointPackageV2 } from '../common/endpoint/utils/package_v2';
|
|||
import { getAssistantTools } from './assistant/tools';
|
||||
import { turnOffAgentPolicyFeatures } from './endpoint/migrations/turn_off_agent_policy_features';
|
||||
import { getCriblPackagePolicyPostCreateOrUpdateCallback } from './security_integrations';
|
||||
import { scheduleEntityAnalyticsMigration } from './lib/entity_analytics/migrations';
|
||||
|
||||
export type { SetupPlugins, StartPlugins, PluginSetup, PluginStart } from './plugin_contract';
|
||||
|
||||
|
@ -213,6 +214,15 @@ export class Plugin implements ISecuritySolutionPlugin {
|
|||
});
|
||||
}
|
||||
|
||||
scheduleEntityAnalyticsMigration({
|
||||
getStartServices: core.getStartServices,
|
||||
taskManager: plugins.taskManager,
|
||||
logger: this.logger,
|
||||
auditLogger: plugins.security?.audit.withoutRequest,
|
||||
}).catch((err) => {
|
||||
logger.error(`Error scheduling entity analytics migration: ${err}`);
|
||||
});
|
||||
|
||||
const requestContextFactory = new RequestContextFactory({
|
||||
config,
|
||||
logger,
|
||||
|
|
|
@ -33,6 +33,7 @@ import { AssetCriticalityDataClient } from './lib/entity_analytics/asset_critica
|
|||
import { createDetectionRulesClient } from './lib/detection_engine/rule_management/logic/detection_rules_client/detection_rules_client';
|
||||
import { buildMlAuthz } from './lib/machine_learning/authz';
|
||||
import { EntityStoreDataClient } from './lib/entity_analytics/entity_store/entity_store_data_client';
|
||||
import { AssetCriticalityEcsMigrationClient } from './lib/entity_analytics/asset_criticality/asset_criticality_migration_client';
|
||||
|
||||
export interface IRequestContextFactory {
|
||||
create(
|
||||
|
@ -206,6 +207,11 @@ export class RequestContextFactory implements IRequestContextFactory {
|
|||
soClient,
|
||||
logger,
|
||||
}),
|
||||
assetCriticalityMigrationClient: new AssetCriticalityEcsMigrationClient({
|
||||
logger,
|
||||
auditLogger: getAuditLogger(),
|
||||
esClient,
|
||||
}),
|
||||
});
|
||||
}),
|
||||
};
|
||||
|
|
|
@ -229,5 +229,6 @@
|
|||
"@kbn/inference-plugin",
|
||||
"@kbn/core-saved-objects-server-mocks",
|
||||
"@kbn/core-http-router-server-internal",
|
||||
"@kbn/core-security-server-mocks",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -153,6 +153,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
'osquery:telemetry-saved-queries',
|
||||
'report:execute',
|
||||
'risk_engine:risk_scoring',
|
||||
'security-solution-ea-asset-criticality-ecs-migration',
|
||||
'security:endpoint-diagnostics',
|
||||
'security:endpoint-meta-telemetry',
|
||||
'security:telemetry-configuration',
|
||||
|
|
|
@ -10,6 +10,10 @@ import { omit } from 'lodash';
|
|||
import { AssetCriticalityRecord } from '@kbn/security-solution-plugin/common/api/entity_analytics';
|
||||
import _ from 'lodash';
|
||||
import { CreateAssetCriticalityRecord } from '@kbn/security-solution-plugin/common/api/entity_analytics';
|
||||
import {
|
||||
CRITICALITY_VALUES,
|
||||
CriticalityValues,
|
||||
} from '@kbn/security-solution-plugin/server/lib/entity_analytics/asset_criticality/constants';
|
||||
import {
|
||||
cleanRiskEngine,
|
||||
cleanAssetCriticality,
|
||||
|
@ -85,6 +89,41 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
updated_at: {
|
||||
type: 'date',
|
||||
},
|
||||
asset: {
|
||||
properties: {
|
||||
criticality: {
|
||||
type: 'keyword',
|
||||
},
|
||||
},
|
||||
},
|
||||
host: {
|
||||
properties: {
|
||||
asset: {
|
||||
properties: {
|
||||
criticality: {
|
||||
type: 'keyword',
|
||||
},
|
||||
},
|
||||
},
|
||||
name: {
|
||||
type: 'keyword',
|
||||
},
|
||||
},
|
||||
},
|
||||
user: {
|
||||
properties: {
|
||||
asset: {
|
||||
properties: {
|
||||
criticality: {
|
||||
type: 'keyword',
|
||||
},
|
||||
},
|
||||
},
|
||||
name: {
|
||||
type: 'keyword',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -188,12 +227,23 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
const startTime = Date.now() - 1000 * TEST_DATA_LENGTH;
|
||||
const records: AssetCriticalityRecord[] = Array.from(
|
||||
{ length: TEST_DATA_LENGTH },
|
||||
(__, i) => ({
|
||||
id_field: 'host.name',
|
||||
id_value: `host-${i}`,
|
||||
criticality_level: LEVELS[Math.floor(i / 10)],
|
||||
'@timestamp': new Date(startTime + i * 1000).toISOString(),
|
||||
})
|
||||
(__, i) => {
|
||||
const hostName = `host-${i}`;
|
||||
const criticality = LEVELS[Math.floor(i / 10)];
|
||||
return {
|
||||
id_field: 'host.name',
|
||||
id_value: hostName,
|
||||
criticality_level: criticality,
|
||||
'@timestamp': new Date(startTime + i * 1000).toISOString(),
|
||||
host: {
|
||||
name: hostName,
|
||||
criticality,
|
||||
},
|
||||
asset: {
|
||||
criticality,
|
||||
},
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const createRecords = () => createAssetCriticalityRecords(records, es);
|
||||
|
@ -341,7 +391,6 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
const expectAssetCriticalityDocMatching = async (expectedDoc: {
|
||||
id_field: string;
|
||||
id_value: string;
|
||||
criticality_level: string;
|
||||
}) => {
|
||||
const esDoc = await getAssetCriticalityDoc({
|
||||
es,
|
||||
|
@ -398,7 +447,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
failed: 0,
|
||||
});
|
||||
|
||||
await expectAssetCriticalityDocMatching(validRecord);
|
||||
await expectAssetCriticalityDocMatching(assetCreateTypeToAssetRecord(validRecord));
|
||||
});
|
||||
|
||||
it('should correctly upload valid records for multiple entities', async () => {
|
||||
|
@ -419,7 +468,9 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
failed: 0,
|
||||
});
|
||||
|
||||
await Promise.all(validRecords.map(expectAssetCriticalityDocMatching));
|
||||
await Promise.all(
|
||||
validRecords.map(assetCreateTypeToAssetRecord).map(expectAssetCriticalityDocMatching)
|
||||
);
|
||||
});
|
||||
|
||||
it('should return a 400 if a record is invalid', async () => {
|
||||
|
@ -443,7 +494,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
|
||||
describe('delete', () => {
|
||||
it('should correctly delete asset criticality if it exists', async () => {
|
||||
const assetCriticality = {
|
||||
const assetCriticality: CreateAssetCriticalityRecord = {
|
||||
id_field: 'host.name',
|
||||
id_value: 'delete-me',
|
||||
criticality_level: 'high_impact',
|
||||
|
@ -454,16 +505,21 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
const res = await assetCriticalityRoutes.delete('host.name', 'delete-me');
|
||||
|
||||
expect(res.body.deleted).to.eql(true);
|
||||
expect(_.omit(res.body.record, '@timestamp')).to.eql(assetCriticality);
|
||||
expect(_.omit(res.body.record, '@timestamp')).to.eql(
|
||||
assetCreateTypeToAssetRecord(assetCriticality)
|
||||
);
|
||||
|
||||
const doc = await getAssetCriticalityDoc({
|
||||
idField: 'host.name',
|
||||
idValue: 'delete-me',
|
||||
es,
|
||||
});
|
||||
|
||||
const deletedDoc = { ...assetCriticality, criticality_level: 'deleted' };
|
||||
|
||||
expect(_.omit(doc, '@timestamp')).to.eql(deletedDoc);
|
||||
const deletedDoc = {
|
||||
...assetCriticality,
|
||||
criticality_level: CRITICALITY_VALUES.DELETED,
|
||||
};
|
||||
expect(_.omit(doc, '@timestamp')).to.eql(assetCreateTypeToAssetRecord(deletedDoc));
|
||||
});
|
||||
|
||||
it('should not return 404 if the asset criticality does not exist', async () => {
|
||||
|
@ -483,3 +539,23 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Update type to allow 'deleted' value
|
||||
type CreateAssetCriticalityRecordWithDeleted = {
|
||||
[K in keyof CreateAssetCriticalityRecord]: K extends 'criticality_level'
|
||||
? CriticalityValues
|
||||
: AssetCriticalityRecord[K];
|
||||
};
|
||||
|
||||
const assetCreateTypeToAssetRecord = (asset: CreateAssetCriticalityRecordWithDeleted) => ({
|
||||
...asset,
|
||||
asset: {
|
||||
criticality: asset.criticality_level,
|
||||
},
|
||||
host: {
|
||||
name: asset.id_value,
|
||||
asset: {
|
||||
criticality: asset.criticality_level,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -4,8 +4,7 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import expect from '@kbn/expect';
|
||||
import omit from 'lodash/omit';
|
||||
import expect from 'expect';
|
||||
import {
|
||||
assetCriticalityRouteHelpersFactory,
|
||||
cleanAssetCriticality,
|
||||
|
@ -31,8 +30,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
idField: expectedDoc.id_field,
|
||||
idValue: expectedDoc.id_value,
|
||||
});
|
||||
|
||||
expect(omit(esDoc, '@timestamp')).to.eql(expectedDoc);
|
||||
expect(esDoc).toEqual(expect.objectContaining(expectedDoc));
|
||||
};
|
||||
|
||||
before(async () => {
|
||||
|
@ -51,8 +49,8 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
it('should correctly upload a valid csv with one entity', async () => {
|
||||
const validCsv = 'host,host-1,low_impact';
|
||||
const { body } = await assetCriticalityRoutes.uploadCsv(validCsv);
|
||||
expect(body.errors).to.eql([]);
|
||||
expect(body.stats).to.eql({
|
||||
expect(body.errors).toEqual([]);
|
||||
expect(body.stats).toEqual({
|
||||
total: 1,
|
||||
successful: 1,
|
||||
failed: 0,
|
||||
|
@ -74,8 +72,8 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
|
||||
const validCsv = 'host,update-host-1,low_impact';
|
||||
const { body } = await assetCriticalityRoutes.uploadCsv(validCsv);
|
||||
expect(body.errors).to.eql([]);
|
||||
expect(body.stats).to.eql({
|
||||
expect(body.errors).toEqual([]);
|
||||
expect(body.stats).toEqual({
|
||||
total: 1,
|
||||
successful: 1,
|
||||
failed: 0,
|
||||
|
@ -103,13 +101,13 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
|
||||
const { body } = await assetCriticalityRoutes.uploadCsv(invalidRows.join('\n'));
|
||||
|
||||
expect(body.stats).to.eql({
|
||||
expect(body.stats).toEqual({
|
||||
total: 8,
|
||||
successful: 0,
|
||||
failed: 8,
|
||||
});
|
||||
|
||||
expect(body.errors).to.eql([
|
||||
expect(body.errors).toEqual([
|
||||
{
|
||||
index: 0,
|
||||
message:
|
||||
|
@ -155,13 +153,13 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
|
||||
const { body } = await assetCriticalityRoutes.uploadCsv(lines.join('\n'));
|
||||
|
||||
expect(body.stats).to.eql({
|
||||
expect(body.stats).toEqual({
|
||||
total: 3,
|
||||
successful: 2,
|
||||
failed: 1,
|
||||
});
|
||||
|
||||
expect(body.errors).to.eql([
|
||||
expect(body.errors).toEqual([
|
||||
{
|
||||
index: 1,
|
||||
message:
|
||||
|
@ -184,7 +182,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
|
||||
it('should return 200 if the csv is empty', async () => {
|
||||
const { body } = await assetCriticalityRoutes.uploadCsv('');
|
||||
expect(body.stats).to.eql({
|
||||
expect(body.stats).toEqual({
|
||||
total: 0,
|
||||
successful: 0,
|
||||
failed: 0,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue