[8.x] [SecuritySolution] Asset Criticality ECS compatibility (#194109) (#194711)

# 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:
Kibana Machine 2024-10-03 01:23:05 +10:00 committed by GitHub
parent eeee8f7d4d
commit d8f01e8d43
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 1151 additions and 50 deletions

View file

@ -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:

View file

@ -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:

View file

@ -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.

View file

@ -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'

View file

@ -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:

View file

@ -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:

View file

@ -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,
},
})
);
});
});
});

View file

@ -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) {

View file

@ -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)
);
});
});
});

View file

@ -0,0 +1,96 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { 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,
}
);
};
}

View file

@ -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,
},
});

View file

@ -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

View file

@ -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 } = {

View file

@ -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,
},
};
};

View file

@ -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"'
);
});
});
});

View file

@ -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}"`);
},
};
};

View file

@ -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,

View file

@ -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 = {

View file

@ -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,

View file

@ -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 });
};

View file

@ -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,

View file

@ -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,
}),
});
}),
};

View file

@ -229,5 +229,6 @@
"@kbn/inference-plugin",
"@kbn/core-saved-objects-server-mocks",
"@kbn/core-http-router-server-internal",
"@kbn/core-security-server-mocks",
]
}

View file

@ -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',

View file

@ -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,
},
},
});

View file

@ -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,