mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
Kuery api strict validation (#161064)
## Summary
Require validation for endpoints accepting `kuery` as a parameter in
POST or PUT requests.
**IMPORTANT**: This PR is part of the work needed to prepare the APIs
for Serverless.
### Some context
The initial idea was to completely remove any KQL queries from being
exposed in the endpoints, but after some discussion we came to the
agreement that they can stay but need to be validated, so only allowed
parameters can be sent. A similar approach is being followed by other
teams as well.
Impacted endpoints:
- `GET api/fleet/agents`
- `GET api/fleet/agent_status`
- `GET api/fleet/agent_policies`
- `GET api/fleet/package_policies`
- `GET api/fleet/enrollment_api_keys`
- `GET api/fleet/agent_status`
All these endpoints accept as a parameter `ListWithKuery`. It was
originally being deprecated but it was then decided to keep it and add
validation to the endpoints instead.
The endpoint `api/fleet/agents/action_status` doesn't accept `kuery`
anymore, since it was not being passed internally.
### What's changing
The KQL passed to these endpoints will be accepted in two possible
formats:
```
GET kbn:api/fleet/agents?kuery=local_metadata.agent.version="8.8.0"
GET kbn:api/fleet/agents?kuery=fleet-agents.local_metadata.agent.version="8.8.0"
```
Note that originally only the second format was going to accepted, but
we decided to avoid enforcing it as it would introduce a breaking
change, possibly breaking many customers automations.
### How it works
The code for `ValidateFilterKueryNode` has been adapted from a [similar
function](45a483f496/packages/core/saved-objects/core-saved-objects-api-server-internal/src/lib/search/utils/filter_utils.ts (L102)
)
already used in Kibana core. I added several tests where with some
common queries that are performed in the UI just to be sure that they
would pass validation. Additional queries can be validated by these
tests in the future.
`ValidateFilterKueryNode` needs to have the SO or index and a mapping
with the parameters to validate against. I copied over the mappings for
the necessary entities; if in the future we intend to expose a new
mapping parameter in the endpoints, it will be necessary to add it there
as well, or the validation will fail.
### UI
I also checked that the UI doesn't fail when using the KQL search boxes
for Agents, Agent policies and Enrollment keys and made sure that they
expose the same values present in the mappings.
### Testing
From dev tools, you can test the affected endpoints by entering the
following queries:
```
# agents
GET kbn:api/fleet/agents?kuery=fleet-agents.active:true
GET kbn:api/fleet/agents?kuery=active:true
# tags
GET kbn:api/fleet/agents/tags?kuery=fleet-agents.tags:tag1
GET kbn:api/fleet/agents/tags?kuery=tags:tag1
# agent status
GET kbn:/api/fleet/agent_status?kuery=fleet-agents.policy_id:fleet-server-policy
GET kbn:/api/fleet/agent_status?kuery=policy_id:fleet-server-policy
# package policies
GET kbn:/api/fleet/package_policies?kuery=ingest-package-policies.package.name:fleet_server
# agent policies
GET kbn:/api/fleet/agent_policies?kuery=ingest-agent-policies.name:"Fleet Server Policy"
GET kbn:/api/fleet/agent_policies?kuery=name:"Fleet Server Policy"
# enrollment keys
GET kbn:/api/fleet/enrollment_api_keys?kuery=fleet-enrollment-api-keys.policy_id:policy1
GET kbn:/api/fleet/enrollment_api_keys?kuery=policy1
```
These should all pass validation; modifying the parameters (for instance
with non existing ones) should fail validation
### Checklist
- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [ ] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
---------
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Patryk Kopycinski <contact@patrykkopycinski.com>
This commit is contained in:
parent
e298d2c51f
commit
82a177603d
28 changed files with 2179 additions and 109 deletions
|
@ -58,6 +58,7 @@ export {
|
|||
ENDPOINT_PRIVILEGES,
|
||||
// dashboards ids
|
||||
DASHBOARD_LOCATORS_IDS,
|
||||
FLEET_ENROLLMENT_API_PREFIX,
|
||||
} from './constants';
|
||||
export {
|
||||
// Route services
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -415,3 +415,9 @@ export interface FleetServerAgentAction {
|
|||
|
||||
[k: string]: unknown;
|
||||
}
|
||||
|
||||
export interface ActionStatusOptions {
|
||||
errorSize: number;
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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' },
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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';
|
||||
|
|
332
x-pack/plugins/fleet/server/constants/mappings.ts
Normal file
332
x-pack/plugins/fleet/server/constants/mappings.ts
Normal file
|
@ -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;
|
444
x-pack/plugins/fleet/server/routes/utils/filter_utils.test.ts
Normal file
444
x-pack/plugins/fleet/server/routes/utils/filter_utils.test.ts
Normal file
|
@ -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',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
255
x-pack/plugins/fleet/server/routes/utils/filter_utils.ts
Normal file
255
x-pack/plugins/fleet/server/routes/utils/filter_utils.ts
Normal file
|
@ -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;
|
||||
}
|
||||
};
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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<ActionStatus[]> {
|
||||
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<ActionStatus[]> {
|
||||
const res = await esClient.search<FleetServerAgentAction>({
|
||||
index: AGENT_ACTIONS_INDEX,
|
||||
|
|
|
@ -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 <T = unknown>(
|
||||
soClient: SavedObjectsClientContract,
|
||||
options: Omit<ListWithKuery, 'page' | 'perPage'> & {
|
||||
type: string;
|
||||
}
|
||||
): Promise<Pick<SavedObjectsFindResponse<T>, 'saved_objects' | 'total'>> => {
|
||||
const { type, sortField, sortOrder, kuery } = options;
|
||||
let savedObjectResults: SavedObjectsFindResponse<T>['saved_objects'] = [];
|
||||
|
||||
const query = {
|
||||
type,
|
||||
sortField,
|
||||
sortOrder,
|
||||
filter: kuery,
|
||||
page: 1,
|
||||
perPage: SO_SEARCH_LIMIT,
|
||||
};
|
||||
|
||||
const { saved_objects: initialSOs, total } = await soClient.find<T>(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<T>({
|
||||
// ...query,
|
||||
// page: currentPage,
|
||||
// });
|
||||
// if (currentPageSavedObjects.length) {
|
||||
// savedObjectResults = savedObjectResults.concat(currentPageSavedObjects);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
return {
|
||||
saved_objects: savedObjectResults,
|
||||
total,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 }),
|
||||
}),
|
||||
};
|
||||
|
|
|
@ -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()),
|
||||
}),
|
||||
|
|
|
@ -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 })),
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
},
|
||||
})
|
||||
),
|
||||
}),
|
||||
};
|
||||
|
||||
|
|
|
@ -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)])
|
||||
),
|
||||
|
|
|
@ -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 }),
|
||||
}),
|
||||
};
|
||||
|
|
|
@ -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",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue