mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[E&C][ES Query] adds runtime mappings and fields support to the ES Query ruletype (#138427)
This PR adds Runtime Fields support to the ES Query Rule Type when using the DSL Query mode.
This commit is contained in:
parent
ba8a267050
commit
502dc0a4d0
9 changed files with 738 additions and 100 deletions
|
@ -1,5 +1,5 @@
|
|||
[role="xpack"]
|
||||
[[rule-type-es-query]]
|
||||
[role="xpack"]
|
||||
=== {es} query
|
||||
|
||||
The {es} query rule type runs a user-configured query, compares the number of
|
||||
|
@ -27,8 +27,8 @@ the *time window*.
|
|||
Size:: Specifies the number of documents to pass to the configured actions when
|
||||
the threshold condition is met.
|
||||
{es} query:: Specifies the ES DSL query. The number of documents that
|
||||
match this query is evaluated against the threshold condition. Only the `query`
|
||||
field is used, other DSL fields are not considered.
|
||||
match this query is evaluated against the threshold condition. Only the `query`, `fields` and `runtime_mappings`
|
||||
fields are used, other DSL fields are not considered.
|
||||
Threshold:: Defines a threshold value and a comparison operator (`is above`,
|
||||
`is above or equals`, `is below`, `is below or equals`, or `is between`). The
|
||||
number of documents that match the specified query is compared to this
|
||||
|
@ -74,7 +74,38 @@ over these hits to get values from the ES documents into your actions.
|
|||
+
|
||||
[role="screenshot"]
|
||||
image::images/rule-types-es-query-example-action-variable.png[Iterate over hits using Mustache template syntax]
|
||||
|
||||
+
|
||||
The documents returned by `context.hits` include the {ref}/mapping-source-field.html[`_source`] field.
|
||||
If the {es} query search API's {ref}/search-fields.html#search-fields-param[`fields`] parameter is used, documents will also return the `fields` field,
|
||||
which can be used to access any runtime fields defined by the {ref}/runtime-search-request.html[`runtime_mappings`] parameter as the following example shows:
|
||||
+
|
||||
--
|
||||
[source]
|
||||
--------------------------------------------------
|
||||
{{#context.hits}}
|
||||
timestamp: {{_source.@timestamp}}
|
||||
day of the week: {{fields.day_of_week}} <1>
|
||||
{{/context.hits}}
|
||||
--------------------------------------------------
|
||||
// NOTCONSOLE
|
||||
<1> The `fields` parameter here is used to access the `day_of_week` runtime field.
|
||||
--
|
||||
+
|
||||
As the {ref}/search-fields.html#search-fields-response[`fields`] response always returns an array of values for each field,
|
||||
the https://mustache.github.io/[Mustache] template array syntax is used to iterate over these values in your actions as the following example shows:
|
||||
+
|
||||
--
|
||||
[source]
|
||||
--------------------------------------------------
|
||||
{{#context.hits}}
|
||||
Labels:
|
||||
{{#fields.labels}}
|
||||
- {{.}}
|
||||
{{/fields.labels}}
|
||||
{{/context.hits}}
|
||||
--------------------------------------------------
|
||||
// NOTCONSOLE
|
||||
--
|
||||
|
||||
[float]
|
||||
==== Test your query
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { isRuleType, ruleTypeMappings } from '@kbn/securitysolution-rules';
|
||||
import { isString } from 'lodash/fp';
|
||||
import { omit } from 'lodash';
|
||||
import { omit, pick } from 'lodash';
|
||||
import moment from 'moment-timezone';
|
||||
import { gte } from 'semver';
|
||||
import {
|
||||
|
@ -40,7 +40,8 @@ interface AlertLogMeta extends LogMeta {
|
|||
}
|
||||
|
||||
type AlertMigration = (
|
||||
doc: SavedObjectUnsanitizedDoc<RawRule>
|
||||
doc: SavedObjectUnsanitizedDoc<RawRule>,
|
||||
context: SavedObjectMigrationContext
|
||||
) => SavedObjectUnsanitizedDoc<RawRule>;
|
||||
|
||||
function createEsoMigration(
|
||||
|
@ -169,6 +170,12 @@ export function getMigrations(
|
|||
pipeMigrations(addSearchType, removeInternalTags, convertSnoozes)
|
||||
);
|
||||
|
||||
const migrationRules850 = createEsoMigration(
|
||||
encryptedSavedObjects,
|
||||
(doc): doc is SavedObjectUnsanitizedDoc<RawRule> => isEsQueryRuleType(doc),
|
||||
pipeMigrations(stripOutRuntimeFieldsInOldESQuery)
|
||||
);
|
||||
|
||||
return mergeSavedObjectMigrationMaps(
|
||||
{
|
||||
'7.10.0': executeMigrationWithErrorHandling(migrationWhenRBACWasIntroduced, '7.10.0'),
|
||||
|
@ -182,6 +189,7 @@ export function getMigrations(
|
|||
'8.0.1': executeMigrationWithErrorHandling(migrationRules801, '8.0.1'),
|
||||
'8.2.0': executeMigrationWithErrorHandling(migrationRules820, '8.2.0'),
|
||||
'8.3.0': executeMigrationWithErrorHandling(migrationRules830, '8.3.0'),
|
||||
'8.5.0': executeMigrationWithErrorHandling(migrationRules850, '8.5.0'),
|
||||
},
|
||||
getSearchSourceMigrations(encryptedSavedObjects, searchSourceMigrations)
|
||||
);
|
||||
|
@ -750,6 +758,47 @@ function addSecuritySolutionAADRuleTypeTags(
|
|||
: doc;
|
||||
}
|
||||
|
||||
function stripOutRuntimeFieldsInOldESQuery(
|
||||
doc: SavedObjectUnsanitizedDoc<RawRule>,
|
||||
context: SavedObjectMigrationContext
|
||||
): SavedObjectUnsanitizedDoc<RawRule> {
|
||||
const isESDSLrule =
|
||||
isEsQueryRuleType(doc) && !isSerializedSearchSource(doc.attributes.params.searchConfiguration);
|
||||
|
||||
if (isESDSLrule) {
|
||||
try {
|
||||
const parsedQuery = JSON.parse(doc.attributes.params.esQuery as string);
|
||||
// parsing and restringifying will cause us to lose the formatting so we only do so if this rule has
|
||||
// fields other than `query` which is the only valid field at this stage
|
||||
const hasFieldsOtherThanQuery = Object.keys(parsedQuery).some((key) => key !== 'query');
|
||||
return hasFieldsOtherThanQuery
|
||||
? {
|
||||
...doc,
|
||||
attributes: {
|
||||
...doc.attributes,
|
||||
params: {
|
||||
...doc.attributes.params,
|
||||
esQuery: JSON.stringify(pick(parsedQuery, 'query'), null, 4),
|
||||
},
|
||||
},
|
||||
}
|
||||
: doc;
|
||||
} catch (err) {
|
||||
// Instead of failing the upgrade when an unparsable rule is encountered, we log that the rule caouldn't be migrated and
|
||||
// as a result legacy parameters might cause the rule to behave differently if it is, in fact, still running at all
|
||||
context.log.error<AlertLogMeta>(
|
||||
`unable to migrate and remove legacy runtime fields in rule ${doc.id} due to invalid query: "${doc.attributes.params.esQuery}" - query must be JSON`,
|
||||
{
|
||||
migrations: {
|
||||
alertDocument: doc,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
return doc;
|
||||
}
|
||||
|
||||
function addThreatIndicatorPathToThreatMatchRules(
|
||||
doc: SavedObjectUnsanitizedDoc<RawRule>
|
||||
): SavedObjectUnsanitizedDoc<RawRule> {
|
||||
|
@ -957,8 +1006,8 @@ function removeInternalTags(
|
|||
}
|
||||
|
||||
function pipeMigrations(...migrations: AlertMigration[]): AlertMigration {
|
||||
return (doc: SavedObjectUnsanitizedDoc<RawRule>) =>
|
||||
migrations.reduce((migratedDoc, nextMigration) => nextMigration(migratedDoc), doc);
|
||||
return (doc: SavedObjectUnsanitizedDoc<RawRule>, context: SavedObjectMigrationContext) =>
|
||||
migrations.reduce((migratedDoc, nextMigration) => nextMigration(migratedDoc, context), doc);
|
||||
}
|
||||
|
||||
function mapSearchSourceMigrationFunc(
|
||||
|
|
|
@ -21,6 +21,8 @@ export interface BuildSortedEventsQuery extends BuildSortedEventsQueryOpts {
|
|||
sortOrder?: 'asc' | 'desc';
|
||||
searchAfterSortId: string | number | undefined;
|
||||
timeField: string;
|
||||
fields?: string[];
|
||||
runtime_mappings?: unknown;
|
||||
}
|
||||
|
||||
export const buildSortedEventsQuery = ({
|
||||
|
@ -35,6 +37,9 @@ export const buildSortedEventsQuery = ({
|
|||
timeField,
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
track_total_hits,
|
||||
fields,
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
runtime_mappings,
|
||||
}: BuildSortedEventsQuery): ESSearchRequest => {
|
||||
const sortField = timeField;
|
||||
const docFields = [timeField].map((tstamp) => ({
|
||||
|
@ -82,6 +87,8 @@ export const buildSortedEventsQuery = ({
|
|||
},
|
||||
],
|
||||
},
|
||||
...(runtime_mappings ? { runtime_mappings } : {}),
|
||||
...(fields ? { fields } : {}),
|
||||
};
|
||||
|
||||
if (searchAfterSortId) {
|
||||
|
|
|
@ -26,13 +26,18 @@ export async function fetchEsQuery(
|
|||
) {
|
||||
const { scopedClusterClient, logger } = services;
|
||||
const esClient = scopedClusterClient.asCurrentUser;
|
||||
const { parsedQuery, dateStart, dateEnd } = getSearchParams(params);
|
||||
const {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
parsedQuery: { query, fields, runtime_mappings },
|
||||
dateStart,
|
||||
dateEnd,
|
||||
} = getSearchParams(params);
|
||||
|
||||
const filter = timestamp
|
||||
? {
|
||||
bool: {
|
||||
filter: [
|
||||
parsedQuery.query,
|
||||
query,
|
||||
{
|
||||
bool: {
|
||||
must_not: [
|
||||
|
@ -56,9 +61,9 @@ export async function fetchEsQuery(
|
|||
],
|
||||
},
|
||||
}
|
||||
: parsedQuery.query;
|
||||
: query;
|
||||
|
||||
const query = buildSortedEventsQuery({
|
||||
const sortedQuery = buildSortedEventsQuery({
|
||||
index: params.index,
|
||||
from: dateStart,
|
||||
to: dateEnd,
|
||||
|
@ -68,11 +73,15 @@ export async function fetchEsQuery(
|
|||
searchAfterSortId: undefined,
|
||||
timeField: params.timeField,
|
||||
track_total_hits: true,
|
||||
fields,
|
||||
runtime_mappings,
|
||||
});
|
||||
|
||||
logger.debug(`es query rule ${ES_QUERY_ID}:${ruleId} "${name}" query - ${JSON.stringify(query)}`);
|
||||
logger.debug(
|
||||
`es query rule ${ES_QUERY_ID}:${ruleId} "${name}" query - ${JSON.stringify(sortedQuery)}`
|
||||
);
|
||||
|
||||
const { body: searchResult } = await esClient.search(query, { meta: true });
|
||||
const { body: searchResult } = await esClient.search(sortedQuery, { meta: true });
|
||||
|
||||
logger.debug(
|
||||
` es query rule ${ES_QUERY_ID}:${ruleId} "${name}" result - ${JSON.stringify(searchResult)}`
|
||||
|
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* 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 { FtrProviderContext } from '../../../../../common/ftr_provider_context';
|
||||
import { Spaces } from '../../../../scenarios';
|
||||
import {
|
||||
ESTestIndexTool,
|
||||
ES_TEST_INDEX_NAME,
|
||||
getUrlPrefix,
|
||||
ObjectRemover,
|
||||
} from '../../../../../common/lib';
|
||||
import { createEsDocuments } from '../lib/create_test_data';
|
||||
|
||||
export const RULE_TYPE_ID = '.es-query';
|
||||
export const CONNECTOR_TYPE_ID = '.index';
|
||||
export const ES_TEST_INDEX_SOURCE = 'builtin-rule:es-query';
|
||||
export const ES_TEST_INDEX_REFERENCE = '-na-';
|
||||
export const ES_TEST_OUTPUT_INDEX_NAME = `${ES_TEST_INDEX_NAME}-output`;
|
||||
export const ES_TEST_DATA_STREAM_NAME = 'test-data-stream';
|
||||
|
||||
export const RULE_INTERVALS_TO_WRITE = 5;
|
||||
export const RULE_INTERVAL_SECONDS = 4;
|
||||
export const RULE_INTERVAL_MILLIS = RULE_INTERVAL_SECONDS * 1000;
|
||||
export const ES_GROUPS_TO_WRITE = 3;
|
||||
|
||||
export async function createConnector(
|
||||
supertest: any,
|
||||
objectRemover: ObjectRemover,
|
||||
index: string
|
||||
): Promise<string> {
|
||||
const { body: createdConnector } = await supertest
|
||||
.post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'index action for es query FT',
|
||||
connector_type_id: CONNECTOR_TYPE_ID,
|
||||
config: {
|
||||
index,
|
||||
},
|
||||
secrets: {},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const connectorId = createdConnector.id;
|
||||
objectRemover.add(Spaces.space1.id, connectorId, 'connector', 'actions');
|
||||
|
||||
return connectorId;
|
||||
}
|
||||
|
||||
export interface CreateRuleParams {
|
||||
name: string;
|
||||
size: number;
|
||||
thresholdComparator: string;
|
||||
threshold: number[];
|
||||
timeWindowSize?: number;
|
||||
esQuery?: string;
|
||||
timeField?: string;
|
||||
searchConfiguration?: unknown;
|
||||
searchType?: 'searchSource';
|
||||
notifyWhen?: string;
|
||||
indexName?: string;
|
||||
}
|
||||
|
||||
export function getRuleServices(getService: FtrProviderContext['getService']) {
|
||||
const retry = getService('retry');
|
||||
const es = getService('es');
|
||||
const esTestIndexTool = new ESTestIndexTool(es, retry);
|
||||
const esTestIndexToolOutput = new ESTestIndexTool(es, retry, ES_TEST_OUTPUT_INDEX_NAME);
|
||||
const esTestIndexToolDataStream = new ESTestIndexTool(es, retry, ES_TEST_DATA_STREAM_NAME);
|
||||
|
||||
async function createEsDocumentsInGroups(
|
||||
groups: number,
|
||||
endDate: string,
|
||||
indexTool: ESTestIndexTool = esTestIndexTool,
|
||||
indexName: string = ES_TEST_INDEX_NAME
|
||||
) {
|
||||
await createEsDocuments(
|
||||
es,
|
||||
indexTool,
|
||||
endDate,
|
||||
RULE_INTERVALS_TO_WRITE,
|
||||
RULE_INTERVAL_MILLIS,
|
||||
groups,
|
||||
indexName
|
||||
);
|
||||
}
|
||||
|
||||
async function waitForDocs(count: number): Promise<any[]> {
|
||||
return await esTestIndexToolOutput.waitForDocs(
|
||||
ES_TEST_INDEX_SOURCE,
|
||||
ES_TEST_INDEX_REFERENCE,
|
||||
count
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
retry,
|
||||
es,
|
||||
esTestIndexTool,
|
||||
esTestIndexToolOutput,
|
||||
esTestIndexToolDataStream,
|
||||
createEsDocumentsInGroups,
|
||||
waitForDocs,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,279 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
|
||||
import { Spaces } from '../../../../scenarios';
|
||||
import { FtrProviderContext } from '../../../../../common/ftr_provider_context';
|
||||
import { ES_TEST_INDEX_NAME, getUrlPrefix, ObjectRemover } from '../../../../../common/lib';
|
||||
import { createDataStream, deleteDataStream } from '../lib/create_test_data';
|
||||
import {
|
||||
createConnector,
|
||||
CreateRuleParams,
|
||||
ES_GROUPS_TO_WRITE,
|
||||
ES_TEST_DATA_STREAM_NAME,
|
||||
ES_TEST_INDEX_REFERENCE,
|
||||
ES_TEST_INDEX_SOURCE,
|
||||
ES_TEST_OUTPUT_INDEX_NAME,
|
||||
getRuleServices,
|
||||
RULE_INTERVALS_TO_WRITE,
|
||||
RULE_INTERVAL_MILLIS,
|
||||
RULE_INTERVAL_SECONDS,
|
||||
RULE_TYPE_ID,
|
||||
} from './common';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function ruleTests({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const { es, esTestIndexTool, esTestIndexToolOutput, createEsDocumentsInGroups, waitForDocs } =
|
||||
getRuleServices(getService);
|
||||
|
||||
describe('rule', async () => {
|
||||
let endDate: string;
|
||||
let connectorId: string;
|
||||
const objectRemover = new ObjectRemover(supertest);
|
||||
|
||||
beforeEach(async () => {
|
||||
await esTestIndexTool.destroy();
|
||||
await esTestIndexTool.setup();
|
||||
|
||||
await esTestIndexToolOutput.destroy();
|
||||
await esTestIndexToolOutput.setup();
|
||||
|
||||
connectorId = await createConnector(supertest, objectRemover, ES_TEST_OUTPUT_INDEX_NAME);
|
||||
|
||||
// write documents in the future, figure out the end date
|
||||
const endDateMillis = Date.now() + (RULE_INTERVALS_TO_WRITE - 1) * RULE_INTERVAL_MILLIS;
|
||||
endDate = new Date(endDateMillis).toISOString();
|
||||
|
||||
await createDataStream(es, ES_TEST_DATA_STREAM_NAME);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await objectRemover.removeAll();
|
||||
await esTestIndexTool.destroy();
|
||||
await esTestIndexToolOutput.destroy();
|
||||
await deleteDataStream(es, ES_TEST_DATA_STREAM_NAME);
|
||||
});
|
||||
|
||||
it(`runs correctly: runtime fields for esQuery search type`, async () => {
|
||||
// write documents from now to the future end date in groups
|
||||
await createEsDocumentsInGroups(ES_GROUPS_TO_WRITE, endDate);
|
||||
await createRule({
|
||||
name: 'always fire',
|
||||
esQuery: `
|
||||
{
|
||||
"runtime_mappings": {
|
||||
"testedValueSquared": {
|
||||
"type": "long",
|
||||
"script": {
|
||||
"source": "emit(doc['testedValue'].value * doc['testedValue'].value);"
|
||||
}
|
||||
},
|
||||
"evenOrOdd": {
|
||||
"type": "keyword",
|
||||
"script": {
|
||||
"source": "emit(doc['testedValue'].value % 2 == 0 ? 'even' : 'odd');"
|
||||
}
|
||||
}
|
||||
},
|
||||
"fields": ["testedValueSquared", "evenOrOdd"],
|
||||
"query": {
|
||||
"match_all": { }
|
||||
}
|
||||
}`.replace(`"`, `\"`),
|
||||
size: 100,
|
||||
thresholdComparator: '>',
|
||||
threshold: [-1],
|
||||
});
|
||||
|
||||
const docs = await waitForDocs(2);
|
||||
for (let i = 0; i < docs.length; i++) {
|
||||
const doc = docs[i];
|
||||
const { name, title } = doc._source.params;
|
||||
expect(name).to.be('always fire');
|
||||
expect(title).to.be(`rule 'always fire' matched query`);
|
||||
|
||||
const hits = JSON.parse(doc._source.hits);
|
||||
expect(hits).not.to.be.empty();
|
||||
hits.forEach((hit: any) => {
|
||||
expect(hit.fields).not.to.be.empty();
|
||||
expect(hit.fields.testedValueSquared).not.to.be.empty();
|
||||
// fields returns as an array of values
|
||||
hit.fields.testedValueSquared.forEach((testedValueSquared: number) => {
|
||||
expect(hit._source.testedValue * hit._source.testedValue).to.be(testedValueSquared);
|
||||
});
|
||||
hit.fields.evenOrOdd.forEach((evenOrOdd: string) => {
|
||||
expect(hit._source.testedValue % 2 === 0 ? 'even' : 'odd').to.be(evenOrOdd);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it(`runs correctly: fetches wildcard fields in esQuery search type`, async () => {
|
||||
// write documents from now to the future end date in groups
|
||||
await createEsDocumentsInGroups(ES_GROUPS_TO_WRITE, endDate);
|
||||
await createRule({
|
||||
name: 'always fire',
|
||||
esQuery: `
|
||||
{
|
||||
"fields": ["*"],
|
||||
"query": {
|
||||
"match_all": { }
|
||||
}
|
||||
}`.replace(`"`, `\"`),
|
||||
size: 100,
|
||||
thresholdComparator: '>',
|
||||
threshold: [-1],
|
||||
});
|
||||
|
||||
const docs = await waitForDocs(2);
|
||||
for (let i = 0; i < docs.length; i++) {
|
||||
const doc = docs[i];
|
||||
const { name, title } = doc._source.params;
|
||||
expect(name).to.be('always fire');
|
||||
expect(title).to.be(`rule 'always fire' matched query`);
|
||||
|
||||
const hits = JSON.parse(doc._source.hits);
|
||||
expect(hits).not.to.be.empty();
|
||||
hits.forEach((hit: any) => {
|
||||
expect(hit.fields).not.to.be.empty();
|
||||
expect(Object.keys(hit.fields).sort()).to.eql(Object.keys(hit._source).sort());
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it(`runs correctly: fetches field formatting in esQuery search type`, async () => {
|
||||
const reIsNumeric = /^\d+$/;
|
||||
|
||||
// write documents from now to the future end date in groups
|
||||
await createEsDocumentsInGroups(ES_GROUPS_TO_WRITE, endDate);
|
||||
await createRule({
|
||||
name: 'always fire',
|
||||
esQuery: `
|
||||
{
|
||||
"fields": [
|
||||
{
|
||||
"field": "@timestamp",
|
||||
"format": "epoch_millis"
|
||||
}
|
||||
],
|
||||
"query": {
|
||||
"match_all": { }
|
||||
}
|
||||
}`.replace(`"`, `\"`),
|
||||
size: 100,
|
||||
thresholdComparator: '>',
|
||||
threshold: [-1],
|
||||
});
|
||||
|
||||
const docs = await waitForDocs(2);
|
||||
for (let i = 0; i < docs.length; i++) {
|
||||
const doc = docs[i];
|
||||
const { name, title } = doc._source.params;
|
||||
expect(name).to.be('always fire');
|
||||
expect(title).to.be(`rule 'always fire' matched query`);
|
||||
|
||||
const hits = JSON.parse(doc._source.hits);
|
||||
expect(hits).not.to.be.empty();
|
||||
|
||||
hits.forEach((hit: any) => {
|
||||
expect(hit.fields).not.to.be.empty();
|
||||
hit.fields['@timestamp'].forEach((timestamp: string) => {
|
||||
expect(reIsNumeric.test(timestamp)).to.be(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
async function createRule(params: CreateRuleParams): Promise<string> {
|
||||
const action = {
|
||||
id: connectorId,
|
||||
group: 'query matched',
|
||||
params: {
|
||||
documents: [
|
||||
{
|
||||
source: ES_TEST_INDEX_SOURCE,
|
||||
reference: ES_TEST_INDEX_REFERENCE,
|
||||
params: {
|
||||
name: '{{{rule.name}}}',
|
||||
value: '{{{context.value}}}',
|
||||
title: '{{{context.title}}}',
|
||||
message: '{{{context.message}}}',
|
||||
},
|
||||
// wrap in brackets
|
||||
hits: '[{{context.hits}}]',
|
||||
date: '{{{context.date}}}',
|
||||
previousTimestamp: '{{{state.latestTimestamp}}}',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const recoveryAction = {
|
||||
id: connectorId,
|
||||
group: 'recovered',
|
||||
params: {
|
||||
documents: [
|
||||
{
|
||||
source: ES_TEST_INDEX_SOURCE,
|
||||
reference: ES_TEST_INDEX_REFERENCE,
|
||||
params: {
|
||||
name: '{{{rule.name}}}',
|
||||
value: '{{{context.value}}}',
|
||||
title: '{{{context.title}}}',
|
||||
message: '{{{context.message}}}',
|
||||
},
|
||||
// wrap in brackets
|
||||
hits: '[{{context.hits}}]',
|
||||
date: '{{{context.date}}}',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const ruleParams =
|
||||
params.searchType === 'searchSource'
|
||||
? {
|
||||
searchConfiguration: params.searchConfiguration,
|
||||
}
|
||||
: {
|
||||
index: [params.indexName || ES_TEST_INDEX_NAME],
|
||||
timeField: params.timeField || 'date',
|
||||
esQuery: params.esQuery,
|
||||
};
|
||||
|
||||
const { body: createdRule } = await supertest
|
||||
.post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: params.name,
|
||||
consumer: 'alerts',
|
||||
enabled: true,
|
||||
rule_type_id: RULE_TYPE_ID,
|
||||
schedule: { interval: `${RULE_INTERVAL_SECONDS}s` },
|
||||
actions: [action, recoveryAction],
|
||||
notify_when: params.notifyWhen || 'onActiveAlert',
|
||||
params: {
|
||||
size: params.size,
|
||||
timeWindowSize: params.timeWindowSize || RULE_INTERVAL_SECONDS * 5,
|
||||
timeWindowUnit: 's',
|
||||
thresholdComparator: params.thresholdComparator,
|
||||
threshold: params.threshold,
|
||||
searchType: params.searchType,
|
||||
...ruleParams,
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const ruleId = createdRule.id;
|
||||
objectRemover.add(Spaces.space1.id, ruleId, 'rule', 'alerting');
|
||||
|
||||
return ruleId;
|
||||
}
|
||||
});
|
||||
}
|
|
@ -9,35 +9,35 @@ import expect from '@kbn/expect';
|
|||
|
||||
import { Spaces } from '../../../../scenarios';
|
||||
import { FtrProviderContext } from '../../../../../common/ftr_provider_context';
|
||||
import { ES_TEST_INDEX_NAME, getUrlPrefix, ObjectRemover } from '../../../../../common/lib';
|
||||
import {
|
||||
ESTestIndexTool,
|
||||
ES_TEST_INDEX_NAME,
|
||||
getUrlPrefix,
|
||||
ObjectRemover,
|
||||
} from '../../../../../common/lib';
|
||||
import { createEsDocuments, createDataStream, deleteDataStream } from '../lib/create_test_data';
|
||||
|
||||
const RULE_TYPE_ID = '.es-query';
|
||||
const CONNECTOR_TYPE_ID = '.index';
|
||||
const ES_TEST_INDEX_SOURCE = 'builtin-rule:es-query';
|
||||
const ES_TEST_INDEX_REFERENCE = '-na-';
|
||||
const ES_TEST_OUTPUT_INDEX_NAME = `${ES_TEST_INDEX_NAME}-output`;
|
||||
const ES_TEST_DATA_STREAM_NAME = 'test-data-stream';
|
||||
|
||||
const RULE_INTERVALS_TO_WRITE = 5;
|
||||
const RULE_INTERVAL_SECONDS = 4;
|
||||
const RULE_INTERVAL_MILLIS = RULE_INTERVAL_SECONDS * 1000;
|
||||
const ES_GROUPS_TO_WRITE = 3;
|
||||
createConnector,
|
||||
CreateRuleParams,
|
||||
ES_GROUPS_TO_WRITE,
|
||||
ES_TEST_DATA_STREAM_NAME,
|
||||
ES_TEST_INDEX_REFERENCE,
|
||||
ES_TEST_INDEX_SOURCE,
|
||||
ES_TEST_OUTPUT_INDEX_NAME,
|
||||
getRuleServices,
|
||||
RULE_INTERVALS_TO_WRITE,
|
||||
RULE_INTERVAL_MILLIS,
|
||||
RULE_INTERVAL_SECONDS,
|
||||
RULE_TYPE_ID,
|
||||
} from './common';
|
||||
import { createDataStream, deleteDataStream } from '../lib/create_test_data';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function ruleTests({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const retry = getService('retry');
|
||||
const indexPatterns = getService('indexPatterns');
|
||||
const es = getService('es');
|
||||
const esTestIndexTool = new ESTestIndexTool(es, retry);
|
||||
const esTestIndexToolOutput = new ESTestIndexTool(es, retry, ES_TEST_OUTPUT_INDEX_NAME);
|
||||
const esTestIndexToolDataStream = new ESTestIndexTool(es, retry, ES_TEST_DATA_STREAM_NAME);
|
||||
const {
|
||||
es,
|
||||
esTestIndexTool,
|
||||
esTestIndexToolOutput,
|
||||
esTestIndexToolDataStream,
|
||||
createEsDocumentsInGroups,
|
||||
waitForDocs,
|
||||
} = getRuleServices(getService);
|
||||
|
||||
describe('rule', async () => {
|
||||
let endDate: string;
|
||||
|
@ -51,7 +51,7 @@ export default function ruleTests({ getService }: FtrProviderContext) {
|
|||
await esTestIndexToolOutput.destroy();
|
||||
await esTestIndexToolOutput.setup();
|
||||
|
||||
connectorId = await createConnector(supertest, objectRemover);
|
||||
connectorId = await createConnector(supertest, objectRemover, ES_TEST_OUTPUT_INDEX_NAME);
|
||||
|
||||
// write documents in the future, figure out the end date
|
||||
const endDateMillis = Date.now() + (RULE_INTERVALS_TO_WRITE - 1) * RULE_INTERVAL_MILLIS;
|
||||
|
@ -130,7 +130,7 @@ export default function ruleTests({ getService }: FtrProviderContext) {
|
|||
].forEach(([searchType, initData]) =>
|
||||
it(`runs correctly: threshold on hit count < > for ${searchType} search type`, async () => {
|
||||
// write documents from now to the future end date in groups
|
||||
await createEsDocumentsInGroups(ES_GROUPS_TO_WRITE);
|
||||
await createEsDocumentsInGroups(ES_GROUPS_TO_WRITE, endDate);
|
||||
await initData();
|
||||
|
||||
const docs = await waitForDocs(2);
|
||||
|
@ -222,7 +222,7 @@ export default function ruleTests({ getService }: FtrProviderContext) {
|
|||
].forEach(([searchType, initData]) =>
|
||||
it(`runs correctly: use epoch millis - threshold on hit count < > for ${searchType} search type`, async () => {
|
||||
// write documents from now to the future end date in groups
|
||||
await createEsDocumentsInGroups(ES_GROUPS_TO_WRITE);
|
||||
await createEsDocumentsInGroups(ES_GROUPS_TO_WRITE, endDate);
|
||||
await initData();
|
||||
|
||||
const docs = await waitForDocs(2);
|
||||
|
@ -333,7 +333,7 @@ export default function ruleTests({ getService }: FtrProviderContext) {
|
|||
].forEach(([searchType, initData]) =>
|
||||
it(`runs correctly with query: threshold on hit count < > for ${searchType}`, async () => {
|
||||
// write documents from now to the future end date in groups
|
||||
await createEsDocumentsInGroups(ES_GROUPS_TO_WRITE);
|
||||
await createEsDocumentsInGroups(ES_GROUPS_TO_WRITE, endDate);
|
||||
await initData();
|
||||
|
||||
const docs = await waitForDocs(1);
|
||||
|
@ -471,7 +471,7 @@ export default function ruleTests({ getService }: FtrProviderContext) {
|
|||
|
||||
// delay to let rule run once before adding data
|
||||
await new Promise((resolve) => setTimeout(resolve, 3000));
|
||||
await createEsDocumentsInGroups(1);
|
||||
await createEsDocumentsInGroups(1, endDate);
|
||||
|
||||
const docs = await waitForDocs(2);
|
||||
const activeDoc = docs[0];
|
||||
|
@ -573,6 +573,7 @@ export default function ruleTests({ getService }: FtrProviderContext) {
|
|||
// write documents from now to the future end date in groups
|
||||
await createEsDocumentsInGroups(
|
||||
ES_GROUPS_TO_WRITE,
|
||||
endDate,
|
||||
esTestIndexToolDataStream,
|
||||
ES_TEST_DATA_STREAM_NAME
|
||||
);
|
||||
|
@ -602,44 +603,6 @@ export default function ruleTests({ getService }: FtrProviderContext) {
|
|||
})
|
||||
);
|
||||
|
||||
async function createEsDocumentsInGroups(
|
||||
groups: number,
|
||||
indexTool: ESTestIndexTool = esTestIndexTool,
|
||||
indexName: string = ES_TEST_INDEX_NAME
|
||||
) {
|
||||
await createEsDocuments(
|
||||
es,
|
||||
indexTool,
|
||||
endDate,
|
||||
RULE_INTERVALS_TO_WRITE,
|
||||
RULE_INTERVAL_MILLIS,
|
||||
groups,
|
||||
indexName
|
||||
);
|
||||
}
|
||||
|
||||
async function waitForDocs(count: number): Promise<any[]> {
|
||||
return await esTestIndexToolOutput.waitForDocs(
|
||||
ES_TEST_INDEX_SOURCE,
|
||||
ES_TEST_INDEX_REFERENCE,
|
||||
count
|
||||
);
|
||||
}
|
||||
|
||||
interface CreateRuleParams {
|
||||
name: string;
|
||||
size: number;
|
||||
thresholdComparator: string;
|
||||
threshold: number[];
|
||||
timeWindowSize?: number;
|
||||
esQuery?: string;
|
||||
timeField?: string;
|
||||
searchConfiguration?: unknown;
|
||||
searchType?: 'searchSource';
|
||||
notifyWhen?: string;
|
||||
indexName?: string;
|
||||
}
|
||||
|
||||
async function createRule(params: CreateRuleParams): Promise<string> {
|
||||
const action = {
|
||||
id: connectorId,
|
||||
|
@ -725,23 +688,3 @@ export default function ruleTests({ getService }: FtrProviderContext) {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function createConnector(supertest: any, objectRemover: ObjectRemover): Promise<string> {
|
||||
const { body: createdConnector } = await supertest
|
||||
.post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'index action for es query FT',
|
||||
connector_type_id: CONNECTOR_TYPE_ID,
|
||||
config: {
|
||||
index: ES_TEST_OUTPUT_INDEX_NAME,
|
||||
},
|
||||
secrets: {},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const connectorId = createdConnector.id;
|
||||
objectRemover.add(Spaces.space1.id, connectorId, 'connector', 'actions');
|
||||
|
||||
return connectorId;
|
||||
}
|
||||
|
|
|
@ -443,5 +443,63 @@ export default function createGetTests({ getService }: FtrProviderContext) {
|
|||
expect(response.statusCode).to.equal(200);
|
||||
expect(response.body._source?.alert?.tags).to.eql(['test-tag-1', 'foo-tag']);
|
||||
});
|
||||
|
||||
it('8.5.0 removes runtime and field params from older ES Query rules', async () => {
|
||||
const response = await es.get<{
|
||||
alert: {
|
||||
params: {
|
||||
esQuery: string;
|
||||
};
|
||||
};
|
||||
}>(
|
||||
{
|
||||
index: '.kibana',
|
||||
id: 'alert:c8b39c29-d860-43b6-8817-b8058d80ddbc',
|
||||
},
|
||||
{ meta: true }
|
||||
);
|
||||
expect(response.statusCode).to.eql(200);
|
||||
expect(response.body._source?.alert?.params?.esQuery).to.eql(
|
||||
JSON.stringify({ query: { match_all: {} } }, null, 4)
|
||||
);
|
||||
});
|
||||
|
||||
it('8.5.0 doesnt reformat ES Query rules that dot have a runetime field on them', async () => {
|
||||
const response = await es.get<{
|
||||
alert: {
|
||||
params: {
|
||||
esQuery: string;
|
||||
};
|
||||
};
|
||||
}>(
|
||||
{
|
||||
index: '.kibana',
|
||||
id: 'alert:62c62b7f-8bf3-4104-a064-6247b7bda44f',
|
||||
},
|
||||
{ meta: true }
|
||||
);
|
||||
expect(response.statusCode).to.eql(200);
|
||||
expect(response.body._source?.alert?.params?.esQuery).to.eql(
|
||||
'{\n\t"query":\n{\n\t"match_all":\n\t{}\n}\n}'
|
||||
);
|
||||
});
|
||||
|
||||
it('8.5.0 doesnt fail upgrade when an ES Query rule is not parsable', async () => {
|
||||
const response = await es.get<{
|
||||
alert: {
|
||||
params: {
|
||||
esQuery: string;
|
||||
};
|
||||
};
|
||||
}>(
|
||||
{
|
||||
index: '.kibana',
|
||||
id: 'alert:f0d13f4d-35ae-4554-897a-6392e97bb84c',
|
||||
},
|
||||
{ meta: true }
|
||||
);
|
||||
expect(response.statusCode).to.eql(200);
|
||||
expect(response.body._source?.alert?.params?.esQuery).to.eql('{"query":}');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1041,3 +1041,156 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
"type": "doc",
|
||||
"value": {
|
||||
"id": "alert:c8b39c29-d860-43b6-8817-b8058d80ddbc",
|
||||
"index": ".kibana_1",
|
||||
"source": {
|
||||
"alert": {
|
||||
"name": "Old ESQuery with Runtimefield",
|
||||
"alertTypeId": ".es-query",
|
||||
"consumer": "alerts",
|
||||
"params": {
|
||||
"esQuery": "{\n \"runtime_mappings\": {\n \"kebabAsSnake\": {\n \"type\": \"keyword\",\n \"script\": {\n \"source\": \"\\\"\\\"\\\"\\\"\"\n }\n }\n },\n \"fields\": [\"kebabAsSnake\"],\n \"query\":{\n \"match_all\" : {}\n }}",
|
||||
"size": 100,
|
||||
"timeWindowSize": 5,
|
||||
"timeWindowUnit": "m",
|
||||
"threshold": [
|
||||
1000
|
||||
],
|
||||
"thresholdComparator": ">",
|
||||
"index": [
|
||||
"kibana_sample_data_ecommerce"
|
||||
],
|
||||
"timeField": "order_date"
|
||||
},
|
||||
"schedule": {
|
||||
"interval": "1m"
|
||||
},
|
||||
"enabled": true,
|
||||
"actions": [
|
||||
],
|
||||
"throttle": null,
|
||||
"apiKeyOwner": null,
|
||||
"createdBy" : "elastic",
|
||||
"updatedBy" : "elastic",
|
||||
"createdAt": "2022-03-26T16:04:50.698Z",
|
||||
"muteAll": false,
|
||||
"mutedInstanceIds": [],
|
||||
"scheduledTaskId": "c8b39c29-d860-43b6-8817-b8058d80ddbc",
|
||||
"tags": []
|
||||
},
|
||||
"type": "alert",
|
||||
"updated_at": "2022-03-26T16:05:55.957Z",
|
||||
"migrationVersion": {
|
||||
"alert": "8.0.1"
|
||||
},
|
||||
"references": [
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
"type": "doc",
|
||||
"value": {
|
||||
"id": "alert:62c62b7f-8bf3-4104-a064-6247b7bda44f",
|
||||
"index": ".kibana_1",
|
||||
"source": {
|
||||
"alert": {
|
||||
"name": "Old ESQuery without a Runtimefield but custom formatting",
|
||||
"alertTypeId": ".es-query",
|
||||
"consumer": "alerts",
|
||||
"params": {
|
||||
"esQuery": "{\n\t\"query\":\n{\n\t\"match_all\":\n\t{}\n}\n}",
|
||||
"size": 100,
|
||||
"timeWindowSize": 5,
|
||||
"timeWindowUnit": "m",
|
||||
"threshold": [
|
||||
1000
|
||||
],
|
||||
"thresholdComparator": ">",
|
||||
"index": [
|
||||
"kibana_sample_data_ecommerce"
|
||||
],
|
||||
"timeField": "order_date"
|
||||
},
|
||||
"schedule": {
|
||||
"interval": "1m"
|
||||
},
|
||||
"enabled": true,
|
||||
"actions": [
|
||||
],
|
||||
"throttle": null,
|
||||
"apiKeyOwner": null,
|
||||
"createdBy" : "elastic",
|
||||
"updatedBy" : "elastic",
|
||||
"createdAt": "2022-03-26T16:04:50.698Z",
|
||||
"muteAll": false,
|
||||
"mutedInstanceIds": [],
|
||||
"scheduledTaskId": "62c62b7f-8bf3-4104-a064-6247b7bda44f",
|
||||
"tags": []
|
||||
},
|
||||
"type": "alert",
|
||||
"updated_at": "2022-03-26T16:05:55.957Z",
|
||||
"migrationVersion": {
|
||||
"alert": "8.0.1"
|
||||
},
|
||||
"references": [
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
"type": "doc",
|
||||
"value": {
|
||||
"id": "alert:f0d13f4d-35ae-4554-897a-6392e97bb84c",
|
||||
"index": ".kibana_1",
|
||||
"source": {
|
||||
"alert": {
|
||||
"name": "Old ESQuery with unparsable query",
|
||||
"alertTypeId": ".es-query",
|
||||
"consumer": "alerts",
|
||||
"params": {
|
||||
"esQuery": "{\"query\":}",
|
||||
"size": 100,
|
||||
"timeWindowSize": 5,
|
||||
"timeWindowUnit": "m",
|
||||
"threshold": [
|
||||
1000
|
||||
],
|
||||
"thresholdComparator": ">",
|
||||
"index": [
|
||||
"kibana_sample_data_ecommerce"
|
||||
],
|
||||
"timeField": "order_date"
|
||||
},
|
||||
"schedule": {
|
||||
"interval": "1m"
|
||||
},
|
||||
"enabled": true,
|
||||
"actions": [
|
||||
],
|
||||
"throttle": null,
|
||||
"apiKeyOwner": null,
|
||||
"createdBy" : "elastic",
|
||||
"updatedBy" : "elastic",
|
||||
"createdAt": "2022-03-26T16:04:50.698Z",
|
||||
"muteAll": false,
|
||||
"mutedInstanceIds": [],
|
||||
"scheduledTaskId": "f0d13f4d-35ae-4554-897a-6392e97bb84c",
|
||||
"tags": []
|
||||
},
|
||||
"type": "alert",
|
||||
"updated_at": "2022-03-26T16:05:55.957Z",
|
||||
"migrationVersion": {
|
||||
"alert": "8.0.1"
|
||||
},
|
||||
"references": [
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue