Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Un-revert "Siem query rule - reduce field_caps usage" #186317

Merged
merged 2 commits into from
Jun 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/plugins/data/common/search/search_source/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ export * from './fetch';
export * from './search_source';
export * from './search_source_service';
export * from './types';
export * from './query_to_fields';
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { EsQuerySortValue, queryToFields, SearchRequest, SortDirection } from '../..';
import { DataViewLazy } from '@kbn/data-views-plugin/common';

describe('SearchSource#queryToFields', () => {
it('should include time field', async () => {
const dataView = {
timeFieldName: '@timestamp',
getSourceFiltering: jest.fn(),
getFields: jest.fn().mockResolvedValue({
getFieldMapSorted: jest.fn(),
}),
};
const request: SearchRequest = { query: [] };
await queryToFields({ dataView: dataView as unknown as DataViewLazy, request });
const { fieldName } = dataView.getFields.mock.calls[0][0];
expect(fieldName).toEqual(['@timestamp']);
});

it('should include sort field', async () => {
const dataView = {
getSourceFiltering: jest.fn(),
getFields: jest.fn().mockResolvedValue({
getFieldMapSorted: jest.fn(),
}),
};
const sort: EsQuerySortValue = { bytes: SortDirection.asc };
const request: SearchRequest = { query: [] };
await queryToFields({ dataView: dataView as unknown as DataViewLazy, sort, request });
const { fieldName } = dataView.getFields.mock.calls[0][0];
expect(fieldName).toEqual(['bytes']);
});

it('should include request KQL query fields', async () => {
const dataView = {
timeFieldName: '@timestamp',
getSourceFiltering: jest.fn(),
getFields: jest.fn().mockResolvedValue({
getFieldMapSorted: jest.fn(),
}),
};
const request: SearchRequest = {
query: [
{
language: 'kuery',
query: 'log.level: debug AND NOT message: unknown',
},
],
};
await queryToFields({ dataView: dataView as unknown as DataViewLazy, request });
const { fieldName } = dataView.getFields.mock.calls[0][0];
expect(fieldName).toEqual(['@timestamp', 'log.level', 'message']);
});

it('should not include request Lucene query fields', async () => {
const dataView = {
timeFieldName: '@timestamp',
getSourceFiltering: jest.fn(),
getFields: jest.fn().mockResolvedValue({
getFieldMapSorted: jest.fn(),
}),
};
const request: SearchRequest = {
query: [
{
language: 'lucene',
query: 'host: artifacts\\.*',
},
],
};
await queryToFields({ dataView: dataView as unknown as DataViewLazy, request });
const { fieldName } = dataView.getFields.mock.calls[0][0];
expect(fieldName).toEqual(['@timestamp']);
});
});
58 changes: 58 additions & 0 deletions src/plugins/data/common/search/search_source/query_to_fields.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { DataViewLazy } from '@kbn/data-views-plugin/common';
import { fromKueryExpression, getKqlFieldNames } from '@kbn/es-query';
import type { SearchRequest } from './fetch';
import { EsQuerySortValue } from '../..';

