Skip to content

Commit

Permalink
IM rule use fields caps (#168447)
Browse files Browse the repository at this point in the history
## IM rule use fields caps instead of field mappign API

Serverless don't support `getFieldMapping` API, so we use `fieldCaps`
API to check if the field are support term query.

---------

Co-authored-by: Kibana Machine <[email protected]>
  • Loading branch information
nkhristinin and kibanamachine authored Oct 13, 2023
1 parent caeae04 commit 2d92f6c
Show file tree
Hide file tree
Showing 2 changed files with 80 additions and 84 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { IndicesGetFieldMappingResponse } from '@elastic/elasticsearch/lib/api/types';
import type { FieldCapsResponse } from '@elastic/elasticsearch/lib/api/types';
import type { RuleExecutorServicesMock } from '@kbn/alerting-plugin/server/mocks';
import { alertsMock } from '@kbn/alerting-plugin/server/mocks';
import { ruleExecutionLogMock } from '../../../rule_monitoring/mocks';
Expand All @@ -14,52 +14,40 @@ import {
getAllowedFieldsForTermQuery,
} from './get_allowed_fields_for_terms_query';

const indexMapping = {
'source-index': {
mappings: {
'host.name': {
full_name: 'host.name',
mapping: {
name: {
type: 'keyword',
},
},
},
'url.full': {
full_name: 'url.full',
mapping: {
full: {
type: 'keyword',
},
},
const fieldsCapsResponse: FieldCapsResponse = {
indices: ['source-index', 'other-source-index'],
fields: {
'url.full': {
keyword: {
type: 'keyword',
metadata_field: false,
searchable: true,
aggregatable: true,
},
'source.range': {
full_name: 'source.range',
mapping: {
range: {
type: 'ip_range',
},
},
},
'host.name': {
keyword: {
type: 'keyword',
metadata_field: false,
searchable: true,
aggregatable: true,
},
},
},
'other-source-index': {
mappings: {
'host.name': {
full_name: 'host.name',
mapping: {
name: {
type: 'keyword',
},
},
'host.ip': {
ip: {
type: 'ip',
metadata_field: false,
searchable: true,
aggregatable: true,
},
'host.ip': {
full_name: 'host.ip',
mapping: {
name: {
type: 'ip',
},
},
},
'source.range': {
ip_range: {
type: 'ip_range',
metadata_field: false,
searchable: true,
aggregatable: true,
indices: ['source-index'],
},
},
},
Expand All @@ -68,31 +56,38 @@ const indexMapping = {
describe('get_allowed_fields_for_terms_query copy', () => {
describe('getAllowedFieldForTermQueryFromMapping', () => {
it('should return map of fields allowed for term query', () => {
const result = getAllowedFieldForTermQueryFromMapping(
indexMapping as IndicesGetFieldMappingResponse
);
const result = getAllowedFieldForTermQueryFromMapping(fieldsCapsResponse, [
'host.ip',
'url.full',
'host.name',
'source.range',
]);
expect(result).toEqual({
'host.ip': true,
'url.full': true,
'host.name': true,
});
});
it('should disable fields if in one index type not supported', () => {
const result = getAllowedFieldForTermQueryFromMapping({
'new-source-index': {
mappings: {
const result = getAllowedFieldForTermQueryFromMapping(
{
...fieldsCapsResponse,
fields: {
...fieldsCapsResponse.fields,
'host.name': {
full_name: 'host.name',
mapping: {
name: {
type: 'text',
},
...fieldsCapsResponse.fields['host.name'],
text: {
type: 'text',
metadata_field: false,
searchable: true,
aggregatable: true,
indices: ['new-source-index'],
},
},
},
},
...indexMapping,
} as IndicesGetFieldMappingResponse);
['host.ip', 'url.full', 'host.name', 'source.range']
);
expect(result).toEqual({
'host.ip': true,
'url.full': true,
Expand All @@ -106,16 +101,16 @@ describe('get_allowed_fields_for_terms_query copy', () => {

beforeEach(() => {
alertServices = alertsMock.createRuleExecutorServices();
alertServices.scopedClusterClient.asCurrentUser.indices.getFieldMapping.mockResolvedValue(
indexMapping as IndicesGetFieldMappingResponse
alertServices.scopedClusterClient.asCurrentUser.fieldCaps.mockResolvedValue(
fieldsCapsResponse
);
ruleExecutionLogger = ruleExecutionLogMock.forExecutors.create();
});

it('should return map of fields allowed for term query for source and threat indices', async () => {
const threatMatchedFields = {
source: ['host.name', 'url.full'],
threat: ['host.name', 'url.full'],
source: ['host.name', 'url.full', 'host.ip'],
threat: ['host.name', 'url.full', 'host.ip'],
};
const threatIndex = ['threat-index'];
const inputIndex = ['source-index'];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,39 +5,34 @@
* 2.0.
*/

import type { IndicesGetFieldMappingResponse } from '@elastic/elasticsearch/lib/api/types';
import type { FieldCapsResponse } from '@elastic/elasticsearch/lib/api/types';
import type { AllowedFieldsForTermsQuery, GetAllowedFieldsForTermQuery } from './types';

const allowedFieldTypes = ['keyword', 'constant_keyword', 'wildcard', 'ip'];
const allowedFieldTypesSet = new Set(['keyword', 'constant_keyword', 'wildcard', 'ip']);

/*
* Return map of fields allowed for term query
*/
export const getAllowedFieldForTermQueryFromMapping = (
indexMapping: IndicesGetFieldMappingResponse
fieldsCapsResponse: FieldCapsResponse,
fields: string[]
): Record<string, boolean> => {
const result: Record<string, boolean> = {};
const notAllowedFields: string[] = [];
const fieldsCaps = fieldsCapsResponse.fields;

const indices = Object.values(indexMapping);
indices.forEach((index) => {
Object.entries(index.mappings).forEach(([field, fieldValue]) => {
Object.values(fieldValue.mapping).forEach((mapping) => {
const fieldType = mapping?.type;
if (!fieldType) return;
const availableFields = fields.filter((field) => {
const fieldCaps = fieldsCaps[field];

if (allowedFieldTypes.includes(fieldType) && !notAllowedFields.includes(field)) {
result[field] = true;
} else {
notAllowedFields.push(field);
// if we the field allowed in one index, but not allowed in another, we should delete it from result
delete result[field];
}
});
const isAllVariationsAllowed = Object.values(fieldCaps).every((fieldCapByType) => {
return allowedFieldTypesSet.has(fieldCapByType.type);
});

return isAllVariationsAllowed;
});

return result;
return availableFields.reduce<Record<string, boolean>>((acc, field) => {
acc[field] = true;
return acc;
}, {});
};

/**
Expand All @@ -53,19 +48,25 @@ export const getAllowedFieldsForTermQuery = async ({
let allowedFieldsForTermsQuery = { source: {}, threat: {} };
try {
const [sourceFieldsMapping, threatFieldsMapping] = await Promise.all([
services.scopedClusterClient.asCurrentUser.indices.getFieldMapping({
services.scopedClusterClient.asCurrentUser.fieldCaps({
index: inputIndex,
fields: threatMatchedFields.source,
}),
services.scopedClusterClient.asCurrentUser.indices.getFieldMapping({
services.scopedClusterClient.asCurrentUser.fieldCaps({
index: threatIndex,
fields: threatMatchedFields.threat,
}),
]);

allowedFieldsForTermsQuery = {
source: getAllowedFieldForTermQueryFromMapping(sourceFieldsMapping),
threat: getAllowedFieldForTermQueryFromMapping(threatFieldsMapping),
source: getAllowedFieldForTermQueryFromMapping(
sourceFieldsMapping,
threatMatchedFields.source
),
threat: getAllowedFieldForTermQueryFromMapping(
threatFieldsMapping,
threatMatchedFields.threat
),
};
} catch (e) {
ruleExecutionLogger.debug(`Can't get allowed fields for terms query: ${e}`);
Expand Down

0 comments on commit 2d92f6c

Please sign in to comment.