Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* 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 { ALERT_RULE_NAME, ALERT_STATUS, ALERT_DURATION } from '@kbn/rule-data-utils';
import { getAlertsDataViewBase } from './get_alerts_data_view_base';

describe('getAlertsDataViewBase', () => {
it('should return a DataViewBase with the alerts index pattern', () => {
const dataView = getAlertsDataViewBase();
expect(dataView.title).toBe('.alerts-*');
expect(dataView.fields.length).toBeGreaterThan(0);
});

it('should map keyword fields with correct esTypes', () => {
const dataView = getAlertsDataViewBase();
const ruleNameField = dataView.fields.find((f) => f.name === ALERT_RULE_NAME);

expect(ruleNameField).toBeDefined();
expect(ruleNameField!.type).toBe('string');
expect(ruleNameField!.esTypes).toEqual(['keyword']);
});

it('should map date fields correctly', () => {
const dataView = getAlertsDataViewBase();
const statusField = dataView.fields.find((f) => f.name === ALERT_STATUS);

expect(statusField).toBeDefined();
expect(statusField!.type).toBe('string');
expect(statusField!.esTypes).toEqual(['keyword']);
});

it('should map long fields as number type', () => {
const dataView = getAlertsDataViewBase();
const durationField = dataView.fields.find((f) => f.name === ALERT_DURATION);

expect(durationField).toBeDefined();
expect(durationField!.type).toBe('number');
expect(durationField!.esTypes).toEqual(['long']);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* 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 { alertFieldMap } from '@kbn/alerts-as-data-utils';
import type { DataViewBase, DataViewFieldBase } from '@kbn/es-query';

const ES_TYPE_TO_KBN_TYPE: Record<string, string> = {
keyword: 'string',
text: 'string',
long: 'number',
integer: 'number',
short: 'number',
byte: 'number',
double: 'number',
float: 'number',
half_float: 'number',
scaled_float: 'number',
date: 'date',
date_range: 'date_range',
boolean: 'boolean',
flattened: 'string',
version: 'string',
unmapped: 'string',
};

export function getAlertsDataViewBase(): DataViewBase {
const fields: DataViewFieldBase[] = Object.entries(alertFieldMap).map(([name, def]) => ({
name,
type: ES_TYPE_TO_KBN_TYPE[def.type] ?? def.type,
esTypes: [def.type],
scripted: false,
}));

return { title: '.alerts-*', fields };
}
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,59 @@ describe('MaintenanceWindowClient - create', () => {
`);
});

it('should throw if trying to create a maintenance window with invalid scoped query', async () => {
it.each([
['test*', 'test*'],
['test rule*', 'test rule*'],
])(
'should generate wildcard query for keyword fields with KQL pattern: %s',
async (kqlPattern, expectedWildcardValue) => {
jest.useFakeTimers().setSystemTime(new Date('2023-02-26T00:00:00.000Z'));

const mockMaintenanceWindow = getMockMaintenanceWindow({
expirationDate: moment(new Date()).tz('UTC').add(1, 'year').toISOString(),
});

savedObjectsClient.create.mockResolvedValueOnce({
attributes: mockMaintenanceWindow,
version: '123',
id: 'test-id',
} as unknown as SavedObject);

await createMaintenanceWindow(mockContext, {
data: {
title: mockMaintenanceWindow.title,
duration: mockMaintenanceWindow.duration,
rRule: mockMaintenanceWindow.rRule as CreateMaintenanceWindowParams['data']['rRule'],
scopedQuery: {
kql: `kibana.alert.rule.name: ${kqlPattern}`,
filters: [],
},
},
});

const createdAttributes = savedObjectsClient.create.mock.calls[0][1] as MaintenanceWindow;
const dsl = createdAttributes.scopedQuery!.dsl!;
const parsedDsl = JSON.parse(dsl);

const wildcardClause = parsedDsl.bool.filter[0];
expect(wildcardClause).toEqual({
bool: {
should: [
{
wildcard: {
'kibana.alert.rule.name': {
value: expectedWildcardValue,
},
},
},
],
minimum_should_match: 1,
},
});
}
);

it('should throw if trying to create a maintenance window with invalid scope', async () => {
jest.useFakeTimers().setSystemTime(new Date('2023-02-26T00:00:00.000Z'));

const mockMaintenanceWindow = getMockMaintenanceWindow({
Expand Down Expand Up @@ -306,4 +358,57 @@ describe('MaintenanceWindowClient - create', () => {
- [data.categoryIds.1]: expected value to equal [null]"
`);
});
it('should pass Query DSL wildcard filter through unchanged without requiring index pattern', async () => {
jest.useFakeTimers().setSystemTime(new Date('2023-02-26T00:00:00.000Z'));

const mockMaintenanceWindow = getMockMaintenanceWindow({
expirationDate: moment(new Date()).tz('UTC').add(1, 'year').toISOString(),
});

savedObjectsClient.create.mockResolvedValueOnce({
attributes: mockMaintenanceWindow,
version: '123',
id: 'test-id',
} as unknown as SavedObject);

const wildcardQuery = {
wildcard: {
'kibana.alert.rule.name': {
value: 'example*',
},
},
};

await createMaintenanceWindow(mockContext, {
data: {
title: mockMaintenanceWindow.title,
duration: mockMaintenanceWindow.duration,
rRule: mockMaintenanceWindow.rRule as CreateMaintenanceWindowParams['data']['rRule'],
scopedQuery: {
kql: '',
filters: [
{
meta: {
disabled: false,
negate: false,
alias: null,
},
$state: {
store: FilterStateStore.APP_STATE,
},
query: wildcardQuery,
},
],
},
},
});

const createdAttributes = savedObjectsClient.create.mock.calls[0][1] as MaintenanceWindow;
const dsl = createdAttributes.scopedQuery!.dsl!;
const parsedDsl = JSON.parse(dsl);

// Query DSL filters are passed through translateToQuery() as filter.query,
// so the wildcard query should appear unchanged in the output — no index pattern needed.
expect(parsedDsl.bool.filter[0]).toEqual(wildcardQuery);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { SavedObjectsUtils } from '@kbn/core/server';
import type { Filter } from '@kbn/es-query';
import { buildEsQuery } from '@kbn/es-query';
import { getEsQueryConfig } from '../../../../lib/get_es_query_config';
import { getAlertsDataViewBase } from '../../lib/get_alerts_data_view_base';
import { generateMaintenanceWindowEvents } from '../../lib/generate_maintenance_window_events';
import type { MaintenanceWindowClientContext } from '../../../../../common';
import { getScopedQueryErrorMessage } from '../../../../../common';
Expand Down Expand Up @@ -39,12 +40,13 @@ export async function createMaintenanceWindow(
}

let scopedQueryWithGeneratedValue = scopedQuery;
const indexPattern = getAlertsDataViewBase();

try {
if (scopedQuery) {
const dsl = JSON.stringify(
buildEsQuery(
undefined,
indexPattern,
[{ query: scopedQuery.kql, language: 'kuery' }],
scopedQuery.filters as Filter[],
esQueryConfig
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,75 @@ describe('MaintenanceWindowClient - update', () => {
);
});

it('should remove maintenance window with scoped query', async () => {
it.each([
['test*', 'test*'],
['test rule*', 'test rule*'],
])(
'should generate wildcard query for keyword fields with KQL pattern: %s',
async (kqlPattern, expectedWildcardValue) => {
jest.useFakeTimers().setSystemTime(new Date(firstTimestamp));

const mockMaintenanceWindow = getMockMaintenanceWindow({
duration: 60 * 60 * 1000,
rRule: {
tzid: 'CET',
dtstart: '2023-03-26T00:00:00.000Z',
freq: Frequency.WEEKLY,
interval: 1,
count: 5,
} as MaintenanceWindow['rRule'],
events: [{ gte: '2023-03-26T00:00:00.000Z', lte: '2023-03-26T00:12:34.000Z' }],
expirationDate: moment(new Date(firstTimestamp)).tz('UTC').add(2, 'week').toISOString(),
});

savedObjectsClient.get.mockResolvedValue({
attributes: mockMaintenanceWindow,
version: '123',
id: 'test-id',
} as unknown as SavedObject);

savedObjectsClient.create.mockResolvedValue({
attributes: {
...mockMaintenanceWindow,
...updatedAttributes,
...updatedMetadata,
},
id: 'test-id',
} as unknown as SavedObject);

await updateMaintenanceWindow(mockContext, {
id: 'test-id',
data: {
scopedQuery: {
kql: `kibana.alert.rule.name: ${kqlPattern}`,
filters: [],
},
},
});

const createdAttributes = savedObjectsClient.create.mock.calls[0][1] as MaintenanceWindow;
const dsl = createdAttributes.scopedQuery!.dsl!;
const parsedDsl = JSON.parse(dsl);

const wildcardClause = parsedDsl.bool.filter[0];
expect(wildcardClause).toEqual({
bool: {
should: [
{
wildcard: {
'kibana.alert.rule.name': {
value: expectedWildcardValue,
},
},
},
],
minimum_should_match: 1,
},
});
}
);

it('should remove maintenance window with scope', async () => {
jest.useFakeTimers().setSystemTime(new Date(firstTimestamp));

const modifiedEvents = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { buildEsQuery } from '@kbn/es-query';
import type { MaintenanceWindowClientContext } from '../../../../../common';
import { getScopedQueryErrorMessage } from '../../../../../common';
import { getEsQueryConfig } from '../../../../lib/get_es_query_config';
import { getAlertsDataViewBase } from '../../lib/get_alerts_data_view_base';
import type { MaintenanceWindow } from '../../types';
import {
generateMaintenanceWindowEvents,
Expand Down Expand Up @@ -60,11 +61,13 @@ async function updateWithOCC(
}

let scopedQueryWithGeneratedValue = scopedQuery;
const indexPattern = getAlertsDataViewBase();

try {
if (scopedQuery) {
const dsl = JSON.stringify(
buildEsQuery(
undefined,
indexPattern,
[{ query: scopedQuery.kql, language: 'kuery' }],
scopedQuery.filters as Filter[],
esQueryConfig
Expand Down
Loading
Loading