export async function queryToFields({
dataView,
sort,
request,
}: {
dataView: DataViewLazy;
sort?: EsQuerySortValue | EsQuerySortValue[];
request: SearchRequest;
}) {
let fields = dataView.timeFieldName ? [dataView.timeFieldName] : [];
if (sort) {
const sortArr = Array.isArray(sort) ? sort : [sort];
fields.push(...sortArr.flatMap((s) => Object.keys(s)));
}
for (const query of request.query) {
if (query.query && query.language === 'kuery') {
const nodes = fromKueryExpression(query.query);
const queryFields = getKqlFieldNames(nodes);
fields = fields.concat(queryFields);
}
}
Comment on lines +23 to +34
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: if the let usage is only due to the concat call at the end, maybe that can be replaced with push(...queryFields) and make this a const

Suggested change
let fields = dataView.timeFieldName ? [dataView.timeFieldName] : [];
if (sort) {
const sortArr = Array.isArray(sort) ? sort : [sort];
fields.push(...sortArr.flatMap((s) => Object.keys(s)));
}
for (const query of request.query) {
if (query.query && query.language === 'kuery') {
const nodes = fromKueryExpression(query.query);
const queryFields = getKqlFieldNames(nodes);
fields = fields.concat(queryFields);
}
}
const fields = dataView.timeFieldName ? [dataView.timeFieldName] : [];
if (sort) {
const sortArr = Array.isArray(sort) ? sort : [sort];
fields.push(...sortArr.flatMap((s) => Object.keys(s)));
}
for (const query of request.query) {
if (query.query && query.language === 'kuery') {
const nodes = fromKueryExpression(query.query);
const queryFields = getKqlFieldNames(nodes);
fields.push(...queryFields);
}
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although this nit makes sense, I prefer to leave this PR as close to its original counterpart (#184890) as possible

const filters = request.filters;
if (filters) {
const filtersArr = Array.isArray(filters) ? filters : [filters];
for (const f of filtersArr) {
// unified search bar filters have meta object and key (regular filters)
// unified search bar "custom" filters ("Edit as query DSL", where meta.key is not present but meta is)
// Any other Elasticsearch query DSL filter that gets passed in by consumers (not coming from unified search, and these probably won't have a meta key at all)
if (f?.meta?.key && f.meta.disabled !== true) {
fields.push(f.meta.key);
}
}
}

// if source filtering is enabled, we need to fetch all the fields
const fieldName =
dataView.getSourceFiltering() && dataView.getSourceFiltering().excludes.length ? ['*'] : fields;

if (fieldName.length) {
return (await dataView.getFields({ fieldName })).getFieldMapSorted();
}

// no fields needed to be loaded for query
return {};
}
41 changes: 2 additions & 39 deletions src/plugins/data/common/search/search_source/search_source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,9 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import {
buildEsQuery,
Filter,
fromKueryExpression,
isOfQueryType,
isPhraseFilter,
isPhrasesFilter,
getKqlFieldNames,
} from '@kbn/es-query';
import { fieldWildcardFilter } from '@kbn/kibana-utils-plugin/common';
import { getHighlightRequest } from '@kbn/field-formats-plugin/common';
Expand All @@ -95,6 +93,7 @@ import type { ISearchGeneric, IKibanaSearchResponse, IEsSearchResponse } from '@
import { normalizeSortRequest } from './normalize_sort_request';

import { AggConfigSerialized, DataViewField, SerializedSearchSourceFields } from '../..';
import { queryToFields } from './query_to_fields';

import { AggConfigs, EsQuerySortValue } from '../..';
import type {
Expand Down Expand Up @@ -778,43 +777,7 @@ export class SearchSource {

public async loadDataViewFields(dataView: DataViewLazy) {
const request = this.mergeProps(this, { body: {} });
let fields = dataView.timeFieldName ? [dataView.timeFieldName] : [];
const sort = this.getField('sort');
if (sort) {
const sortArr = Array.isArray(sort) ? sort : [sort];
for (const s of sortArr) {
const keys = Object.keys(s);
fields = fields.concat(keys);
}
}
for (const query of request.query) {
if (query.query && query.language === 'kuery') {
const nodes = fromKueryExpression(query.query);
const queryFields = getKqlFieldNames(nodes);
fields = fields.concat(queryFields);
}
}
const filters = request.filters;
if (filters) {
const filtersArr = Array.isArray(filters) ? filters : [filters];
for (const f of filtersArr) {
fields = fields.concat(f.meta.key);
}
}
fields = fields.filter((f) => Boolean(f));

if (dataView.getSourceFiltering() && dataView.getSourceFiltering().excludes.length) {
// if source filtering is enabled, we need to fetch all the fields
return (await dataView.getFields({ fieldName: ['*'] })).getFieldMapSorted();
} else if (fields.length) {
return (
await dataView.getFields({
fieldName: fields,
})
).getFieldMapSorted();
}
// no fields needed to be loaded for query
return {};
return await queryToFields({ dataView, request });
}

private flatten() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,14 @@ export const createRuleTypeMocks = (
alertWithPersistence: jest.fn(),
logger: loggerMock,
shouldWriteAlerts: () => true,
dataViews: {
createDataViewLazy: jest.fn().mockResolvedValue({
getFields: jest.fn().mockResolvedValue({
getFieldMapSorted: jest.fn().mockReturnValue({}),
}),
getSourceFiltering: jest.fn().mockReturnValue({ excludes: [] }),
}),
},
};

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import {
hasTimestampFields,
isMachineLearningParams,
isEsqlParams,
isQueryParams,
isEqlParams,
getDisabledActionsWarningText,
} from './utils/utils';
import { DEFAULT_MAX_SIGNALS, DEFAULT_SEARCH_AFTER_PAGE_SIZE } from '../../../../common/constants';
Expand Down Expand Up @@ -341,7 +343,12 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper =
});
}

if (!isMachineLearningParams(params) && !isEsqlParams(params)) {
if (
!isMachineLearningParams(params) &&
!isEsqlParams(params) &&
!isQueryParams(params) &&
!isEqlParams(params)
) {
inputIndexFields = await getFieldsForWildcard({
index: inputIndex,
dataViews: services.dataViews,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export const queryExecutor = async ({
index: runOpts.inputIndex,
exceptionFilter: runOpts.exceptionFilter,
fields: runOpts.inputIndexFields,
loadFields: true,
});

const license = await firstValueFrom(licensing.license$);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ import type { SavedIdOrUndefined } from '../../../../../common/api/detection_eng
import type { PartialFilter } from '../../types';
import { withSecuritySpan } from '../../../../utils/with_security_span';
import type { ESBoolQuery } from '../../../../../common/typed_json';
import { getQueryFilter } from './get_query_filter';
import { getQueryFilter as getQueryFilterNoLoadFields } from './get_query_filter';
import { getQueryFilterLoadFields } from './get_query_filter_load_fields';

export interface GetFilterArgs {
type: Type;
Expand All @@ -38,6 +39,7 @@ export interface GetFilterArgs {
index: IndexPatternArray | undefined;
exceptionFilter: Filter | undefined;
fields?: DataViewFieldBase[];
loadFields?: boolean;
}

interface QueryAttributes {
Expand All @@ -59,7 +61,11 @@ export const getFilter = async ({
query,
exceptionFilter,
fields = [],
loadFields = false,
}: GetFilterArgs): Promise<ESBoolQuery> => {
const getQueryFilter = loadFields
? getQueryFilterLoadFields(services.dataViews)
: getQueryFilterNoLoadFields;
const queryFilter = () => {
if (query != null && language != null && index != null) {
return getQueryFilter({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* 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 { Language } from '@kbn/securitysolution-io-ts-alerting-types';
import type { Filter, EsQueryConfig, DataViewFieldBase } from '@kbn/es-query';
import { DataView } from '@kbn/data-views-plugin/server';
import { queryToFields } from '@kbn/data-plugin/common';
import type { DataViewsContract } from '@kbn/data-views-plugin/common';
import type { FieldFormatsStartCommon } from '@kbn/field-formats-plugin/common';
import { buildEsQuery } from '@kbn/es-query';
import type { ESBoolQuery } from '../../../../../common/typed_json';
import { getAllFilters } from './get_query_filter';
import type {
IndexPatternArray,
RuleQuery,
} from '../../../../../common/api/detection_engine/model/rule_schema';

export const getQueryFilterLoadFields =
(dataViewsService: DataViewsContract) =>
async ({
query,
language,
filters,
index,
exceptionFilter,
}: {
query: RuleQuery;
language: Language;
filters: unknown;
index: IndexPatternArray;
exceptionFilter: Filter | undefined;
fields?: DataViewFieldBase[];
}): Promise<ESBoolQuery> => {
const config: EsQueryConfig = {
allowLeadingWildcards: true,
queryStringOptions: { analyze_wildcard: true },
ignoreFilterIfFieldNotInIndex: false,
dateFormatTZ: 'Zulu',
};

const initialQuery = { query, language };
const allFilters = getAllFilters(filters as Filter[], exceptionFilter);

const title = (index ?? []).join();

const dataViewLazy = await dataViewsService.createDataViewLazy({ title });

const flds = await queryToFields({
dataView: dataViewLazy,
request: { query: [initialQuery], filters: allFilters },
});

const dataViewLimitedFields = new DataView({
spec: { title },
fieldFormats: {} as unknown as FieldFormatsStartCommon,
shortDotsEnable: false,
metaFields: [],
});

dataViewLimitedFields.fields.replaceAll(Object.values(flds).map((fld) => fld.toSpec()));

return buildEsQuery(dataViewLimitedFields, initialQuery, allFilters, config);
};