diff --git a/x-pack/plugins/fleet/common/index.ts b/x-pack/plugins/fleet/common/index.ts index 6d89be74ada25d..df9ecae76bea5d 100644 --- a/x-pack/plugins/fleet/common/index.ts +++ b/x-pack/plugins/fleet/common/index.ts @@ -58,6 +58,7 @@ export { ENDPOINT_PRIVILEGES, // dashboards ids DASHBOARD_LOCATORS_IDS, + FLEET_ENROLLMENT_API_PREFIX, } from './constants'; export { // Route services diff --git a/x-pack/plugins/fleet/common/openapi/paths/agents@action_status.yaml b/x-pack/plugins/fleet/common/openapi/paths/agents@action_status.yaml index 89bc0d6c891788..c0e81da8331d52 100644 --- a/x-pack/plugins/fleet/common/openapi/paths/agents@action_status.yaml +++ b/x-pack/plugins/fleet/common/openapi/paths/agents@action_status.yaml @@ -8,7 +8,7 @@ get: - schema: type: integer default: 5 - in: query + in: query name: errorSize responses: '200': @@ -78,7 +78,7 @@ get: type: string description: policy id (POLICY_CHANGE action) revision: - type: string + type: string description: new policy revision (POLICY_CHANGE action) creationTime: type: string @@ -90,11 +90,11 @@ get: type: object properties: agentId: - type: string + type: string error: type: string timestamp: - type: string + type: string required: - actionId - complete diff --git a/x-pack/plugins/fleet/common/types/models/agent.ts b/x-pack/plugins/fleet/common/types/models/agent.ts index 6c4108abf622ab..f8ed69d6d4d339 100644 --- a/x-pack/plugins/fleet/common/types/models/agent.ts +++ b/x-pack/plugins/fleet/common/types/models/agent.ts @@ -415,3 +415,9 @@ export interface FleetServerAgentAction { [k: string]: unknown; } + +export interface ActionStatusOptions { + errorSize: number; + page?: number; + perPage?: number; +} diff --git a/x-pack/plugins/fleet/common/types/rest_spec/common.ts b/x-pack/plugins/fleet/common/types/rest_spec/common.ts index cd3279b4cb25e7..07bd3c7019c168 100644 --- a/x-pack/plugins/fleet/common/types/rest_spec/common.ts +++ b/x-pack/plugins/fleet/common/types/rest_spec/common.ts @@ -7,9 +7,6 @@ import type { HttpFetchQuery } from '@kbn/core/public'; -/** - * @deprecated will be replaced by a "narrow" set of parameters - */ export interface ListWithKuery extends HttpFetchQuery { page?: number; perPage?: number; diff --git a/x-pack/plugins/fleet/public/applications/fleet/components/search_bar.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/components/search_bar.test.tsx index e13c7006e36dc5..5478aca7c5baaa 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/components/search_bar.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/components/search_bar.test.tsx @@ -208,14 +208,6 @@ describe('SearchBar', () => { }); describe('filterAndConvertFields', () => { - it('prepends the fieldPrefix if passed and hides some fields ', async () => { - expect(filterAndConvertFields(fields, '.test-index', 'test-index')).toEqual({ - 'test-index.api_key': { esTypes: ['keyword'], name: 'test-index.api_key', type: 'string' }, - 'test-index.name': { esTypes: ['keyword'], name: 'test-index.name', type: 'string' }, - 'test-index.version': { esTypes: ['keyword'], name: 'test-index.version', type: 'string' }, - }); - }); - it('leaves the fields names unchanged and does not hide any fields if fieldPrefix is not passed', async () => { expect(filterAndConvertFields(fields, '.test-index')).toEqual({ _id: { esTypes: ['_id'], name: '_id', type: 'string' }, diff --git a/x-pack/plugins/fleet/public/applications/fleet/components/search_bar.tsx b/x-pack/plugins/fleet/public/applications/fleet/components/search_bar.tsx index f2a26126f36378..5e610a46e3f345 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/components/search_bar.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/components/search_bar.tsx @@ -49,22 +49,14 @@ export const filterAndConvertFields = ( if (indexPattern === INDEX_NAME) { filteredFields = fields.filter((field) => field.name.startsWith(fieldPrefix)); } else { - // Concatenate the fields with the prefix - const withPrefix = fields.map((field) => { - return !field.name.startsWith(fieldPrefix) - ? { ...field, name: `${fieldPrefix}.${field.name}` } - : field; - }); // filter out fields that have names to be hidden - filteredFields = withPrefix.filter((field) => { - if (field.name.startsWith(fieldPrefix)) { - for (const hiddenField of HIDDEN_FIELDS) { - if (field.name.includes(hiddenField)) { - return false; - } + filteredFields = fields.filter((field) => { + for (const hiddenField of HIDDEN_FIELDS) { + if (field.name.includes(hiddenField)) { + return false; } - return true; } + return true; }); } } else { diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/hooks/use_fleet_server_unhealthy.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/hooks/use_fleet_server_unhealthy.tsx index 3c7ba71199eaa7..5dd3fb9b5dfd99 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/hooks/use_fleet_server_unhealthy.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/hooks/use_fleet_server_unhealthy.tsx @@ -37,7 +37,7 @@ export function useFleetServerUnhealthy() { if (agentPolicyIds.length > 0) { const agentStatusesRes = await sendGetAgentStatus({ - kuery: agentPolicyIds.map((policyId) => `policy_id:"${policyId}"`).join(' or '), + kuery: agentPolicyIds.map((policyId) => `policy_id:${policyId}`).join(' or '), }); if (agentStatusesRes.error) { diff --git a/x-pack/plugins/fleet/server/constants/index.ts b/x-pack/plugins/fleet/server/constants/index.ts index 3635f8b38020af..356aaa0e0cf8e0 100644 --- a/x-pack/plugins/fleet/server/constants/index.ts +++ b/x-pack/plugins/fleet/server/constants/index.ts @@ -95,3 +95,4 @@ export { } from './fleet_es_assets'; export { FILE_STORAGE_DATA_AGENT_INDEX } from './fleet_es_assets'; export { FILE_STORAGE_METADATA_AGENT_INDEX } from './fleet_es_assets'; +export * from './mappings'; diff --git a/x-pack/plugins/fleet/server/constants/mappings.ts b/x-pack/plugins/fleet/server/constants/mappings.ts new file mode 100644 index 00000000000000..617a5d35a46de0 --- /dev/null +++ b/x-pack/plugins/fleet/server/constants/mappings.ts @@ -0,0 +1,332 @@ +/* + * 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. + */ + +/** + * 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 + */ + +export const AGENT_POLICY_MAPPINGS = { + properties: { + agent_features: { + properties: { + name: { type: 'keyword' }, + enabled: { type: 'boolean' }, + }, + }, + data_output_id: { type: 'keyword' }, + description: { type: 'text' }, + download_source_id: { type: 'keyword' }, + fleet_server_host_id: { type: 'keyword' }, + inactivity_timeout: { type: 'integer' }, + is_default: { type: 'boolean' }, + is_default_fleet_server: { type: 'boolean' }, + is_managed: { type: 'boolean' }, + is_preconfigured: { type: 'keyword' }, + is_protected: { type: 'boolean' }, + monitoring_enabled: { type: 'keyword', index: false }, + monitoring_output_id: { type: 'keyword' }, + name: { type: 'keyword' }, + namespace: { type: 'keyword' }, + revision: { type: 'integer' }, + schema_version: { type: 'version' }, + status: { type: 'keyword' }, + unenroll_timeout: { type: 'integer' }, + updated_at: { type: 'date' }, + updated_by: { type: 'keyword' }, + }, +} as const; + +export const PACKAGE_POLICIES_MAPPINGS = { + properties: { + name: { type: 'keyword' }, + description: { type: 'text' }, + namespace: { type: 'keyword' }, + enabled: { type: 'boolean' }, + is_managed: { type: 'boolean' }, + policy_id: { type: 'keyword' }, + package: { + properties: { + name: { type: 'keyword' }, + title: { type: 'keyword' }, + version: { type: 'keyword' }, + }, + }, + elasticsearch: { + dynamic: false, + properties: {}, + }, + vars: { type: 'flattened' }, + inputs: { + dynamic: false, + properties: {}, + }, + secret_references: { properties: { id: { type: 'keyword' } } }, + revision: { type: 'integer' }, + updated_at: { type: 'date' }, + updated_by: { type: 'keyword' }, + created_at: { type: 'date' }, + created_by: { type: 'keyword' }, + }, +} as const; + +export const AGENT_MAPPINGS = { + properties: { + access_api_key_id: { + type: 'keyword', + }, + action_seq_no: { + type: 'integer', + }, + active: { + type: 'boolean', + }, + agent: { + properties: { + id: { + type: 'keyword', + }, + version: { + type: 'keyword', + }, + }, + }, + default_api_key: { + type: 'keyword', + }, + default_api_key_id: { + type: 'keyword', + }, + enrollment_id: { + type: 'keyword', + }, + enrolled_at: { + type: 'date', + }, + last_checkin: { + type: 'date', + }, + last_checkin_message: { + type: 'text', + properties: { + keyword: { + type: 'keyword', + }, + }, + }, + last_checkin_status: { + type: 'keyword', + }, + last_updated: { + type: 'date', + }, + local_metadata: { + properties: { + elastic: { + properties: { + agent: { + properties: { + build: { + properties: { + original: { + type: 'text', + properties: { + keyword: { + type: 'keyword', + }, + }, + }, + }, + }, + id: { + type: 'keyword', + }, + log_level: { + type: 'keyword', + }, + snapshot: { + type: 'boolean', + }, + upgradeable: { + type: 'boolean', + }, + version: { + type: 'text', + properties: { + keyword: { + type: 'keyword', + }, + }, + }, + }, + }, + }, + }, + host: { + properties: { + architecture: { + type: 'keyword', + }, + hostname: { + type: 'text', + properties: { + keyword: { + type: 'keyword', + }, + }, + }, + id: { + type: 'keyword', + }, + ip: { + type: 'text', + properties: { + keyword: { + type: 'keyword', + }, + }, + }, + mac: { + type: 'text', + properties: { + keyword: { + type: 'keyword', + }, + }, + }, + name: { + type: 'text', + properties: { + keyword: { + type: 'keyword', + }, + }, + }, + }, + }, + os: { + properties: { + family: { + type: 'keyword', + }, + full: { + type: 'text', + properties: { + keyword: { + type: 'keyword', + }, + }, + }, + kernel: { + type: 'text', + properties: { + keyword: { + type: 'keyword', + }, + }, + }, + name: { + type: 'text', + properties: { + keyword: { + type: 'keyword', + }, + }, + }, + platform: { + type: 'keyword', + }, + version: { + type: 'text', + properties: { + keyword: { + type: 'keyword', + }, + }, + }, + }, + }, + }, + }, + packages: { + type: 'keyword', + }, + policy_output_permissions_hash: { + type: 'keyword', + }, + policy_coordinator_idx: { + type: 'integer', + }, + policy_id: { + type: 'keyword', + }, + policy_revision_idx: { + type: 'integer', + }, + type: { + type: 'keyword', + }, + tags: { + type: 'keyword', + }, + unenrolled_at: { + type: 'date', + }, + unenrollment_started_at: { + type: 'date', + }, + unenrolled_reason: { + type: 'keyword', + }, + updated_at: { + type: 'date', + }, + upgrade_started_at: { + type: 'date', + }, + upgraded_at: { + type: 'date', + }, + upgrade_status: { + type: 'keyword', + }, + // added to allow validation on status field + status: { + type: 'keyword', + }, + }, +} as const; + +export const ENROLLMENT_API_KEY_MAPPINGS = { + properties: { + active: { + type: 'boolean', + }, + api_key: { + type: 'keyword', + }, + api_key_id: { + type: 'keyword', + }, + created_at: { + type: 'date', + }, + expire_at: { + type: 'date', + }, + name: { + type: 'keyword', + }, + policy_id: { + type: 'keyword', + }, + updated_at: { + type: 'date', + }, + }, +} as const; diff --git a/x-pack/plugins/fleet/server/routes/utils/filter_utils.test.ts b/x-pack/plugins/fleet/server/routes/utils/filter_utils.test.ts new file mode 100644 index 00000000000000..f888959b152c40 --- /dev/null +++ b/x-pack/plugins/fleet/server/routes/utils/filter_utils.test.ts @@ -0,0 +1,444 @@ +/* + * 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 * as esKuery from '@kbn/es-query'; + +import { validateFilterKueryNode } from './filter_utils'; + +const mockMappings = { + properties: { + updated_at: { + type: 'date', + }, + foo: { + properties: { + title: { + type: 'text', + }, + description: { + type: 'text', + }, + bytes: { + type: 'integer', + }, + }, + }, + bar: { + properties: { + _id: { + type: 'keyword', + }, + foo: { + type: 'text', + }, + description: { + type: 'text', + }, + }, + }, + bean: { + properties: { + canned: { + fields: { + text: { + type: 'text', + }, + }, + type: 'keyword', + }, + }, + }, + alert: { + properties: { + actions: { + type: 'nested', + properties: { + group: { + type: 'keyword', + }, + actionRef: { + type: 'keyword', + }, + actionTypeId: { + type: 'keyword', + }, + params: { + enabled: false, + type: 'object', + }, + }, + }, + params: { + type: 'flattened', + }, + }, + }, + hiddenType: { + properties: { + description: { + type: 'text', + }, + }, + }, + }, +} as const; + +describe('Filter Utils', () => { + describe('ValidateFilterKueryNode', () => { + describe('Validate general kueries through KueryNode', () => { + it('Simple filter', () => { + const validationObject = validateFilterKueryNode({ + astFilter: esKuery.fromKueryExpression( + 'foo.updated_at: 5678654567 and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or foo.attributes.description :*)' + ), + types: ['foo'], + indexMapping: mockMappings, + }); + + expect(validationObject).toEqual([ + { + astPath: 'arguments.0', + error: null, + isSavedObjectAttr: true, + key: 'foo.updated_at', + type: 'foo', + }, + { + astPath: 'arguments.1', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.bytes', + type: 'foo', + }, + { + astPath: 'arguments.2', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.bytes', + type: 'foo', + }, + { + astPath: 'arguments.3', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.title', + type: 'foo', + }, + { + astPath: 'arguments.4.arguments.0', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.description', + type: 'foo', + }, + { + astPath: 'arguments.4.arguments.1', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.description', + type: 'foo', + }, + ]); + }); + + it('Nested filter query', () => { + const validationObject = validateFilterKueryNode({ + astFilter: esKuery.fromKueryExpression( + 'alert.attributes.actions:{ actionTypeId: ".server-log" }' + ), + types: ['alert'], + indexMapping: mockMappings, + hasNestedKey: true, + }); + expect(validationObject).toEqual([ + { + astPath: 'arguments.1', + error: null, + isSavedObjectAttr: false, + key: 'alert.attributes.actions.actionTypeId', + type: 'alert', + }, + ]); + }); + + it('Accept defined key even if not wrapped by a saved object type', () => { + const validationObject = validateFilterKueryNode({ + astFilter: esKuery.fromKueryExpression( + 'updated_at: 5678654567 and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or foo.attributes.description :*)' + ), + types: ['foo'], + indexMapping: mockMappings, + }); + + expect(validationObject).toEqual([ + { + astPath: 'arguments.0', + error: null, + isSavedObjectAttr: true, + key: 'updated_at', + type: null, + }, + { + astPath: 'arguments.1', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.bytes', + type: 'foo', + }, + { + astPath: 'arguments.2', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.bytes', + type: 'foo', + }, + { + astPath: 'arguments.3', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.title', + type: 'foo', + }, + { + astPath: 'arguments.4.arguments.0', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.description', + type: 'foo', + }, + { + astPath: 'arguments.4.arguments.1', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.description', + type: 'foo', + }, + ]); + }); + + it('Return Error if key of a saved object type is not wrapped with attributes', () => { + const validationObject = validateFilterKueryNode({ + astFilter: esKuery.fromKueryExpression( + 'foo.updated_at: 5678654567 and foo.attributes.bytes > 1000 and foo.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or foo.description :*)' + ), + types: ['foo'], + indexMapping: mockMappings, + }); + + expect(validationObject).toEqual([ + { + astPath: 'arguments.0', + error: null, + isSavedObjectAttr: true, + key: 'foo.updated_at', + type: 'foo', + }, + { + astPath: 'arguments.1', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.bytes', + type: 'foo', + }, + { + astPath: 'arguments.2', + error: + "This key 'foo.bytes' does NOT match the filter proposition SavedObjectType.attributes.key", + isSavedObjectAttr: false, + key: 'foo.bytes', + type: 'foo', + }, + { + astPath: 'arguments.3', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.title', + type: 'foo', + }, + { + astPath: 'arguments.4.arguments.0', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.description', + type: 'foo', + }, + { + astPath: 'arguments.4.arguments.1', + error: + "This key 'foo.description' does NOT match the filter proposition SavedObjectType.attributes.key", + isSavedObjectAttr: false, + key: 'foo.description', + type: 'foo', + }, + ]); + }); + + it('Return Error if filter is not using an allowed type', () => { + const validationObject = validateFilterKueryNode({ + astFilter: esKuery.fromKueryExpression( + 'bar.updated_at: 5678654567 and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.title: "best" and (foo.attributes.description: t* or foo.attributes.description :*)' + ), + types: ['foo'], + indexMapping: mockMappings, + }); + + expect(validationObject).toEqual([ + { + astPath: 'arguments.0', + error: `This key 'bar.updated_at' does NOT exist in foo saved object index patterns`, + isSavedObjectAttr: true, + key: 'bar.updated_at', + type: 'bar', + }, + { + astPath: 'arguments.1', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.bytes', + type: 'foo', + }, + { + astPath: 'arguments.2', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.bytes', + type: 'foo', + }, + { + astPath: 'arguments.3', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.title', + type: 'foo', + }, + { + astPath: 'arguments.4.arguments.0', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.description', + type: 'foo', + }, + { + astPath: 'arguments.4.arguments.1', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.description', + type: 'foo', + }, + ]); + }); + + it('Return Error if filter is using an non-existing key in the index patterns of the saved object type', () => { + const validationObject = validateFilterKueryNode({ + astFilter: esKuery.fromKueryExpression( + 'foo.updated_at33: 5678654567 and foo.attributes.bytes > 1000 and foo.attributes.bytes < 8000 and foo.attributes.header: "best" and (foo.attributes.description: t* or foo.attributes.description :*)' + ), + types: ['foo'], + indexMapping: mockMappings, + }); + + expect(validationObject).toEqual([ + { + astPath: 'arguments.0', + error: "This key 'foo.updated_at33' does NOT exist in foo saved object index patterns", + isSavedObjectAttr: false, + key: 'foo.updated_at33', + type: 'foo', + }, + { + astPath: 'arguments.1', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.bytes', + type: 'foo', + }, + { + astPath: 'arguments.2', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.bytes', + type: 'foo', + }, + { + astPath: 'arguments.3', + error: + "This key 'foo.attributes.header' does NOT exist in foo saved object index patterns", + isSavedObjectAttr: false, + key: 'foo.attributes.header', + type: 'foo', + }, + { + astPath: 'arguments.4.arguments.0', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.description', + type: 'foo', + }, + { + astPath: 'arguments.4.arguments.1', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.description', + type: 'foo', + }, + ]); + }); + + it('Return Error if filter is using an non-existing key null key', () => { + const validationObject = validateFilterKueryNode({ + astFilter: esKuery.fromKueryExpression('foo.attributes.description: hello AND bye'), + types: ['foo'], + indexMapping: mockMappings, + }); + + expect(validationObject).toEqual([ + { + astPath: 'arguments.0', + error: null, + isSavedObjectAttr: false, + key: 'foo.attributes.description', + type: 'foo', + }, + { + astPath: 'arguments.1', + error: 'The key is empty and needs to be wrapped by a saved object type like foo', + isSavedObjectAttr: false, + key: null, + type: null, + }, + ]); + }); + + it('Multiple nested filter queries', () => { + const validationObject = validateFilterKueryNode({ + astFilter: esKuery.fromKueryExpression( + 'alert.attributes.actions:{ actionTypeId: ".server-log" AND actionRef: "foo" }' + ), + types: ['alert'], + indexMapping: mockMappings, + }); + + expect(validationObject).toEqual([ + { + astPath: 'arguments.1.arguments.0', + error: null, + isSavedObjectAttr: false, + key: 'alert.attributes.actions.actionTypeId', + type: 'alert', + }, + { + astPath: 'arguments.1.arguments.1', + error: null, + isSavedObjectAttr: false, + key: 'alert.attributes.actions.actionRef', + type: 'alert', + }, + ]); + }); + }); + }); +}); diff --git a/x-pack/plugins/fleet/server/routes/utils/filter_utils.ts b/x-pack/plugins/fleet/server/routes/utils/filter_utils.ts new file mode 100644 index 00000000000000..fccddd66891c6b --- /dev/null +++ b/x-pack/plugins/fleet/server/routes/utils/filter_utils.ts @@ -0,0 +1,255 @@ +/* + * 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 { get } from 'lodash'; +import * as esKuery from '@kbn/es-query'; +import type { IndexMapping } from '@kbn/core-saved-objects-base-server-internal'; + +type KueryNode = any; + +const astFunctionType = ['is', 'range', 'nested']; +const allowedTerms = ['_exists_']; + +interface ValidateFilterKueryNode { + astPath: string; + error: string; + isSavedObjectAttr: boolean; + key: string; + type: string | null; +} + +interface ValidateFilterKueryNodeParams { + astFilter: KueryNode; + types: string[]; + indexMapping: IndexMapping; + hasNestedKey?: boolean; + nestedKeys?: string; + storeValue?: boolean; + path?: string; + skipNormalization?: boolean; +} + +export const validateFilterKueryNode = ({ + astFilter, + types, + indexMapping, + hasNestedKey = false, + nestedKeys, + storeValue = false, + path = 'arguments', + skipNormalization, +}: ValidateFilterKueryNodeParams): ValidateFilterKueryNode[] => { + let localNestedKeys: string | undefined; + return astFilter.arguments.reduce((kueryNode: string[], ast: KueryNode, index: number) => { + if (hasNestedKey && ast.type === 'literal' && ast.value != null) { + localNestedKeys = ast.value; + } else if (ast.type === 'literal' && ast.value && typeof ast.value === 'string') { + const key = ast.value.replace('.attributes', ''); + const mappingKey = 'properties.' + key.split('.').join('.properties.'); + const field = get(indexMapping, mappingKey); + + if (field != null && field.type === 'nested') { + localNestedKeys = ast.value; + } + } + + if (ast.arguments) { + const myPath = `${path}.${index}`; + return [ + ...kueryNode, + ...validateFilterKueryNode({ + astFilter: ast, + types, + indexMapping, + storeValue: ast.type === 'function' && astFunctionType.includes(ast.function), + path: `${myPath}.arguments`, + hasNestedKey: ast.type === 'function' && ast.function === 'nested', + nestedKeys: localNestedKeys || nestedKeys, + skipNormalization, + }), + ]; + } + if (storeValue && index === 0) { + const splitPath = path.split('.'); + const astPath = path.includes('.') + ? splitPath.slice(0, splitPath.length - 1).join('.') + : `${path}.${index}`; + const key = nestedKeys != null ? `${nestedKeys}.${ast.value}` : ast.value; + + return [ + ...kueryNode, + { + astPath, + error: hasFilterKeyError(key, types, indexMapping, skipNormalization), + isSavedObjectAttr: isSavedObjectAttr(key, indexMapping), + key, + type: getType(key), + }, + ]; + } + return kueryNode; + }, []); +}; + +const getType = (key: string | undefined | null) => { + if (key != null && key.includes('.')) { + return key.split('.')[0]; + } else if (allowedTerms.some((term) => term === key)) { + return 'searchTerm'; + } else { + return null; + } +}; + +/** + * Is this filter key referring to a a top-level SavedObject attribute such as + * `updated_at` or `references`. + * + * @param key + * @param indexMapping + */ +export const isSavedObjectAttr = (key: string | null | undefined, indexMapping: IndexMapping) => { + const keySplit = key != null ? key.split('.') : []; + if (keySplit.length === 1 && fieldDefined(indexMapping, keySplit[0])) { + return true; + } else if (keySplit.length === 2 && keySplit[1] === 'id') { + return true; + } else if (keySplit.length === 2 && fieldDefined(indexMapping, keySplit[1])) { + return true; + } else { + return false; + } +}; + +export const hasFilterKeyError = ( + key: string | null | undefined, + types: string[], + 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.includes('.')) { + if (allowedTerms.some((term) => term === key) || fieldDefined(indexMapping, key)) { + return null; + } + 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`; + } + // In some cases we don't want to check about the `attributes` presence + // In that case pass the `skipNormalization` parameter + if ( + (!skipNormalization && keySplit.length === 2 && fieldDefined(indexMapping, key)) || + (!skipNormalization && keySplit.length > 2 && keySplit[1] !== 'attributes') + ) { + 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)) + ) { + return `This key '${key}' does NOT exist in ${types.join()} saved object index patterns`; + } + } + return null; +}; + +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; + } + + if (mappingKey === 'properties.id') { + return true; + } + + // If the `mappingKey` does not match a valid path, before returning false, + // we want to check and see if the intended path was for a multi-field + // such as `x.attributes.field.text` where `field` is mapped to both text + // and keyword + const propertiesAttribute = 'properties'; + const indexOfLastProperties = mappingKey.lastIndexOf(propertiesAttribute); + const fieldMapping = mappingKey.substr(0, indexOfLastProperties); + const fieldType = mappingKey.substr( + mappingKey.lastIndexOf(propertiesAttribute) + `${propertiesAttribute}.`.length + ); + const mapping = `${fieldMapping}fields.${fieldType}`; + if (get(indexMappings, mapping) != null) { + return true; + } + + // If the path is for a flattened type field, we'll assume the mappings are defined. + const keys = key.split('.'); + for (let i = 0; i < keys.length; i++) { + const path = `properties.${keys.slice(0, i + 1).join('.properties.')}`; + if (get(indexMappings, path)?.type === 'flattened') { + return true; + } + } + + return false; +}; + +export const validateKuery = ( + kuery: string | undefined, + allowedTypes: string[], + indexMapping: IndexMapping, + skipNormalization?: boolean +) => { + let isValid = true; + let error: string | undefined; + + if (!kuery) { + isValid = true; + } + try { + if (kuery && indexMapping) { + const astFilter = esKuery.fromKueryExpression(kuery); + const validationObject = validateFilterKueryNode({ + astFilter, + types: allowedTypes, + indexMapping, + storeValue: true, + skipNormalization, + }); + if (validationObject.some((obj) => obj.error != null)) { + error = `KQLSyntaxError: ${validationObject + .filter((obj) => obj.error != null) + .map((obj) => obj.error) + .join('\n')}`; + isValid = false; + } + } else { + isValid = true; + } + return { isValid, error }; + } catch (e) { + isValid = false; + error = e.message; + } +}; diff --git a/x-pack/plugins/fleet/server/routes/utils/filter_utils_real_queries.test.ts b/x-pack/plugins/fleet/server/routes/utils/filter_utils_real_queries.test.ts new file mode 100644 index 00000000000000..e8d3b655988061 --- /dev/null +++ b/x-pack/plugins/fleet/server/routes/utils/filter_utils_real_queries.test.ts @@ -0,0 +1,755 @@ +/* + * 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 * as esKuery from '@kbn/es-query'; + +import { + AGENT_POLICY_SAVED_OBJECT_TYPE, + PACKAGE_POLICY_SAVED_OBJECT_TYPE, + AGENTS_PREFIX, + AGENT_POLICY_MAPPINGS, + PACKAGE_POLICIES_MAPPINGS, + AGENT_MAPPINGS, + ENROLLMENT_API_KEY_MAPPINGS, +} from '../../constants'; + +import { FLEET_ENROLLMENT_API_PREFIX } from '../../../common/constants'; + +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 () => { + const astFilter = esKuery.fromKueryExpression( + `${AGENT_POLICY_SAVED_OBJECT_TYPE}.data_output_id: test_id` + ); + const validationObject = validateFilterKueryNode({ + astFilter, + types: [AGENT_POLICY_SAVED_OBJECT_TYPE], + indexMapping: AGENT_POLICY_MAPPINGS, + storeValue: true, + }); + expect(validationObject).toEqual([ + { + astPath: 'arguments.0', + error: null, + isSavedObjectAttr: true, + key: 'ingest-agent-policies.data_output_id', + type: 'ingest-agent-policies', + }, + ]); + }); + + it('Test 2 - search by inactivity timeout', async () => { + const astFilter = esKuery.fromKueryExpression( + `${AGENT_POLICY_SAVED_OBJECT_TYPE}.inactivity_timeout:*` + ); + const validationObject = validateFilterKueryNode({ + astFilter, + types: [AGENT_POLICY_SAVED_OBJECT_TYPE], + indexMapping: AGENT_POLICY_MAPPINGS, + storeValue: true, + }); + expect(validationObject).toEqual([ + { + astPath: 'arguments.0', + error: null, + isSavedObjectAttr: true, + key: 'ingest-agent-policies.inactivity_timeout', + type: 'ingest-agent-policies', + }, + ]); + }); + + it('Test 3 - 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:*)` + ), + types: [AGENT_POLICY_SAVED_OBJECT_TYPE], + indexMapping: AGENT_POLICY_MAPPINGS, + storeValue: true, + }); + + expect(validationObject).toEqual([ + { + astPath: 'arguments.0', + error: null, + isSavedObjectAttr: true, + key: 'ingest-agent-policies.download_source_id', + type: 'ingest-agent-policies', + }, + { + astPath: 'arguments.1.arguments.0', + error: null, + isSavedObjectAttr: true, + key: 'ingest-agent-policies.download_source_id', + type: 'ingest-agent-policies', + }, + ]); + }); + + it('Test 4', 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:*)` + ); + const validationObject = validateFilterKueryNode({ + astFilter, + types: [AGENT_POLICY_SAVED_OBJECT_TYPE], + indexMapping: AGENT_POLICY_MAPPINGS, + storeValue: true, + }); + + expect(validationObject).toEqual([ + { + astPath: 'arguments.0', + error: null, + isSavedObjectAttr: true, + key: 'ingest-agent-policies.data_output_id', + type: 'ingest-agent-policies', + }, + { + astPath: 'arguments.1', + error: null, + isSavedObjectAttr: true, + key: 'ingest-agent-policies.monitoring_output_id', + type: 'ingest-agent-policies', + }, + { + astPath: 'arguments.2.arguments.0', + error: null, + isSavedObjectAttr: true, + key: 'ingest-agent-policies.data_output_id', + type: 'ingest-agent-policies', + }, + ]); + }); + + it('Test 5 - 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` + ); + const validationObject = validateFilterKueryNode({ + astFilter, + types: [AGENT_POLICY_SAVED_OBJECT_TYPE], + indexMapping: AGENT_POLICY_MAPPINGS, + storeValue: true, + }); + expect(validationObject).toEqual([ + { + astPath: 'arguments.0', + error: + "This key 'ingest-agent-policies.package_policies' does NOT exist in ingest-agent-policies saved object index patterns", + isSavedObjectAttr: false, + key: 'ingest-agent-policies.package_policies', + type: 'ingest-agent-policies', + }, + { + astPath: 'arguments.1', + error: + "This key 'ingest-agent-policies.package_policies' does NOT exist in ingest-agent-policies saved object index patterns", + isSavedObjectAttr: false, + key: 'ingest-agent-policies.package_policies', + type: 'ingest-agent-policies', + }, + ]); + }); + }); + + describe('Package policies', () => { + it('Search by package name', async () => { + const astFilter = esKuery.fromKueryExpression( + `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.attributes.package.name:packageName` + ); + const validationObject = validateFilterKueryNode({ + astFilter, + types: [PACKAGE_POLICY_SAVED_OBJECT_TYPE], + indexMapping: PACKAGE_POLICIES_MAPPINGS, + storeValue: true, + }); + expect(validationObject).toEqual([ + { + astPath: 'arguments.0', + error: null, + isSavedObjectAttr: false, + key: 'ingest-package-policies.attributes.package.name', + type: 'ingest-package-policies', + }, + ]); + }); + + it('It fails if the kuery is not normalized', async () => { + const astFilter = esKuery.fromKueryExpression( + `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:packageName` + ); + const validationObject = validateFilterKueryNode({ + astFilter, + types: [PACKAGE_POLICY_SAVED_OBJECT_TYPE], + indexMapping: PACKAGE_POLICIES_MAPPINGS, + storeValue: true, + }); + expect(validationObject).toEqual([ + { + astPath: 'arguments.0', + error: + "This key 'ingest-package-policies.package.name' does NOT match the filter proposition SavedObjectType.attributes.key", + isSavedObjectAttr: false, + key: 'ingest-package-policies.package.name', + type: 'ingest-package-policies', + }, + ]); + }); + + it('It does not check attributes if skipNormalization is passed', async () => { + const astFilter = esKuery.fromKueryExpression( + `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:packageName` + ); + const validationObject = validateFilterKueryNode({ + astFilter, + types: [PACKAGE_POLICY_SAVED_OBJECT_TYPE], + indexMapping: PACKAGE_POLICIES_MAPPINGS, + storeValue: true, + skipNormalization: true, + }); + expect(validationObject).toEqual([ + { + astPath: 'arguments.0', + error: null, + isSavedObjectAttr: false, + key: 'ingest-package-policies.package.name', + type: 'ingest-package-policies', + }, + ]); + }); + + it('Allows passing query without SO', async () => { + const astFilter = esKuery.fromKueryExpression(`package.name:packageName`); + const validationObject = validateFilterKueryNode({ + astFilter, + types: [PACKAGE_POLICY_SAVED_OBJECT_TYPE], + indexMapping: PACKAGE_POLICIES_MAPPINGS, + storeValue: true, + skipNormalization: true, + }); + expect(validationObject).toEqual([ + { + astPath: 'arguments.0', + error: null, + isSavedObjectAttr: true, + key: 'package.name', + type: 'package', + }, + ]); + }); + }); + + describe('Agents', () => { + it('Search policy id', async () => { + const astFilter = esKuery.fromKueryExpression(`${AGENTS_PREFIX}.policy_id: "policy_id"`); + const validationObject = validateFilterKueryNode({ + astFilter, + types: [AGENTS_PREFIX], + indexMapping: AGENT_MAPPINGS, + storeValue: true, + }); + expect(validationObject).toEqual([ + { + astPath: 'arguments.0', + error: null, + isSavedObjectAttr: true, + key: 'fleet-agents.policy_id', + type: 'fleet-agents', + }, + ]); + }); + + it('Search by multiple ids', async () => { + const astFilter = esKuery.fromKueryExpression( + `${AGENTS_PREFIX}.attributes.agent.id : (id_1 or id_2)` + ); + + const validationObject = validateFilterKueryNode({ + astFilter, + types: [AGENTS_PREFIX], + indexMapping: AGENT_MAPPINGS, + storeValue: true, + }); + expect(validationObject).toEqual([ + { + astPath: 'arguments.0', + error: null, + isSavedObjectAttr: false, + key: 'fleet-agents.attributes.agent.id', + type: 'fleet-agents', + }, + { + astPath: 'arguments.1', + error: null, + isSavedObjectAttr: false, + key: 'fleet-agents.attributes.agent.id', + type: 'fleet-agents', + }, + ]); + }); + + it('Search agent by policy Id and enrolled since more than 10m', async () => { + const astFilter = esKuery.fromKueryExpression( + `${AGENTS_PREFIX}.policy_id: "policyId" and not (_exists_: "${AGENTS_PREFIX}.unenrolled_at") and ${AGENTS_PREFIX}.enrolled_at >= now-10m` + ); + const validationObject = validateFilterKueryNode({ + astFilter, + types: [AGENTS_PREFIX], + indexMapping: AGENT_MAPPINGS, + storeValue: true, + }); + expect(validationObject).toEqual([ + { + astPath: 'arguments.0', + error: null, + isSavedObjectAttr: true, + key: 'fleet-agents.policy_id', + type: 'fleet-agents', + }, + { + astPath: 'arguments.1.arguments.0', + error: null, + isSavedObjectAttr: false, + key: '_exists_', + type: 'searchTerm', + }, + { + astPath: 'arguments.2', + error: null, + isSavedObjectAttr: true, + key: 'fleet-agents.enrolled_at', + type: 'fleet-agents', + }, + ]); + }); + + it('Search agent by multiple policy Ids and tags', async () => { + const astFilter = esKuery.fromKueryExpression( + `${AGENTS_PREFIX}.policy_id: (policyId1 or policyId2) and ${AGENTS_PREFIX}.tags: (tag1)` + ); + const validationObject = validateFilterKueryNode({ + astFilter, + types: [AGENTS_PREFIX], + indexMapping: AGENT_MAPPINGS, + storeValue: true, + }); + expect(validationObject).toEqual([ + { + astPath: 'arguments.0.arguments.0', + error: null, + isSavedObjectAttr: true, + key: 'fleet-agents.policy_id', + type: 'fleet-agents', + }, + { + astPath: 'arguments.0.arguments.1', + error: null, + isSavedObjectAttr: true, + key: 'fleet-agents.policy_id', + type: 'fleet-agents', + }, + { + astPath: 'arguments.1', + error: null, + isSavedObjectAttr: true, + key: 'fleet-agents.tags', + type: 'fleet-agents', + }, + ]); + }); + + it('Search agent by multiple tags', async () => { + const astFilter = esKuery.fromKueryExpression( + `${AGENTS_PREFIX}.tags: (tag1 or tag2 or tag3)` + ); + const validationObject = validateFilterKueryNode({ + astFilter, + types: [AGENTS_PREFIX], + indexMapping: AGENT_MAPPINGS, + storeValue: true, + }); + expect(validationObject).toEqual([ + { + astPath: 'arguments.0', + error: null, + isSavedObjectAttr: true, + key: 'fleet-agents.tags', + type: 'fleet-agents', + }, + { + astPath: 'arguments.1', + error: null, + isSavedObjectAttr: true, + key: 'fleet-agents.tags', + type: 'fleet-agents', + }, + { + astPath: 'arguments.2', + error: null, + isSavedObjectAttr: true, + key: 'fleet-agents.tags', + type: 'fleet-agents', + }, + ]); + }); + + it('Returns error if kuery is passed without a reference to the index', async () => { + const astFilter = esKuery.fromKueryExpression( + `${AGENTS_PREFIX}.status:online or (${AGENTS_PREFIX}.status:updating or ${AGENTS_PREFIX}.status:unenrolling or ${AGENTS_PREFIX}.status:enrolling)` + ); + const validationObject = validateFilterKueryNode({ + astFilter, + types: [AGENTS_PREFIX], + indexMapping: AGENT_MAPPINGS, + storeValue: true, + }); + expect(validationObject).toEqual([ + { + astPath: 'arguments.0', + error: null, + isSavedObjectAttr: true, + key: 'fleet-agents.status', + type: 'fleet-agents', + }, + { + astPath: 'arguments.1.arguments.0', + error: null, + isSavedObjectAttr: true, + key: 'fleet-agents.status', + type: 'fleet-agents', + }, + { + astPath: 'arguments.1.arguments.1', + error: null, + isSavedObjectAttr: true, + key: 'fleet-agents.status', + type: 'fleet-agents', + }, + { + astPath: 'arguments.1.arguments.2', + error: null, + isSavedObjectAttr: true, + key: 'fleet-agents.status', + type: 'fleet-agents', + }, + ]); + }); + }); + + describe('Enrollment Api keys', () => { + it('Search by policy id', async () => { + const astFilter = esKuery.fromKueryExpression( + `${FLEET_ENROLLMENT_API_PREFIX}.policy_id: policyId1` + ); + const validationObject = validateFilterKueryNode({ + astFilter, + types: [FLEET_ENROLLMENT_API_PREFIX], + indexMapping: ENROLLMENT_API_KEY_MAPPINGS, + storeValue: true, + }); + expect(validationObject).toEqual([ + { + astPath: 'arguments.0', + error: null, + isSavedObjectAttr: true, + key: 'fleet-enrollment-api-keys.policy_id', + type: 'fleet-enrollment-api-keys', + }, + ]); + }); + }); +}); + +describe('validateKuery validates real kueries', () => { + describe('Agent policies', () => { + it('Search by data_output_id', async () => { + const validationObj = validateKuery( + `${AGENT_POLICY_SAVED_OBJECT_TYPE}.data_output_id: test_id`, + [AGENT_POLICY_SAVED_OBJECT_TYPE], + AGENT_POLICY_MAPPINGS, + true + ); + expect(validationObj?.isValid).toEqual(true); + }); + + it('Search by data_output_id without SO wrapping', async () => { + const validationObj = validateKuery( + `${AGENT_POLICY_SAVED_OBJECT_TYPE}.data_output_id: test_id`, + [AGENT_POLICY_SAVED_OBJECT_TYPE], + AGENT_POLICY_MAPPINGS, + true + ); + expect(validationObj?.isValid).toEqual(true); + }); + + it('Search by name', async () => { + const validationObj = validateKuery( + `${AGENT_POLICY_SAVED_OBJECT_TYPE}.name: test_id`, + [AGENT_POLICY_SAVED_OBJECT_TYPE], + AGENT_POLICY_MAPPINGS, + true + ); + expect(validationObj?.isValid).toEqual(true); + }); + + it('Invalid kuery', async () => { + const validationObj = validateKuery( + 'test%3A', + [AGENT_POLICY_SAVED_OBJECT_TYPE], + AGENT_POLICY_MAPPINGS, + 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` + ); + }); + + it('Kuery with non existent parameter wrapped by SO', async () => { + const validationObj = validateKuery( + `${AGENT_POLICY_SAVED_OBJECT_TYPE}.non_existent_parameter: 'test_id'`, + [AGENT_POLICY_SAVED_OBJECT_TYPE], + AGENT_POLICY_MAPPINGS, + true + ); + expect(validationObj?.isValid).toEqual(false); + expect(validationObj?.error).toContain( + `KQLSyntaxError: This key 'ingest-agent-policies.non_existent_parameter' does NOT exist in ingest-agent-policies saved object index patterns` + ); + }); + + it('Kuery with non existent parameter', async () => { + const validationObj = validateKuery( + `non_existent_parameter: 'test_id'`, + [AGENT_POLICY_SAVED_OBJECT_TYPE], + AGENT_POLICY_MAPPINGS, + true + ); + expect(validationObj?.isValid).toEqual(false); + expect(validationObj?.error).toContain( + `KQLSyntaxError: This type 'non_existent_parameter' is not allowed` + ); + }); + }); + + describe('Agents', () => { + it('Test 1 - search policy id', async () => { + const validationObj = validateKuery( + `${AGENTS_PREFIX}.policy_id: "policy_id"`, + [AGENTS_PREFIX], + AGENT_MAPPINGS, + true + ); + expect(validationObj?.isValid).toEqual(true); + }); + + it('Test 2 - status kuery without SO wrapping', async () => { + const validationObj = validateKuery( + `status:online or (status:updating or status:unenrolling or status:enrolling)`, + [AGENTS_PREFIX], + AGENT_MAPPINGS, + true + ); + expect(validationObj?.isValid).toEqual(true); + }); + + it('Test 3 - 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], + AGENT_MAPPINGS, + true + ); + expect(validationObj?.isValid).toEqual(true); + }); + + it('Test 4 - valid kuery without SO wrapping', async () => { + const validationObj = validateKuery( + `local_metadata.elastic.agent.version : "8.6.0"`, + [AGENTS_PREFIX], + AGENT_MAPPINGS, + true + ); + expect(validationObj?.isValid).toEqual(true); + }); + + it('Search by multiple agent ids', async () => { + const validationObj = validateKuery( + `${AGENTS_PREFIX}.agent.id : (id_1 or id_2)`, + [AGENTS_PREFIX], + AGENT_MAPPINGS, + true + ); + expect(validationObj?.isValid).toEqual(true); + }); + + it('Search by complex query', async () => { + const validationObj = validateKuery( + `${AGENTS_PREFIX}.policy_id: "policyId" and not (_exists_: "${AGENTS_PREFIX}.unenrolled_at") and ${AGENTS_PREFIX}.enrolled_at >= now-10m`, + [AGENTS_PREFIX], + AGENT_MAPPINGS, + true + ); + expect(validationObj?.isValid).toEqual(true); + }); + + it('Search by complex query without SO wrapping', async () => { + const validationObj = validateKuery( + `policy_id: "policyId" and not (_exists_: "unenrolled_at") and enrolled_at >= now-10m`, + [AGENTS_PREFIX], + AGENT_MAPPINGS, + true + ); + expect(validationObj?.isValid).toEqual(true); + }); + + it('Search by tags', async () => { + const validationObj = validateKuery( + `${AGENTS_PREFIX}.tags: (tag1 or tag2 or tag3)`, + [AGENTS_PREFIX], + AGENT_MAPPINGS, + true + ); + expect(validationObj?.isValid).toEqual(true); + }); + + it('Search by hostname keyword and status', async () => { + const validationObj = validateKuery( + `(${AGENTS_PREFIX}.local_metadata.host.hostname.keyword:test) and (${AGENTS_PREFIX}.status:online)`, + [AGENTS_PREFIX], + AGENT_MAPPINGS, + true + ); + expect(validationObj?.isValid).toEqual(true); + }); + + it('Search by deeply nested fields', async () => { + const validationObj = validateKuery( + `${AGENTS_PREFIX}.local_metadata.os.version.keyword: test`, + [AGENTS_PREFIX], + AGENT_MAPPINGS, + true + ); + expect(validationObj?.isValid).toEqual(true); + }); + + it('Search by deeply nested fields in local_metadata', async () => { + const validationObj = validateKuery( + `${AGENTS_PREFIX}.local_metadata.elastic.agent.build.original.keyword: test`, + [AGENTS_PREFIX], + AGENT_MAPPINGS, + true + ); + expect(validationObj?.isValid).toEqual(true); + }); + }); + + describe('Package policies', () => { + it('Search by package name without SO', async () => { + const validationObj = validateKuery( + `package.name:fleet_server`, + [PACKAGE_POLICY_SAVED_OBJECT_TYPE], + PACKAGE_POLICIES_MAPPINGS, + true + ); + expect(validationObj?.isValid).toEqual(true); + }); + + it('Search by package name', async () => { + const validationObj = validateKuery( + `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:fleet_server`, + [PACKAGE_POLICY_SAVED_OBJECT_TYPE], + PACKAGE_POLICIES_MAPPINGS, + true + ); + expect(validationObj?.isValid).toEqual(true); + }); + + it('Search by package name works with attributes if skipNormalization is not passed', async () => { + const validationObj = validateKuery( + `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.attributes.package.name:packageName`, + [PACKAGE_POLICY_SAVED_OBJECT_TYPE], + PACKAGE_POLICIES_MAPPINGS + ); + expect(validationObj?.isValid).toEqual(true); + }); + + it('Search by name and version', async () => { + const validationObj = validateKuery( + `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name: "TestName" AND ${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.version: "8.8.0"`, + [PACKAGE_POLICY_SAVED_OBJECT_TYPE], + PACKAGE_POLICIES_MAPPINGS, + true + ); + expect(validationObj?.isValid).toEqual(true); + }); + + it('Invalid search by nested wrong parameter', async () => { + const validationObj = validateKuery( + `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.is_managed:packageName`, + [PACKAGE_POLICY_SAVED_OBJECT_TYPE], + PACKAGE_POLICIES_MAPPINGS, + true + ); + expect(validationObj?.isValid).toEqual(false); + expect(validationObj?.error).toEqual( + `KQLSyntaxError: This key 'ingest-package-policies.package.is_managed' does NOT exist in ingest-package-policies saved object index patterns` + ); + }); + + it('invalid search by nested wrong parameter - without wrapped SO', async () => { + const validationObj = validateKuery( + `package.is_managed:packageName`, + [PACKAGE_POLICY_SAVED_OBJECT_TYPE], + PACKAGE_POLICIES_MAPPINGS, + true + ); + expect(validationObj?.isValid).toEqual(false); + expect(validationObj?.error).toEqual( + `KQLSyntaxError: This key 'package.is_managed' does NOT exist in ingest-package-policies saved object index patterns` + ); + }); + + it('Invalid search by non existent parameter', async () => { + const validationObj = validateKuery( + `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.non_existent_parameter:packageName`, + [PACKAGE_POLICY_SAVED_OBJECT_TYPE], + PACKAGE_POLICIES_MAPPINGS + ); + expect(validationObj?.isValid).toEqual(false); + expect(validationObj?.error).toEqual( + `KQLSyntaxError: This key 'ingest-package-policies.non_existent_parameter' does NOT exist in ingest-package-policies saved object index patterns` + ); + }); + }); + + describe('Enrollment keys', () => { + it('Search by policy id without SO name', async () => { + const validationObj = validateKuery( + `policy_id: policyId1`, + [FLEET_ENROLLMENT_API_PREFIX], + ENROLLMENT_API_KEY_MAPPINGS, + true + ); + expect(validationObj?.isValid).toEqual(true); + }); + + it('Search by policy id', async () => { + const validationObj = validateKuery( + `${FLEET_ENROLLMENT_API_PREFIX}.policy_id: policyId1`, + [FLEET_ENROLLMENT_API_PREFIX], + ENROLLMENT_API_KEY_MAPPINGS, + true + ); + expect(validationObj?.isValid).toEqual(true); + }); + }); +}); diff --git a/x-pack/plugins/fleet/server/services/agents/action_status.ts b/x-pack/plugins/fleet/server/services/agents/action_status.ts index 2872a1671a8f3f..403ddf02a034dc 100644 --- a/x-pack/plugins/fleet/server/services/agents/action_status.ts +++ b/x-pack/plugins/fleet/server/services/agents/action_status.ts @@ -13,8 +13,8 @@ import type { FleetServerAgentAction, ActionStatus, ActionErrorResult, - ListWithKuery, AgentActionType, + ActionStatusOptions, } from '../../types'; import { AGENT_ACTIONS_INDEX, @@ -29,7 +29,7 @@ import { appContextService } from '..'; */ export async function getActionStatuses( esClient: ElasticsearchClient, - options: ListWithKuery & { errorSize: number } + options: ActionStatusOptions ): Promise { const actions = await _getActions(esClient, options); const cancelledActions = await getCancelledActions(esClient); @@ -218,7 +218,7 @@ export async function getCancelledActions( async function _getActions( esClient: ElasticsearchClient, - options: ListWithKuery + options: ActionStatusOptions ): Promise { const res = await esClient.search({ index: AGENT_ACTIONS_INDEX, diff --git a/x-pack/plugins/fleet/server/services/saved_object.ts b/x-pack/plugins/fleet/server/services/saved_object.ts index 43ec734edceb11..0a7a74be705291 100644 --- a/x-pack/plugins/fleet/server/services/saved_object.ts +++ b/x-pack/plugins/fleet/server/services/saved_object.ts @@ -5,11 +5,6 @@ * 2.0. */ -import type { SavedObjectsClientContract, SavedObjectsFindResponse } from '@kbn/core/server'; - -import { SO_SEARCH_LIMIT } from '../constants'; -import type { ListWithKuery } from '../types'; - /** * Escape a value with double quote to use with saved object search * Example: escapeSearchQueryPhrase('-test"toto') => '"-test\"toto""' @@ -29,53 +24,3 @@ export const normalizeKuery = (savedObjectType: string, kuery: string): string = `${savedObjectType}.attributes.` ); }; - -// Like saved object client `.find()`, but ignores `page` and `perPage` parameters and -// returns *all* matching saved objects by collocating results from all `.find` pages. -// This function actually doesn't offer any additional benefits over `.find()` for now -// due to SO client limitations (see comments below), so is a placeholder for when SO -// client is improved. -export const findAllSOs = async ( - soClient: SavedObjectsClientContract, - options: Omit & { - type: string; - } -): Promise, 'saved_objects' | 'total'>> => { - const { type, sortField, sortOrder, kuery } = options; - let savedObjectResults: SavedObjectsFindResponse['saved_objects'] = []; - - const query = { - type, - sortField, - sortOrder, - filter: kuery, - page: 1, - perPage: SO_SEARCH_LIMIT, - }; - - const { saved_objects: initialSOs, total } = await soClient.find(query); - - savedObjectResults = initialSOs; - - // The saved object client can't actually page through more than the first 10,000 - // results, due to the same `index.max_result_window` constraint. The commented out - // code below is an example of paging through rest of results when the SO client - // offers that kind of support. - // if (total > searchLimit) { - // const remainingPages = Math.ceil((total - searchLimit) / searchLimit); - // for (let currentPage = 2; currentPage <= remainingPages + 1; currentPage++) { - // const { saved_objects: currentPageSavedObjects } = await soClient.find({ - // ...query, - // page: currentPage, - // }); - // if (currentPageSavedObjects.length) { - // savedObjectResults = savedObjectResults.concat(currentPageSavedObjects); - // } - // } - // } - - return { - saved_objects: savedObjectResults, - total, - }; -}; diff --git a/x-pack/plugins/fleet/server/types/index.tsx b/x-pack/plugins/fleet/server/types/index.tsx index d9c7ff9db74afb..5144d48fbd6f83 100644 --- a/x-pack/plugins/fleet/server/types/index.tsx +++ b/x-pack/plugins/fleet/server/types/index.tsx @@ -89,6 +89,7 @@ export type { PackageListItem, PackageList, InstallationInfo, + ActionStatusOptions, } from '../../common/types'; export { ElasticsearchAssetType, KibanaAssetType, KibanaSavedObjectType } from '../../common/types'; export { dataTypes } from '../../common/constants'; diff --git a/x-pack/plugins/fleet/server/types/rest_spec/agent.ts b/x-pack/plugins/fleet/server/types/rest_spec/agent.ts index 2ea1a26117481d..e90fda99cdff90 100644 --- a/x-pack/plugins/fleet/server/types/rest_spec/agent.ts +++ b/x-pack/plugins/fleet/server/types/rest_spec/agent.ts @@ -9,16 +9,27 @@ import { schema } from '@kbn/config-schema'; import moment from 'moment'; import semverIsValid from 'semver/functions/valid'; -import { SO_SEARCH_LIMIT } from '../../constants'; +import { SO_SEARCH_LIMIT, AGENTS_PREFIX, AGENT_MAPPINGS } from '../../constants'; import { NewAgentActionSchema } from '../models'; +import { validateKuery } from '../../routes/utils/filter_utils'; + export const GetAgentsRequestSchema = { query: schema.object( { page: schema.number({ defaultValue: 1 }), perPage: schema.number({ defaultValue: 20 }), - kuery: schema.maybe(schema.string()), + kuery: schema.maybe( + schema.string({ + validate: (value: string) => { + const validationObj = validateKuery(value, [AGENTS_PREFIX], AGENT_MAPPINGS, true); + if (validationObj?.error) { + return validationObj?.error; + } + }, + }) + ), showInactive: schema.boolean({ defaultValue: false }), withMetrics: schema.boolean({ defaultValue: false }), showUpgradeable: schema.boolean({ defaultValue: false }), @@ -206,7 +217,16 @@ export const PostBulkUpdateAgentTagsRequestSchema = { export const GetAgentStatusRequestSchema = { query: schema.object({ policyId: schema.maybe(schema.string()), - kuery: schema.maybe(schema.string()), + kuery: schema.maybe( + schema.string({ + validate: (value: string) => { + const validationObj = validateKuery(value, [AGENTS_PREFIX], AGENT_MAPPINGS, true); + if (validationObj?.error) { + return validationObj?.error; + } + }, + }) + ), }), }; @@ -221,7 +241,6 @@ export const GetActionStatusRequestSchema = { query: schema.object({ page: schema.number({ defaultValue: 0 }), perPage: schema.number({ defaultValue: 20 }), - kuery: schema.maybe(schema.string()), errorSize: schema.number({ defaultValue: 5 }), }), }; diff --git a/x-pack/plugins/fleet/server/types/rest_spec/agent_policy.ts b/x-pack/plugins/fleet/server/types/rest_spec/agent_policy.ts index 25275ed998c583..88cc6df372c119 100644 --- a/x-pack/plugins/fleet/server/types/rest_spec/agent_policy.ts +++ b/x-pack/plugins/fleet/server/types/rest_spec/agent_policy.ts @@ -9,10 +9,34 @@ import { schema } from '@kbn/config-schema'; import { NewAgentPolicySchema } from '../models'; -import { ListWithKuerySchema, BulkRequestBodySchema } from './common'; +import { AGENT_POLICY_SAVED_OBJECT_TYPE, AGENT_POLICY_MAPPINGS } from '../../constants'; + +import { validateKuery } from '../../routes/utils/filter_utils'; + +import { BulkRequestBodySchema } from './common'; export const GetAgentPoliciesRequestSchema = { - query: ListWithKuerySchema.extends({ + query: schema.object({ + page: schema.maybe(schema.number({ defaultValue: 1 })), + perPage: schema.maybe(schema.number({ defaultValue: 20 })), + sortField: schema.maybe(schema.string()), + sortOrder: schema.maybe(schema.oneOf([schema.literal('desc'), schema.literal('asc')])), + showUpgradeable: schema.maybe(schema.boolean()), + kuery: schema.maybe( + schema.string({ + validate: (value: string) => { + const validationObj = validateKuery( + value, + [AGENT_POLICY_SAVED_OBJECT_TYPE], + AGENT_POLICY_MAPPINGS, + true + ); + if (validationObj?.error) { + return validationObj?.error; + } + }, + }) + ), noAgentCount: schema.maybe(schema.boolean()), full: schema.maybe(schema.boolean()), }), diff --git a/x-pack/plugins/fleet/server/types/rest_spec/common.ts b/x-pack/plugins/fleet/server/types/rest_spec/common.ts index 884e5922747b23..0c5f16ff87f90f 100644 --- a/x-pack/plugins/fleet/server/types/rest_spec/common.ts +++ b/x-pack/plugins/fleet/server/types/rest_spec/common.ts @@ -8,9 +8,6 @@ import { schema } from '@kbn/config-schema'; import type { TypeOf } from '@kbn/config-schema'; -/** - * @deprecated - */ export const ListWithKuerySchema = schema.object({ page: schema.maybe(schema.number({ defaultValue: 1 })), perPage: schema.maybe(schema.number({ defaultValue: 20 })), diff --git a/x-pack/plugins/fleet/server/types/rest_spec/enrollment_api_key.ts b/x-pack/plugins/fleet/server/types/rest_spec/enrollment_api_key.ts index 9294736999986c..d7bfbb289620ac 100644 --- a/x-pack/plugins/fleet/server/types/rest_spec/enrollment_api_key.ts +++ b/x-pack/plugins/fleet/server/types/rest_spec/enrollment_api_key.ts @@ -7,11 +7,31 @@ import { schema } from '@kbn/config-schema'; +import { ENROLLMENT_API_KEY_MAPPINGS } from '../../constants'; + +import { FLEET_ENROLLMENT_API_PREFIX } from '../../../common/constants'; + +import { validateKuery } from '../../routes/utils/filter_utils'; + export const GetEnrollmentAPIKeysRequestSchema = { query: schema.object({ page: schema.number({ defaultValue: 1 }), perPage: schema.number({ defaultValue: 20 }), - kuery: schema.maybe(schema.string()), + kuery: schema.maybe( + schema.string({ + validate: (value: string) => { + const validationObj = validateKuery( + value, + [FLEET_ENROLLMENT_API_PREFIX], + ENROLLMENT_API_KEY_MAPPINGS, + true + ); + if (validationObj?.error) { + return validationObj?.error; + } + }, + }) + ), }), }; diff --git a/x-pack/plugins/fleet/server/types/rest_spec/package_policy.ts b/x-pack/plugins/fleet/server/types/rest_spec/package_policy.ts index 1538f380fb7b47..88b4452a5fe7a1 100644 --- a/x-pack/plugins/fleet/server/types/rest_spec/package_policy.ts +++ b/x-pack/plugins/fleet/server/types/rest_spec/package_policy.ts @@ -15,10 +15,34 @@ import { import { inputsFormat } from '../../../common/constants'; -import { ListWithKuerySchema, BulkRequestBodySchema } from './common'; +import { PACKAGE_POLICY_SAVED_OBJECT_TYPE, PACKAGE_POLICIES_MAPPINGS } from '../../constants'; + +import { validateKuery } from '../../routes/utils/filter_utils'; + +import { BulkRequestBodySchema } from './common'; export const GetPackagePoliciesRequestSchema = { - query: ListWithKuerySchema.extends({ + query: schema.object({ + page: schema.maybe(schema.number({ defaultValue: 1 })), + perPage: schema.maybe(schema.number({ defaultValue: 20 })), + sortField: schema.maybe(schema.string()), + sortOrder: schema.maybe(schema.oneOf([schema.literal('desc'), schema.literal('asc')])), + showUpgradeable: schema.maybe(schema.boolean()), + kuery: schema.maybe( + schema.string({ + validate: (value: string) => { + const validationObj = validateKuery( + value, + [PACKAGE_POLICY_SAVED_OBJECT_TYPE], + PACKAGE_POLICIES_MAPPINGS, + true + ); + if (validationObj?.error) { + return validationObj?.error; + } + }, + }) + ), format: schema.maybe( schema.oneOf([schema.literal(inputsFormat.Simplified), schema.literal(inputsFormat.Legacy)]) ), diff --git a/x-pack/plugins/fleet/server/types/rest_spec/tags.ts b/x-pack/plugins/fleet/server/types/rest_spec/tags.ts index 2b454647296ebd..beb646ccec9d54 100644 --- a/x-pack/plugins/fleet/server/types/rest_spec/tags.ts +++ b/x-pack/plugins/fleet/server/types/rest_spec/tags.ts @@ -7,9 +7,22 @@ import { schema } from '@kbn/config-schema'; +import { validateKuery } from '../../routes/utils/filter_utils'; + +import { AGENTS_PREFIX, AGENT_MAPPINGS } from '../../constants'; + export const GetTagsRequestSchema = { query: schema.object({ - kuery: schema.maybe(schema.string()), + kuery: schema.maybe( + schema.string({ + validate: (value: string) => { + const validationObj = validateKuery(value, [AGENTS_PREFIX], AGENT_MAPPINGS, true); + if (validationObj?.error) { + return validationObj?.error; + } + }, + }) + ), showInactive: schema.boolean({ defaultValue: false }), }), }; diff --git a/x-pack/plugins/fleet/tsconfig.json b/x-pack/plugins/fleet/tsconfig.json index 4e8ec7de3fcc7d..82b22c90779b76 100644 --- a/x-pack/plugins/fleet/tsconfig.json +++ b/x-pack/plugins/fleet/tsconfig.json @@ -99,5 +99,6 @@ "@kbn/shared-ux-file-types", "@kbn/core-http-router-server-mocks", "@kbn/core-application-browser", + "@kbn/core-saved-objects-base-server-internal", ] } diff --git a/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts b/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts index e7a6a68f751850..47d3f93fd8ddf5 100644 --- a/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts +++ b/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts @@ -21,6 +21,64 @@ export default function (providerContext: FtrProviderContext) { describe('fleet_agent_policies', () => { skipIfNoDockerRegistry(providerContext); + + describe('GET /api/fleet/agent_policies', () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/fleet/empty_fleet_server'); + await kibanaServer.savedObjects.cleanStandardList(); + }); + setupFleetAndAgents(providerContext); + + it('should get list agent policies', async () => { + await supertest.get(`/api/fleet/agent_policies`).expect(200); + }); + + it('should get a list of agent policies by kuery', async () => { + await supertest + .post(`/api/fleet/agent_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'TEST', + namespace: 'default', + }) + .expect(200); + const { body: responseBody } = await supertest + .get(`/api/fleet/agent_policies?kuery=ingest-agent-policies.name:TEST`) + .set('kbn-xsrf', 'xxxx') + .expect(200); + expect(responseBody.items.length).to.eql(1); + }); + + it('should return 200 even if the passed kuery does not have prefix ingest-agent-policies', async () => { + await supertest + .post(`/api/fleet/agent_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'TEST-1', + namespace: 'default', + }) + .expect(200); + await supertest + .get(`/api/fleet/agent_policies?kuery=name:TEST-1`) + .set('kbn-xsrf', 'xxxx') + .expect(200); + }); + + it('should return 400 if passed kuery is not correct', async () => { + await supertest + .get(`/api/fleet/agent_policies?kuery=ingest-agent-policies.non_existent_parameter:test`) + .set('kbn-xsrf', 'xxxx') + .expect(400); + }); + + it('should return 400 if passed kuery is invalid', async () => { + await supertest + .get(`/api/fleet/agent_policies?kuery='test%3A'`) + .set('kbn-xsrf', 'xxxx') + .expect(400); + }); + }); + describe('POST /api/fleet/agent_policies', () => { let systemPkgVersion: string; before(async () => { diff --git a/x-pack/test/fleet_api_integration/apis/agents/list.ts b/x-pack/test/fleet_api_integration/apis/agents/list.ts index d051cf677437a8..328e4a240e77c5 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/list.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/list.ts @@ -72,15 +72,29 @@ export default function ({ getService }: FtrProviderContext) { expect(apiResponse.total).to.eql(4); }); - it('should return a 400 when given an invalid "kuery" value', async () => { - await supertest.get(`/api/fleet/agents?kuery=.test%3A`).expect(400); + it('should return 200 if the passed kuery is valid', async () => { + await supertest + .get(`/api/fleet/agent_status?kuery=fleet-agents.local_metadata.host.hostname:test`) + .set('kbn-xsrf', 'xxxx') + .expect(200); }); - it('should return a 200 and an empty list when given a "kuery" value with a missing saved object type', async () => { - const { body: apiResponse } = await supertest - .get(`/api/fleet/agents?kuery=m`) // missing saved object type + it('should return 200 also if the passed kuery does not have prefix fleet-agents', async () => { + await supertest + .get(`/api/fleet/agent_status?kuery=local_metadata.host.hostname:test`) + .set('kbn-xsrf', 'xxxx') .expect(200); - expect(apiResponse.total).to.eql(0); + }); + + it('should return a 400 when given an invalid "kuery" value', async () => { + await supertest.get(`/api/fleet/agents?kuery='test%3A'`).expect(400); + }); + + it('should return 400 if passed kuery has non existing parameters', async () => { + await supertest + .get(`/api/fleet/agents?kuery=fleet-agents.non_existent_parameter:healthy`) + .set('kbn-xsrf', 'xxxx') + .expect(400); }); it('should accept a valid "kuery" value', async () => { diff --git a/x-pack/test/fleet_api_integration/apis/agents/status.ts b/x-pack/test/fleet_api_integration/apis/agents/status.ts index 4dbd144493c467..498fbe7c42bcea 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/status.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/status.ts @@ -307,5 +307,37 @@ export default function ({ getService }: FtrProviderContext) { }, }); }); + + it('should get a list of agent policies by kuery', async () => { + await supertest + .get(`/api/fleet/agent_status?kuery=fleet-agents.status:healthy`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'TEST', + namespace: 'default', + }) + .expect(200); + }); + + it('should return 200 also if the kuery does not have prefix fleet-agents', async () => { + await supertest + .get(`/api/fleet/agent_status?kuery=status:unhealthy`) + .set('kbn-xsrf', 'xxxx') + .expect(200); + }); + + it('should return 400 if passed kuery has non existing parameters', async () => { + await supertest + .get(`/api/fleet/agent_status?kuery=fleet-agents.non_existent_parameter:healthy`) + .set('kbn-xsrf', 'xxxx') + .expect(400); + }); + + it('should return 400 if passed kuery is not correct', async () => { + await supertest + .get(`/api/fleet/agent_status?kuery='test%3A'`) + .set('kbn-xsrf', 'xxxx') + .expect(400); + }); }); } diff --git a/x-pack/test/fleet_api_integration/apis/agents/update_agent_tags.ts b/x-pack/test/fleet_api_integration/apis/agents/update_agent_tags.ts index 05c1a1f8ae6c7c..197e4da7429bf4 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/update_agent_tags.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/update_agent_tags.ts @@ -111,7 +111,7 @@ export default function (providerContext: FtrProviderContext) { const verifyActionResult = async () => { const { body } = await supertest - .get(`/api/fleet/agents?kuery=tags:newTag`) + .get(`/api/fleet/agents?kuery=fleet-agents.tags:newTag`) .set('kbn-xsrf', 'xxx'); expect(body.total).to.eql(4); }; @@ -134,7 +134,7 @@ export default function (providerContext: FtrProviderContext) { const verifyActionResult = async () => { const { body } = await supertest - .get(`/api/fleet/agents?kuery=tags:existingTag`) + .get(`/api/fleet/agents?kuery=fleet-agents.tags:existingTag`) .set('kbn-xsrf', 'xxx'); expect(body.total).to.eql(0); }; @@ -142,7 +142,35 @@ export default function (providerContext: FtrProviderContext) { await pollResult(actionId, 2, verifyActionResult); }); - it('should return a 403 if user lacks fleet all permissions', async () => { + it('should return 200 also if the kuery is valid', async () => { + await supertest + .get(`/api/fleet/agents?kuery=tags:fleet-agents.existingTag`) + .set('kbn-xsrf', 'xxxx') + .expect(200); + }); + + it('should return 200 also if the kuery does not have prefix fleet-agents', async () => { + await supertest + .get(`/api/fleet/agents?kuery=tags:existingTag`) + .set('kbn-xsrf', 'xxxx') + .expect(200); + }); + + it('should return 400 if the passed kuery is not correct', async () => { + await supertest + .get(`/api/fleet/agents?kuery=fleet-agents.non_existent_parameter:existingTag`) + .set('kbn-xsrf', 'xxxx') + .expect(400); + }); + + it('should return 400 if the passed kuery is invalid', async () => { + await supertest + .get(`/api/fleet/agents?kuery='test%3A'`) + .set('kbn-xsrf', 'xxxx') + .expect(400); + }); + + it('should return a 403 if user lacks "fleet all" permissions', async () => { await supertestWithoutAuth .post(`/api/fleet/agents/bulk_update_agent_tags`) .auth(testUsers.fleet_no_access.username, testUsers.fleet_no_access.password) diff --git a/x-pack/test/fleet_api_integration/apis/enrollment_api_keys/crud.ts b/x-pack/test/fleet_api_integration/apis/enrollment_api_keys/crud.ts index 703621dc7f9d41..d717c6e285c04a 100644 --- a/x-pack/test/fleet_api_integration/apis/enrollment_api_keys/crud.ts +++ b/x-pack/test/fleet_api_integration/apis/enrollment_api_keys/crud.ts @@ -58,6 +58,36 @@ export default function (providerContext: FtrProviderContext) { .auth(testUsers.integr_all_only.username, testUsers.integr_all_only.password) .expect(403); }); + + it('should return 200 if the passed kuery is correct', async () => { + await supertest + .get(`/api/fleet/enrollment_api_keys?kuery=fleet-enrollment-api-keys.policy_id:policy1`) + .set('kbn-xsrf', 'xxxx') + .expect(200); + }); + + it('should return 200 if the passed kuery does not have prefix fleet-enrollment-api-keys', async () => { + await supertest + .get(`/api/fleet/enrollment_api_keys?kuery=policy_id:policy1`) + .set('kbn-xsrf', 'xxxx') + .expect(200); + }); + + it('should return 400 if the passed kuery is not correct', async () => { + await supertest + .get( + `/api/fleet/enrollment_api_keys?kuery=fleet-enrollment-api-keys.non_existent_parameter:test` + ) + .set('kbn-xsrf', 'xxxx') + .expect(400); + }); + + it('should return 400 if the passed kuery is invalid', async () => { + await supertest + .get(`/api/fleet/enrollment_api_keys?kuery='test%3A'`) + .set('kbn-xsrf', 'xxxx') + .expect(400); + }); }); describe('GET /fleet/enrollment_api_keys/{id}', async () => { diff --git a/x-pack/test/fleet_api_integration/apis/package_policy/get.ts b/x-pack/test/fleet_api_integration/apis/package_policy/get.ts index 06d356c03b37ea..ce0a7a1f219c67 100644 --- a/x-pack/test/fleet_api_integration/apis/package_policy/get.ts +++ b/x-pack/test/fleet_api_integration/apis/package_policy/get.ts @@ -426,5 +426,94 @@ export default function (providerContext: FtrProviderContext) { expect(response.body.items[0].id).to.eql(packagePolicyId); }); }); + + describe('get by kuery', async function () { + let agentPolicyId: string; + let endpointPackagePolicyId: string; + + before(async function () { + if (!server.enabled) { + return; + } + + const { body: agentPolicyResponse } = await supertest + .post(`/api/fleet/agent_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'Test policy', + namespace: 'default', + }); + agentPolicyId = agentPolicyResponse.item.id; + + const { body: endpointPackagePolicyResponse } = await supertest + .post(`/api/fleet/package_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'endpoint-1', + description: '', + namespace: 'default', + policy_id: agentPolicyId, + enabled: true, + inputs: [], + force: true, + package: { + name: 'endpoint', + title: 'Elastic Defend', + version: '8.6.1', + }, + }); + endpointPackagePolicyId = endpointPackagePolicyResponse.item.id; + }); + + after(async function () { + if (!server.enabled) { + return; + } + + await supertest + .post(`/api/fleet/package_policies/delete`) + .set('kbn-xsrf', 'xxxx') + .send({ packagePolicyIds: [endpointPackagePolicyId] }) + .expect(200); + + // uninstall endpoint package + await supertest + .delete(`/api/fleet/epm/packages/endpoint-8.6.1`) + .set('kbn-xsrf', 'xxxx') + .send({ force: true }) + .expect(200); + }); + + it('should return 200 if the passed kuery is correct', async () => { + const { body: packagePolicyResponse } = await supertest + .get(`/api/fleet/package_policies?kuery=ingest-package-policies.package.name:endpoint`) + .set('kbn-xsrf', 'xxxx') + .expect(200); + + expect(packagePolicyResponse.items[0].id).to.eql(endpointPackagePolicyId); + }); + it('should return 400 if the passed kuery does not have prefix ingest-package-policies', async () => { + await supertest + .get(`/api/fleet/package_policies?kuery=package.name:endpoint`) + .set('kbn-xsrf', 'xxxx') + .expect(400); + }); + + it('should return 400 if the passed kuery is not correct', async () => { + await supertest + .get( + `/api/fleet/package_policies?kuery=ingest-package-policies.non_existent_parameter:test` + ) + .set('kbn-xsrf', 'xxxx') + .expect(400); + }); + + it('should return 400 if the passed kuery is invalid', async () => { + await supertest + .get(`/api/fleet/package_policies?kuery='test%3A'`) + .set('kbn-xsrf', 'xxxx') + .expect(400); + }); + }); }); }