Skip to content

Commit

Permalink
[8.x] [SecuritySolution] Fix entity-store to support asset criticalit…
Browse files Browse the repository at this point in the history
…y delete (elastic#196680) (elastic#197177)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[SecuritySolution] Fix entity-store to support asset criticality
delete (elastic#196680)](elastic#196680)

<!--- Backport version: 9.4.3 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Pablo
Machado","email":"[email protected]"},"sourceCommit":{"committedDate":"2024-10-22T07:58:20Z","message":"[SecuritySolution]
Fix entity-store to support asset criticality delete (elastic#196680)\n\n##
Summary\r\n\r\nUpdate the entity store API so it does not return the
asset criticality\r\nfield when the value is 'deleted'.\r\n\r\n\r\n###
How to test it\r\n* Open kibana with data\r\n* Install the entity
store\r\n* Update asset criticality for a host or user\r\n* Wait for the
engine to run (I don't know a reliable way to do this)\r\n* Refresh the
entity analytics dashboard, and it should show empty\r\nfields for
deleted asset criticality\r\n\r\n- [ ] Backport it to 8.16\r\n\r\n###
Checklist\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","sha":"597fd3e82e549f7a746728af1a577e9fa982b89d","branchLabelMapping":{"^v9.0.0$":"main","^v8.17.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["bug","release_note:skip","v9.0.0","Team:
SecuritySolution","Theme: entity_analytics","Feature:Entity
Analytics","Team:Entity
Analytics","v8.16.0","backport:version","v8.17.0"],"title":"[SecuritySolution]
Fix entity-store to support asset criticality
delete","number":196680,"url":"https://github.com/elastic/kibana/pull/196680","mergeCommit":{"message":"[SecuritySolution]
Fix entity-store to support asset criticality delete (elastic#196680)\n\n##
Summary\r\n\r\nUpdate the entity store API so it does not return the
asset criticality\r\nfield when the value is 'deleted'.\r\n\r\n\r\n###
How to test it\r\n* Open kibana with data\r\n* Install the entity
store\r\n* Update asset criticality for a host or user\r\n* Wait for the
engine to run (I don't know a reliable way to do this)\r\n* Refresh the
entity analytics dashboard, and it should show empty\r\nfields for
deleted asset criticality\r\n\r\n- [ ] Backport it to 8.16\r\n\r\n###
Checklist\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","sha":"597fd3e82e549f7a746728af1a577e9fa982b89d"}},"sourceBranch":"main","suggestedTargetBranches":["8.16","8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/196680","number":196680,"mergeCommit":{"message":"[SecuritySolution]
Fix entity-store to support asset criticality delete (elastic#196680)\n\n##
Summary\r\n\r\nUpdate the entity store API so it does not return the
asset criticality\r\nfield when the value is 'deleted'.\r\n\r\n\r\n###
How to test it\r\n* Open kibana with data\r\n* Install the entity
store\r\n* Update asset criticality for a host or user\r\n* Wait for the
engine to run (I don't know a reliable way to do this)\r\n* Refresh the
entity analytics dashboard, and it should show empty\r\nfields for
deleted asset criticality\r\n\r\n- [ ] Backport it to 8.16\r\n\r\n###
Checklist\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","sha":"597fd3e82e549f7a746728af1a577e9fa982b89d"}},{"branch":"8.16","label":"v8.16.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"8.x","label":"v8.17.0","branchLabelMappingKey":"^v8.17.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

Co-authored-by: Pablo Machado <[email protected]>
  • Loading branch information
kibanamachine and machadoum authored Oct 22, 2024
1 parent 62a48fe commit 7edc05d
Show file tree
Hide file tree
Showing 5 changed files with 146 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -39,23 +39,25 @@ describe('EntityStoreDataClient', () => {
sortOrder: 'asc' as SortOrder,
};

const emptySearchResponse = {
took: 0,
timed_out: false,
_shards: {
total: 0,
successful: 0,
skipped: 0,
failed: 0,
},
hits: {
total: 0,
hits: [],
},
};

describe('search entities', () => {
beforeEach(() => {
jest.resetAllMocks();
esClientMock.search.mockResolvedValue({
took: 0,
timed_out: false,
_shards: {
total: 0,
successful: 0,
skipped: 0,
failed: 0,
},
hits: {
total: 0,
hits: [],
},
});
esClientMock.search.mockResolvedValue(emptySearchResponse);
});

it('searches in the entities store indices', async () => {
Expand Down Expand Up @@ -133,5 +135,47 @@ describe('EntityStoreDataClient', () => {

expect(response.inspect).toMatchSnapshot();
});

it('returns searched entity record', async () => {
const fakeEntityRecord = { entity_record: true, asset: { criticality: 'low' } };

esClientMock.search.mockResolvedValue({
...emptySearchResponse,
hits: {
total: 1,
hits: [
{
_index: '.entities.v1.latest.security_host_default',
_source: fakeEntityRecord,
},
],
},
});

const response = await dataClient.searchEntities(defaultSearchParams);

expect(response.records[0]).toEqual(fakeEntityRecord);
});

it("returns empty asset criticality when criticality value is 'deleted'", async () => {
const fakeEntityRecord = { entity_record: true };

esClientMock.search.mockResolvedValue({
...emptySearchResponse,
hits: {
total: 1,
hits: [
{
_index: '.entities.v1.latest.security_host_default',
_source: { asset: { criticality: 'deleted' }, ...fakeEntityRecord },
},
],
},
});

const response = await dataClient.searchEntities(defaultSearchParams);

expect(response.records[0]).toEqual(fakeEntityRecord);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ import {
isPromiseFulfilled,
isPromiseRejected,
} from './utils';
import type { EntityRecord } from './types';
import { CRITICALITY_VALUES } from '../asset_criticality/constants';

interface EntityStoreClientOpts {
logger: Logger;
Expand Down Expand Up @@ -406,7 +408,7 @@ export class EntityStoreDataClient {
const sort = sortField ? [{ [sortField]: sortOrder }] : undefined;
const query = filterQuery ? JSON.parse(filterQuery) : undefined;

const response = await this.esClient.search<Entity>({
const response = await this.esClient.search<EntityRecord>({
index,
query,
size: Math.min(perPage, MAX_SEARCH_RESPONSE_SIZE),
Expand All @@ -418,7 +420,19 @@ export class EntityStoreDataClient {

const total = typeof hits.total === 'number' ? hits.total : hits.total?.value ?? 0;

const records = hits.hits.map((hit) => hit._source as Entity);
const records = hits.hits.map((hit) => {
const { asset, ...source } = hit._source as EntityRecord;

const assetOverwrite: Pick<Entity, 'asset'> =
asset && asset.criticality !== CRITICALITY_VALUES.DELETED
? { asset: { criticality: asset.criticality } }
: {};

return {
...source,
...assetOverwrite,
};
});

const inspect: InspectQuery = {
dsl: [JSON.stringify({ index, body: query }, null, 2)],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* 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 { HostEntity, UserEntity } from '../../../../common/api/entity_analytics';
import type { CriticalityValues } from '../asset_criticality/constants';

export interface HostEntityRecord extends Omit<HostEntity, 'asset'> {
asset?: {
criticality: CriticalityValues;
};
}

export interface UserEntityRecord extends Omit<UserEntity, 'asset'> {
asset?: {
criticality: CriticalityValues;
};
}

/**
* It represents the data stored in the entity store index.
*/
export type EntityRecord = HostEntityRecord | UserEntityRecord;
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
fieldOperatorToIngestProcessor,
} from '@kbn/security-solution-plugin/server/lib/entity_analytics/entity_store/field_retention_definition';
import { FtrProviderContext } from '../../../../ftr_provider_context';
import { applyIngestProcessorToDoc } from '../utils/ingest';
export default ({ getService }: FtrProviderContext) => {
const es = getService('es');
const log = getService('log');
Expand All @@ -26,31 +27,8 @@ export default ({ getService }: FtrProviderContext) => {
docSource: any
): Promise<any> => {
const step = fieldOperatorToIngestProcessor(operator, { enrichField: 'historical' });
const doc = {
_index: 'index',
_id: 'id',
_source: docSource,
};

const res = await es.ingest.simulate({
pipeline: {
description: 'test',
processors: [step],
},
docs: [doc],
});

const firstDoc = res.docs?.[0];

// @ts-expect-error error is not in the types
const error = firstDoc?.error;
if (error) {
log.error('Full painless error below: ');
log.error(JSON.stringify(error, null, 2));
throw new Error('Painless error running pipelie see logs for full detail : ' + error?.type);
}

return firstDoc?.doc?._source;
return applyIngestProcessorToDoc([step], docSource, es, log);
};

describe('@ess @serverless @skipInServerlessMKI Entity store - Field Retention Pipeline Steps', () => {
Expand Down Expand Up @@ -90,7 +68,7 @@ export default ({ getService }: FtrProviderContext) => {
expectArraysMatchAnyOrder(resultDoc.test_field, ['foo']);
});

it('should take from history if latest field doesnt have maxLength values', async () => {
it("should take from history if latest field doesn't have maxLength values", async () => {
const op: FieldRetentionOperator = {
operation: 'collect_values',
field: 'test_field',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* 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 { Client } from '@elastic/elasticsearch';
import { IngestProcessorContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { ToolingLog } from '@kbn/tooling-log';

export const applyIngestProcessorToDoc = async (
steps: IngestProcessorContainer[],
docSource: any,
es: Client,
log: ToolingLog
): Promise<any> => {
const doc = {
_index: 'index',
_id: 'id',
_source: docSource,
};

const res = await es.ingest.simulate({
pipeline: {
description: 'test',
processors: steps,
},
docs: [doc],
});

const firstDoc = res.docs?.[0];

// @ts-expect-error error is not in the types
const error = firstDoc?.error;
if (error) {
log.error('Full painless error below: ');
log.error(JSON.stringify(error, null, 2));
throw new Error('Painless error running pipeline see logs for full detail : ' + error?.type);
}

return firstDoc?.doc?._source;
};

0 comments on commit 7edc05d

Please sign in to comment.