Skip to content

Commit

Permalink
[8.10] [Fleet] Fix validation errors in KQL queries (#168329) (#168436)
Browse files Browse the repository at this point in the history
# Backport

This will backport the following commits from `main` to `8.10`:
- [[Fleet] Fix validation errors in KQL queries
(#168329)](#168329)

<!--- Backport version: 8.9.7 -->

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

<!--BACKPORT [{"author":{"name":"Cristina
Amico","email":"[email protected]"},"sourceCommit":{"committedDate":"2023-10-10T08:55:04Z","message":"[Fleet]
Fix validation errors in KQL queries (#168329)\n\nRelated to
https://github.com/elastic/kibana/issues/167139\r\n\r\n##
Summary\r\n\r\n[Fleet] Fix validation errors in KQL
searchboxes\r\n\r\n### Testing\r\n- In the agent list searchbox, type
the following query:\r\n\r\n```\r\n agent.version :
8.10.0\r\n```\r\n\r\nThe UI shouldn't display any error\r\n\r\n###
Checklist\r\n\r\n- [ ] [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: Kibana Machine
<[email protected]>","sha":"7839d518fbeb4a960fd19a98ae4cf13fdff69748","branchLabelMapping":{"^v8.12.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:fix","Team:Fleet","v8.11.0","v8.12.0","v8.10.4"],"number":168329,"url":"https://github.com/elastic/kibana/pull/168329","mergeCommit":{"message":"[Fleet]
Fix validation errors in KQL queries (#168329)\n\nRelated to
https://github.com/elastic/kibana/issues/167139\r\n\r\n##
Summary\r\n\r\n[Fleet] Fix validation errors in KQL
searchboxes\r\n\r\n### Testing\r\n- In the agent list searchbox, type
the following query:\r\n\r\n```\r\n agent.version :
8.10.0\r\n```\r\n\r\nThe UI shouldn't display any error\r\n\r\n###
Checklist\r\n\r\n- [ ] [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: Kibana Machine
<[email protected]>","sha":"7839d518fbeb4a960fd19a98ae4cf13fdff69748"}},"sourceBranch":"main","suggestedTargetBranches":["8.11","8.10"],"targetPullRequestStates":[{"branch":"8.11","label":"v8.11.0","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v8.12.0","labelRegex":"^v8.12.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/168329","number":168329,"mergeCommit":{"message":"[Fleet]
Fix validation errors in KQL queries (#168329)\n\nRelated to
https://github.com/elastic/kibana/issues/167139\r\n\r\n##
Summary\r\n\r\n[Fleet] Fix validation errors in KQL
searchboxes\r\n\r\n### Testing\r\n- In the agent list searchbox, type
the following query:\r\n\r\n```\r\n agent.version :
8.10.0\r\n```\r\n\r\nThe UI shouldn't display any error\r\n\r\n###
Checklist\r\n\r\n- [ ] [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: Kibana Machine
<[email protected]>","sha":"7839d518fbeb4a960fd19a98ae4cf13fdff69748"}},{"branch":"8.10","label":"v8.10.4","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

Co-authored-by: Cristina Amico <[email protected]>
  • Loading branch information
kibanamachine and criamico authored Oct 10, 2023
1 parent c0b8235 commit b3cfe48
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 45 deletions.
9 changes: 6 additions & 3 deletions x-pack/plugins/fleet/server/constants/mappings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@
*/

/**
* The mappings declared closely mirror the ones declared in indices and SOs
* But they are only used to perform validation on those endpoints using ListWithKuery
* Whenever a field is added on any of these mappings, make sure to add it here as well
* ATTENTION: New mappings for Fleet are defined in the ElasticSearch repo.
*
* The following mappings declared here closely mirror them
* But they are only used to perform validation on the endpoints using ListWithKuery
* They're needed to perform searches on these mapping trough the KQL searchboxes in the UI.
* Whenever a field is added on any of these mappings in ES, make sure to add it here as well
*/

export const AGENT_POLICY_MAPPINGS = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,7 @@ describe('Filter Utils', () => {
},
{
astPath: 'arguments.1',
error: 'The key is empty and needs to be wrapped by a saved object type like foo',
error: 'Invalid key',
isSavedObjectAttr: false,
key: null,
type: null,
Expand Down
54 changes: 27 additions & 27 deletions x-pack/plugins/fleet/server/routes/utils/filter_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,8 @@ export const hasFilterKeyError = (
indexMapping: IndexMapping,
skipNormalization?: boolean
): string | null => {
if (key == null) {
return `The key is empty and needs to be wrapped by a saved object type like ${types.join()}`;
if (!key) {
return `Invalid key`;
}
if (!key.includes('.')) {
if (allowedTerms.some((term) => term === key) || fieldDefined(indexMapping, key)) {
Expand All @@ -141,12 +141,11 @@ export const hasFilterKeyError = (
return `This type '${key}' is not allowed`;
} else if (key.includes('.')) {
const keySplit = key.split('.');
if (
keySplit.length <= 1 &&
!fieldDefined(indexMapping, keySplit[0]) &&
!types.includes(keySplit[0])
) {
return `This type '${keySplit[0]}' is not allowed`;
const firstField = keySplit[0];
const hasIndexWrap = types.includes(firstField);

if (keySplit.length <= 1 && !fieldDefined(indexMapping, firstField) && !hasIndexWrap) {
return `This type '${firstField}' is not allowed`;
}
// In some cases we don't want to check about the `attributes` presence
// In that case pass the `skipNormalization` parameter
Expand All @@ -157,34 +156,35 @@ export const hasFilterKeyError = (
return `This key '${key}' does NOT match the filter proposition SavedObjectType.attributes.key`;
}
// Check that the key exists in the mappings
const searchKey =
skipNormalization || keySplit[1] !== 'attributes'
? `${keySplit[0]}.${keySplit.slice(1, keySplit.length).join('.')}`
: `${keySplit[0]}.${keySplit.slice(2, keySplit.length).join('.')}`;
if (
(keySplit.length === 2 && !fieldDefined(indexMapping, keySplit[1])) ||
(keySplit.length === 2 &&
!types.includes(keySplit[0]) &&
!fieldDefined(indexMapping, searchKey)) ||
(keySplit.length > 2 && !fieldDefined(indexMapping, searchKey))
) {
let searchKey = '';
if (keySplit.length === 2) {
searchKey = hasIndexWrap ? keySplit[1] : key;
} else if (keySplit.length > 2) {
searchKey =
skipNormalization || keySplit[1] !== 'attributes'
? `${firstField}.${keySplit.slice(1, keySplit.length).join('.')}`
: `${firstField}.${keySplit.slice(2, keySplit.length).join('.')}`;
}
if (!fieldDefined(indexMapping, searchKey)) {
return `This key '${key}' does NOT exist in ${types.join()} saved object index patterns`;
}
}
return null;
};

const getMappingKey = (key?: string) =>
!!key ? 'properties.' + key.split('.').join('.properties.') : '';

export const fieldDefined = (indexMappings: IndexMapping, key: string): boolean => {
const keySplit = key.split('.');
const shortenedKey = `${keySplit[1]}.${keySplit.slice(2, keySplit.length).join('.')}`;
const mappingKey = 'properties.' + key.split('.').join('.properties.');
const shortenedMappingKey = 'properties.' + shortenedKey.split('.').join('.properties.');

if (get(indexMappings, mappingKey) != null || get(indexMappings, shortenedMappingKey) != null) {
return true;
}
const mappingKey = getMappingKey(key);

if (mappingKey === 'properties.id') {
if (
!!get(indexMappings, mappingKey) ||
!!get(indexMappings, getMappingKey(shortenedKey)) ||
mappingKey === 'properties.id'
) {
return true;
}

Expand All @@ -199,7 +199,7 @@ export const fieldDefined = (indexMappings: IndexMapping, key: string): boolean
mappingKey.lastIndexOf(propertiesAttribute) + `${propertiesAttribute}.`.length
);
const mapping = `${fieldMapping}fields.${fieldType}`;
if (get(indexMappings, mapping) != null) {
if (!!get(indexMappings, mapping)) {
return true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { validateFilterKueryNode, validateKuery } from './filter_utils';

describe('ValidateFilterKueryNode validates real kueries through KueryNode', () => {
describe('Agent policies', () => {
it('Test 1 - search by data_output_id', async () => {
it('Search by data_output_id', async () => {
const astFilter = esKuery.fromKueryExpression(
`${AGENT_POLICY_SAVED_OBJECT_TYPE}.data_output_id: test_id`
);
Expand All @@ -44,7 +44,7 @@ describe('ValidateFilterKueryNode validates real kueries through KueryNode', ()
]);
});

it('Test 2 - search by inactivity timeout', async () => {
it('Search by inactivity timeout', async () => {
const astFilter = esKuery.fromKueryExpression(
`${AGENT_POLICY_SAVED_OBJECT_TYPE}.inactivity_timeout:*`
);
Expand All @@ -65,7 +65,7 @@ describe('ValidateFilterKueryNode validates real kueries through KueryNode', ()
]);
});

it('Test 3 - complex query', async () => {
it('Complex query', async () => {
const validationObject = validateFilterKueryNode({
astFilter: esKuery.fromKueryExpression(
`${AGENT_POLICY_SAVED_OBJECT_TYPE}.download_source_id:some_id or (not ${AGENT_POLICY_SAVED_OBJECT_TYPE}.download_source_id:*)`
Expand Down Expand Up @@ -93,7 +93,7 @@ describe('ValidateFilterKueryNode validates real kueries through KueryNode', ()
]);
});

it('Test 4', async () => {
it('Test another complex query', async () => {
const astFilter = esKuery.fromKueryExpression(
`${AGENT_POLICY_SAVED_OBJECT_TYPE}.data_output_id: test_id or ${AGENT_POLICY_SAVED_OBJECT_TYPE}.monitoring_output_id: test_id or (not ${AGENT_POLICY_SAVED_OBJECT_TYPE}.data_output_id:*)`
);
Expand Down Expand Up @@ -129,7 +129,7 @@ describe('ValidateFilterKueryNode validates real kueries through KueryNode', ()
]);
});

it('Test 5 - returns error if the attribute does not exist', async () => {
it('Returns error if the attribute does not exist', async () => {
const astFilter = esKuery.fromKueryExpression(
`${AGENT_POLICY_SAVED_OBJECT_TYPE}.package_policies:test_id_1 or ${AGENT_POLICY_SAVED_OBJECT_TYPE}.package_policies:test_id_2`
);
Expand Down Expand Up @@ -442,9 +442,47 @@ describe('ValidateFilterKueryNode validates real kueries through KueryNode', ()
},
]);
});
it('Search by version', async () => {
const astFilter = esKuery.fromKueryExpression(`fleet-agents.agent.version: 8.10.0`);
const validationObj = validateFilterKueryNode({
astFilter,
types: [AGENTS_PREFIX],
indexMapping: AGENT_MAPPINGS,
storeValue: true,
skipNormalization: true,
});
expect(validationObj).toEqual([
{
astPath: 'arguments.0',
error: null,
isSavedObjectAttr: false,
key: 'fleet-agents.agent.version',
type: 'fleet-agents',
},
]);
});
it('Search by version without SO wrapping', async () => {
const astFilter = esKuery.fromKueryExpression(`agent.version: 8.10.0`);
const validationObj = validateFilterKueryNode({
astFilter,
types: [AGENTS_PREFIX],
indexMapping: AGENT_MAPPINGS,
storeValue: true,
skipNormalization: true,
});
expect(validationObj).toEqual([
{
astPath: 'arguments.0',
error: null,
isSavedObjectAttr: false,
key: 'agent.version',
type: 'agent',
},
]);
});
});

describe('Enrollment Api keys', () => {
describe('Enrollment API keys', () => {
it('Search by policy id', async () => {
const astFilter = esKuery.fromKueryExpression(
`${FLEET_ENROLLMENT_API_PREFIX}.policy_id: policyId1`
Expand Down Expand Up @@ -508,9 +546,7 @@ describe('validateKuery validates real kueries', () => {
true
);
expect(validationObj?.isValid).toEqual(false);
expect(validationObj?.error).toContain(
`KQLSyntaxError: The key is empty and needs to be wrapped by a saved object type like ingest-agent-policies`
);
expect(validationObj?.error).toContain(`KQLSyntaxError: Invalid key`);
});

it('Kuery with non existent parameter wrapped by SO', async () => {
Expand All @@ -526,7 +562,7 @@ describe('validateKuery validates real kueries', () => {
);
});

it('Kuery with non existent parameter', async () => {
it('Invalid search by non existent parameter', async () => {
const validationObj = validateKuery(
`non_existent_parameter: 'test_id'`,
[AGENT_POLICY_SAVED_OBJECT_TYPE],
Expand All @@ -541,7 +577,7 @@ describe('validateKuery validates real kueries', () => {
});

describe('Agents', () => {
it('Test 1 - search policy id', async () => {
it('Search policy id', async () => {
const validationObj = validateKuery(
`${AGENTS_PREFIX}.policy_id: "policy_id"`,
[AGENTS_PREFIX],
Expand All @@ -551,7 +587,7 @@ describe('validateKuery validates real kueries', () => {
expect(validationObj?.isValid).toEqual(true);
});

it('Test 2 - status kuery without SO wrapping', async () => {
it('Status kuery without SO wrapping', async () => {
const validationObj = validateKuery(
`status:online or (status:updating or status:unenrolling or status:enrolling)`,
[AGENTS_PREFIX],
Expand All @@ -561,7 +597,7 @@ describe('validateKuery validates real kueries', () => {
expect(validationObj?.isValid).toEqual(true);
});

it('Test 3 - status kuery with SO wrapping', async () => {
it('Status kuery with SO wrapping', async () => {
const validationObj = validateKuery(
`${AGENTS_PREFIX}.status:online or (${AGENTS_PREFIX}.status:updating or ${AGENTS_PREFIX}.status:unenrolling or ${AGENTS_PREFIX}.status:enrolling)`,
[AGENTS_PREFIX],
Expand All @@ -571,7 +607,7 @@ describe('validateKuery validates real kueries', () => {
expect(validationObj?.isValid).toEqual(true);
});

it('Test 4 - valid kuery without SO wrapping', async () => {
it('Valid kuery without SO wrapping', async () => {
const validationObj = validateKuery(
`local_metadata.elastic.agent.version : "8.6.0"`,
[AGENTS_PREFIX],
Expand Down Expand Up @@ -650,6 +686,39 @@ describe('validateKuery validates real kueries', () => {
);
expect(validationObj?.isValid).toEqual(true);
});

it('Search by version', async () => {
const validationObj = validateKuery(
`agent.version: "8.10.0"`,
[AGENTS_PREFIX],
AGENT_MAPPINGS,
true
);
expect(validationObj?.isValid).toEqual(true);
});

it('Search by activity', async () => {
const validationObj = validateKuery(`active: true`, [AGENTS_PREFIX], AGENT_MAPPINGS, true);
expect(validationObj?.isValid).toEqual(true);
});

it('Search by agent.id', async () => {
const validationObj = validateKuery(`agent.id: id1`, [AGENTS_PREFIX], AGENT_MAPPINGS, true);
expect(validationObj?.isValid).toEqual(true);
});

it('Invalid search by non existent parameter', async () => {
const validationObj = validateKuery(
`non_existent_parameter: 'test_id'`,
[AGENTS_PREFIX],
AGENT_MAPPINGS,
true
);
expect(validationObj?.isValid).toEqual(false);
expect(validationObj?.error).toContain(
`KQLSyntaxError: This type 'non_existent_parameter' is not allowed`
);
});
});

describe('Package policies', () => {
Expand Down Expand Up @@ -751,5 +820,16 @@ describe('validateKuery validates real kueries', () => {
);
expect(validationObj?.isValid).toEqual(true);
});

it('Invalid search by non existent parameter', async () => {
const validationObj = validateKuery(
`policyId1`,
[FLEET_ENROLLMENT_API_PREFIX],
ENROLLMENT_API_KEY_MAPPINGS,
true
);
expect(validationObj?.isValid).toEqual(false);
expect(validationObj?.error).toEqual(`KQLSyntaxError: Invalid key`);
});
});
});

0 comments on commit b3cfe48

Please sign in to comment.