diff --git a/x-pack/legacy/plugins/siem/scripts/convert_saved_search_to_signals.js b/x-pack/legacy/plugins/siem/scripts/convert_saved_search_to_signals.js index 78281efa09e52..5692ce2721870 100644 --- a/x-pack/legacy/plugins/siem/scripts/convert_saved_search_to_signals.js +++ b/x-pack/legacy/plugins/siem/scripts/convert_saved_search_to_signals.js @@ -30,7 +30,7 @@ const path = require('path'); // into another repository. const INTERVAL = '24h'; const SEVERITY = 'low'; -const TYPE = 'kql'; +const TYPE = 'query'; const FROM = 'now-24h'; const TO = 'now'; const INDEX = ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*']; @@ -70,43 +70,74 @@ async function main() { const files = process.argv[2]; const outputDir = process.argv[3]; - const savedSearchesJson = walk(files).filter(file => file.endsWith('.ndjson')); + const savedSearchesJson = walk(files).filter(file => { + return !path.basename(file).startsWith('.') && file.endsWith('.ndjson'); + }); const savedSearchesParsed = savedSearchesJson.reduce((accum, json) => { const jsonFile = fs.readFileSync(json, 'utf8'); - try { - const parsedFile = JSON.parse(jsonFile); - parsedFile._file = json; - parsedFile.attributes.kibanaSavedObjectMeta.searchSourceJSON = JSON.parse( - parsedFile.attributes.kibanaSavedObjectMeta.searchSourceJSON - ); - return [...accum, parsedFile]; - } catch (err) { - return accum; - } + const jsonLines = jsonFile.split(/\r{0,1}\n/); + const parsedLines = jsonLines.reduce((accum, line, index) => { + try { + const parsedLine = JSON.parse(line); + if (index !== 0) { + parsedLine._file = `${json.substring(0, json.length - '.ndjson'.length)}_${String( + index + )}.ndjson`; + } else { + parsedLine._file = json; + } + parsedLine.attributes.kibanaSavedObjectMeta.searchSourceJSON = JSON.parse( + parsedLine.attributes.kibanaSavedObjectMeta.searchSourceJSON + ); + return [...accum, parsedLine]; + } catch (err) { + console.log('error parsing a line in this file:', json); + return accum; + } + }, []); + return [...accum, ...parsedLines]; }, []); - savedSearchesParsed.forEach(savedSearch => { - const fileToWrite = cleanupFileName(savedSearch._file); + savedSearchesParsed.forEach( + ({ + _file, + attributes: { + description, + title, + kibanaSavedObjectMeta: { + searchSourceJSON: { + query: { query, language }, + filter, + }, + }, + }, + }) => { + const fileToWrite = cleanupFileName(_file); - const query = savedSearch.attributes.kibanaSavedObjectMeta.searchSourceJSON.query.query; - if (query != null && query.trim() !== '') { - const outputMessage = { - id: fileToWrite, - description: savedSearch.attributes.description || savedSearch.attributes.title, - index: INDEX, - interval: INTERVAL, - name: savedSearch.attributes.title, - severity: SEVERITY, - type: TYPE, - from: FROM, - to: TO, - kql: savedSearch.attributes.kibanaSavedObjectMeta.searchSourceJSON.query.query, - }; + if (query != null && query.trim() !== '') { + const outputMessage = { + id: fileToWrite, + description: description || title, + index: INDEX, + interval: INTERVAL, + name: title, + severity: SEVERITY, + type: TYPE, + from: FROM, + to: TO, + query, + language, + filters: filter, + }; - fs.writeFileSync(`${outputDir}/${fileToWrite}.json`, JSON.stringify(outputMessage, null, 2)); + fs.writeFileSync( + `${outputDir}/${fileToWrite}.json`, + JSON.stringify(outputMessage, null, 2) + ); + } } - }); + ); } if (require.main === module) { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/build_events_query.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/build_events_query.ts index 91efdc41ca798..aa221e8f7fb2b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/build_events_query.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/build_events_query.ts @@ -4,41 +4,25 @@ * you may not use this file except in compliance with the Elastic License. */ -import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; - interface BuildEventsScrollQuery { index: string[]; from: string; to: string; - kql: string | undefined; - filter: Record | undefined; + filter: unknown; size: number; scroll: string; } -export const getFilter = (kql: string | undefined, filter: Record | undefined) => { - if (kql != null) { - return toElasticsearchQuery(fromKueryExpression(kql), null); - } else if (filter != null) { - return filter; - } else { - // TODO: Re-visit this error (which should never happen) when we do signal errors for the UI - throw new TypeError('either kql or filter should be set'); - } -}; - export const buildEventsScrollQuery = ({ index, from, to, - kql, filter, size, scroll, }: BuildEventsScrollQuery) => { - const kqlOrFilter = getFilter(kql, filter); const filterWithTime = [ - kqlOrFilter, + filter, { bool: { filter: [ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/build_events_reindex.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/build_events_reindex.ts index c7bdc0263e4ce..4c5d981614cf1 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/build_events_reindex.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/build_events_reindex.ts @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; - // TODO: Re-index is just a temporary solution in order to speed up development // of any front end pieces. This should be replaced with a combination of the file // build_events_query.ts and any scrolling/scaling solutions from that particular @@ -17,9 +15,8 @@ interface BuildEventsReIndexParams { from: string; to: string; signalsIndex: string; - maxDocs: string; - filter: Record | undefined; - kql: string | undefined; + maxDocs: number; + filter: unknown; severity: string; name: string; timeDetected: string; @@ -29,17 +26,6 @@ interface BuildEventsReIndexParams { references: string[]; } -export const getFilter = (kql: string | undefined, filter: Record | undefined) => { - if (kql != null) { - return toElasticsearchQuery(fromKueryExpression(kql), null); - } else if (filter != null) { - return filter; - } else { - // TODO: Re-visit this error (which should never happen) when we do signal errors for the UI - throw new TypeError('either kql or filter should be set'); - } -}; - export const buildEventsReIndex = ({ description, index, @@ -48,7 +34,6 @@ export const buildEventsReIndex = ({ signalsIndex, maxDocs, filter, - kql, severity, name, timeDetected, @@ -57,11 +42,10 @@ export const buildEventsReIndex = ({ type, references, }: BuildEventsReIndexParams) => { - const kqlOrFilter = getFilter(kql, filter); const indexPatterns = index.map(element => `"${element}"`).join(','); const refs = references.map(element => `"${element}"`).join(','); const filterWithTime = [ - kqlOrFilter, + filter, { bool: { filter: [ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_signals.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_signals.ts index 70b6723fc51ee..e5c51d0773679 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_signals.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/create_signals.ts @@ -17,10 +17,13 @@ export const updateIfIdExists = async ({ enabled, filter, from, + query, + language, + savedId, + filters, id, index, interval, - kql, maxSignals, name, severity, @@ -36,10 +39,13 @@ export const updateIfIdExists = async ({ enabled, filter, from, + query, + language, + savedId, + filters, id, index, interval, - kql, maxSignals, name, severity, @@ -62,10 +68,13 @@ export const createSignals = async ({ enabled, filter, from, + query, + language, + savedId, + filters, id, index, interval, - kql, maxSignals, name, severity, @@ -81,10 +90,13 @@ export const createSignals = async ({ enabled, filter, from, + query, + language, + savedId, + filters, id, index, interval, - kql, maxSignals, name, severity, @@ -115,7 +127,10 @@ export const createSignals = async ({ index, from, filter, - kql, + query, + language, + savedId, + filters, maxSignals, name, severity, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_filter.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_filter.test.ts new file mode 100644 index 0000000000000..9f72da44e963b --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_filter.test.ts @@ -0,0 +1,438 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getQueryFilter, getFilter } from './get_filter'; +import { savedObjectsClientMock } from 'src/core/server/mocks'; +import { AlertServices } from '../../../../../alerting/server/types'; + +describe('get_filter', () => { + let savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: { + query: { query: 'host.name: linux', language: 'kuery' }, + filters: [], + }, + })); + let servicesMock: AlertServices = { + savedObjectsClient, + callCluster: jest.fn(), + alertInstanceFactory: jest.fn(), + }; + + beforeAll(() => { + jest.resetAllMocks(); + }); + + beforeEach(() => { + savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.get = jest.fn().mockImplementation(() => ({ + attributes: { + query: { query: 'host.name: linux', language: 'kuery' }, + language: 'kuery', + filters: [], + }, + })); + servicesMock = { + savedObjectsClient, + callCluster: jest.fn(), + alertInstanceFactory: jest.fn(), + }; + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('getQueryFilter', () => { + test('it should work with an empty filter as kuery', () => { + const esQuery = getQueryFilter('host.name: linux', 'kuery', [], ['auditbeat-*']); + expect(esQuery).toEqual({ + bool: { + must: [], + filter: [ + { + bool: { + should: [ + { + match: { + 'host.name': 'linux', + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + should: [], + must_not: [], + }, + }); + }); + + test('it should work with an empty filter as lucene', () => { + const esQuery = getQueryFilter('host.name: linux', 'lucene', [], ['auditbeat-*']); + expect(esQuery).toEqual({ + bool: { + must: [ + { + query_string: { + query: 'host.name: linux', + analyze_wildcard: true, + time_zone: 'Zulu', + }, + }, + ], + filter: [], + should: [], + must_not: [], + }, + }); + }); + + test('it should work with a simple filter as a kuery', () => { + const esQuery = getQueryFilter( + 'host.name: windows', + 'kuery', + [ + { + meta: { + alias: 'custom label here', + disabled: false, + key: 'host.name', + negate: false, + params: { + query: 'siem-windows', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'host.name': 'siem-windows', + }, + }, + }, + ], + ['auditbeat-*'] + ); + expect(esQuery).toEqual({ + bool: { + must: [], + filter: [ + { + bool: { + should: [ + { + match: { + 'host.name': 'windows', + }, + }, + ], + minimum_should_match: 1, + }, + }, + { + match_phrase: { + 'host.name': 'siem-windows', + }, + }, + ], + should: [], + must_not: [], + }, + }); + }); + + test('it should work with a simple filter that is disabled as a kuery', () => { + const esQuery = getQueryFilter( + 'host.name: windows', + 'kuery', + [ + { + meta: { + alias: 'custom label here', + disabled: true, + key: 'host.name', + negate: false, + params: { + query: 'siem-windows', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'host.name': 'siem-windows', + }, + }, + }, + ], + ['auditbeat-*'] + ); + expect(esQuery).toEqual({ + bool: { + must: [], + filter: [ + { + bool: { + should: [ + { + match: { + 'host.name': 'windows', + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + should: [], + must_not: [], + }, + }); + }); + + test('it should work with a simple filter as a lucene', () => { + const esQuery = getQueryFilter( + 'host.name: windows', + 'lucene', + [ + { + meta: { + alias: 'custom label here', + disabled: false, + key: 'host.name', + negate: false, + params: { + query: 'siem-windows', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'host.name': 'siem-windows', + }, + }, + }, + ], + ['auditbeat-*'] + ); + expect(esQuery).toEqual({ + bool: { + must: [ + { + query_string: { + query: 'host.name: windows', + analyze_wildcard: true, + time_zone: 'Zulu', + }, + }, + ], + filter: [ + { + match_phrase: { + 'host.name': 'siem-windows', + }, + }, + ], + should: [], + must_not: [], + }, + }); + }); + + test('it should work with a simple filter that is disabled as a lucene', () => { + const esQuery = getQueryFilter( + 'host.name: windows', + 'lucene', + [ + { + meta: { + alias: 'custom label here', + disabled: true, + key: 'host.name', + negate: false, + params: { + query: 'siem-windows', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'host.name': 'siem-windows', + }, + }, + }, + ], + ['auditbeat-*'] + ); + expect(esQuery).toEqual({ + bool: { + must: [ + { + query_string: { + query: 'host.name: windows', + analyze_wildcard: true, + time_zone: 'Zulu', + }, + }, + ], + filter: [], + should: [], + must_not: [], + }, + }); + }); + }); + + describe('getFilter', () => { + test('returns a filter if given a type of filter as is', async () => { + const filter = await getFilter({ + type: 'filter', + filter: { something: '1' }, + filters: undefined, + language: undefined, + query: undefined, + savedId: undefined, + services: servicesMock, + index: ['auditbeat-*'], + }); + expect(filter).toEqual({ + something: '1', + }); + }); + + test('returns a query if given a type of query', async () => { + const filter = await getFilter({ + type: 'query', + filter: undefined, + filters: undefined, + language: 'kuery', + query: 'host.name: siem', + savedId: undefined, + services: servicesMock, + index: ['auditbeat-*'], + }); + expect(filter).toEqual({ + bool: { + must: [], + filter: [ + { + bool: { + should: [ + { + match: { + 'host.name': 'siem', + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + should: [], + must_not: [], + }, + }); + }); + + test('throws on type query if query is undefined', async () => { + await expect( + getFilter({ + type: 'query', + filter: undefined, + filters: undefined, + language: undefined, + query: 'host.name: siem', + savedId: undefined, + services: servicesMock, + index: ['auditbeat-*'], + }) + ).rejects.toThrow('query, filters, and index parameter should be defined'); + }); + + test('throws on type query if language is undefined', async () => { + await expect( + getFilter({ + type: 'query', + filter: undefined, + filters: undefined, + language: 'kuery', + query: undefined, + savedId: undefined, + services: servicesMock, + index: ['auditbeat-*'], + }) + ).rejects.toThrow('query, filters, and index parameter should be defined'); + }); + + test('throws on type query if index is undefined', async () => { + await expect( + getFilter({ + type: 'query', + filter: undefined, + filters: undefined, + language: 'kuery', + query: 'host.name: siem', + savedId: undefined, + services: servicesMock, + index: undefined, + }) + ).rejects.toThrow('query, filters, and index parameter should be defined'); + }); + + test('returns a saved query if given a type of query', async () => { + const filter = await getFilter({ + type: 'saved_query', + filter: undefined, + filters: undefined, + language: undefined, + query: undefined, + savedId: 'some-id', + services: servicesMock, + index: ['auditbeat-*'], + }); + expect(filter).toEqual({ + bool: { + filter: [ + { bool: { minimum_should_match: 1, should: [{ match: { 'host.name': 'linux' } }] } }, + ], + must: [], + must_not: [], + should: [], + }, + }); + }); + + test('throws on saved query if saved_id is undefined', async () => { + await expect( + getFilter({ + type: 'saved_query', + filter: undefined, + filters: undefined, + language: undefined, + query: undefined, + savedId: undefined, + services: servicesMock, + index: ['auditbeat-*'], + }) + ).rejects.toThrow('savedId parameter should be defined'); + }); + + test('throws on saved query if index is undefined', async () => { + await expect( + getFilter({ + type: 'saved_query', + filter: undefined, + filters: undefined, + language: undefined, + query: undefined, + savedId: 'some-id', + services: servicesMock, + index: undefined, + }) + ).rejects.toThrow('savedId parameter should be defined'); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_filter.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_filter.ts new file mode 100644 index 0000000000000..22ebccbaef6a7 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_filter.ts @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { buildEsQuery } from '@kbn/es-query'; +import { Query } from 'src/plugins/data/common/query'; +import { AlertServices } from '../../../../../alerting/server/types'; +import { SignalAlertParams, PartialFilter } from './types'; +import { assertUnreachable } from '../../../utils/build_query'; + +export const getQueryFilter = ( + query: string, + language: string, + filters: PartialFilter[], + index: string[] +) => { + const indexPattern = { + fields: [], + title: index.join(), + }; + const queries: Query[] = [{ query, language }]; + const config = { + allowLeadingWildcards: true, + queryStringOptions: { analyze_wildcard: true }, + ignoreFilterIfFieldNotInIndex: false, + dateFormatTZ: 'Zulu', + }; + const esQuery = buildEsQuery( + indexPattern, + queries, + filters.filter(f => f.meta != null && f.meta.disabled === false), + config + ); + return esQuery; +}; + +interface GetFilterArgs { + type: SignalAlertParams['type']; + filter: Record | undefined; + filters: PartialFilter[] | undefined; + language: string | undefined; + query: string | undefined; + savedId: string | undefined; + services: AlertServices; + index: string[] | undefined; +} + +export const getFilter = async ({ + filter, + filters, + index, + language, + savedId, + services, + type, + query, +}: GetFilterArgs): Promise => { + switch (type) { + case 'query': { + if (query != null && language != null && index != null) { + return getQueryFilter(query, language, filters || [], index); + } else { + throw new TypeError('query, filters, and index parameter should be defined'); + } + } + case 'saved_query': { + if (savedId != null && index != null) { + const savedObject = await services.savedObjectsClient.get('query', savedId); + return getQueryFilter( + savedObject.attributes.query.query, + savedObject.attributes.query.language, + savedObject.attributes.filters, + index + ); + } else { + throw new TypeError('savedId parameter should be defined'); + } + } + case 'filter': { + return filter; + } + } + return assertUnreachable(type); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/signals_alert_type.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/signals_alert_type.ts index aad2b2b19d67a..8d1668cc6d958 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/signals_alert_type.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/signals_alert_type.ts @@ -18,6 +18,7 @@ import { buildEventsScrollQuery } from './build_events_query'; // bulk scroll class import { scrollAndBulkIndex } from './utils'; import { SignalAlertTypeDefinition } from './types'; +import { getFilter } from './get_filter'; export const signalsAlertType = ({ logger }: { logger: Logger }): SignalAlertTypeDefinition => { return { @@ -31,7 +32,10 @@ export const signalsAlertType = ({ logger }: { logger: Logger }): SignalAlertTyp filter: schema.nullable(schema.object({}, { allowUnknowns: true })), id: schema.string(), index: schema.arrayOf(schema.string()), - kql: schema.nullable(schema.string()), + language: schema.nullable(schema.string()), + savedId: schema.nullable(schema.string()), + query: schema.nullable(schema.string()), + filters: schema.nullable(schema.arrayOf(schema.object({}, { allowUnknowns: true }))), maxSignals: schema.number({ defaultValue: 100 }), name: schema.string(), severity: schema.string(), @@ -51,7 +55,10 @@ export const signalsAlertType = ({ logger }: { logger: Logger }): SignalAlertTyp from, id, index, - kql, + filters, + language, + savedId, + query, maxSignals, name, references, @@ -65,13 +72,23 @@ export const signalsAlertType = ({ logger }: { logger: Logger }): SignalAlertTyp const scroll = scrollLock ? scrollLock : '1m'; const size = scrollSize ? scrollSize : 400; + const esFilter = await getFilter({ + type, + filter, + filters, + language, + query, + savedId, + services, + index, + }); + // TODO: Turn these options being sent in into a template for the alert type const noReIndex = buildEventsScrollQuery({ index, from, to, - kql, - filter, + filter: esFilter, size, scroll, }); @@ -79,7 +96,6 @@ export const signalsAlertType = ({ logger }: { logger: Logger }): SignalAlertTyp const reIndex = buildEventsReIndex({ index, from, - kql, to, // TODO: Change this out once we have solved // https://github.com/elastic/kibana/issues/47002 @@ -88,7 +104,7 @@ export const signalsAlertType = ({ logger }: { logger: Logger }): SignalAlertTyp description, name, timeDetected: new Date().toISOString(), - filter, + filter: esFilter, maxDocs: maxSignals, ruleRevision: 1, id, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts index 2d7b9dbb98d76..4f69c3c8e144b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/types.ts @@ -7,6 +7,7 @@ import { get } from 'lodash/fp'; import Hapi from 'hapi'; +import { Filter } from '@kbn/es-query'; import { SIGNALS_ID } from '../../../../common/constants'; import { Alert, @@ -18,21 +19,38 @@ import { AlertsClient } from '../../../../../alerting/server/alerts_client'; import { ActionsClient } from '../../../../../actions/server/actions_client'; import { SearchResponse } from '../../types'; +export type PartialFilter = Partial; + export interface SignalAlertParams { description: string; + enabled: boolean; + filter: Record | undefined; + filters: PartialFilter[] | undefined; from: string; - id: string; index: string[]; interval: string; - enabled: boolean; - filter: Record | undefined; - kql: string | undefined; - maxSignals: string; + id: string; + language: string | undefined; + maxSignals: number; name: string; + query: string | undefined; + references: string[]; + savedId: string | undefined; severity: string; - type: 'filter' | 'kql'; to: string; - references: string[]; + type: 'filter' | 'query' | 'saved_query'; +} + +export type SignalAlertParamsRest = Omit & { + saved_id: SignalAlertParams['savedId']; + max_signals: SignalAlertParams['maxSignals']; +}; + +export interface FindParamsRest { + per_page: number; + page: number; + sort_field: string; + fields: string[]; } export interface Clients { @@ -73,9 +91,7 @@ export type SignalAlertType = Alert & { }; export interface SignalsRequest extends Hapi.Request { - payload: Omit & { - max_signals: string; - }; + payload: SignalAlertParamsRest; } export type SignalExecutorOptions = Omit & { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_signals.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_signals.test.ts index e3e00e5cea5ce..46ebfe98ce3d9 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_signals.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_signals.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { calculateInterval, calculateKqlAndFilter } from './update_signals'; +import { calculateInterval } from './update_signals'; describe('update_signals', () => { describe('#calculateInterval', () => { @@ -23,30 +23,4 @@ describe('update_signals', () => { expect(interval).toEqual('5m'); }); }); - - describe('#calculateKqlAndFilter', () => { - test('given a undefined kql filter it returns a null kql', () => { - const kqlFilter = calculateKqlAndFilter(undefined, {}); - expect(kqlFilter).toEqual({ - filter: {}, - kql: null, - }); - }); - - test('given a undefined filter it returns a null filter', () => { - const kqlFilter = calculateKqlAndFilter('some kql string', undefined); - expect(kqlFilter).toEqual({ - filter: null, - kql: 'some kql string', - }); - }); - - test('given both a undefined filter and undefined kql it returns both as undefined', () => { - const kqlFilter = calculateKqlAndFilter(undefined, undefined); - expect(kqlFilter).toEqual({ - filter: undefined, - kql: undefined, - }); - }); - }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_signals.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_signals.ts index de2f0d511711c..cb777ad400393 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_signals.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/update_signals.ts @@ -22,30 +22,20 @@ export const calculateInterval = ( } }; -export const calculateKqlAndFilter = ( - kql: string | undefined, - filter: {} | undefined -): { kql: string | null | undefined; filter: {} | null | undefined } => { - if (filter != null) { - return { kql: null, filter }; - } else if (kql != null) { - return { kql, filter: null }; - } else { - return { kql: undefined, filter: undefined }; - } -}; - export const updateSignal = async ({ alertsClient, actionsClient, // TODO: Use this whenever we add feature support for different action types description, enabled, + query, + language, + savedId, + filters, filter, from, id, index, interval, - kql, maxSignals, name, severity, @@ -63,18 +53,19 @@ export const updateSignal = async ({ const alertTypeParams = signal.alertTypeParams || {}; - const { kql: nextKql, filter: nextFilter } = calculateKqlAndFilter(kql, filter); - const nextAlertTypeParams = defaults( { ...alertTypeParams, }, { description, - filter: nextFilter, + filter, from, + query, + language, + savedId, + filters, index, - kql: nextKql, maxSignals, name, severity, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts index 1b6f529a51418..7b3778d606e47 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -6,21 +6,30 @@ import { ServerInjectOptions } from 'hapi'; import { ActionResult } from '../../../../../../actions/server/types'; +import { SignalAlertParamsRest } from '../../alerts/types'; + +// The Omit of filter is because of a Hapi Server Typing issue that I am unclear +// where it comes from. I would hope to remove the "filter" as an omit at some point +// when we upgrade and Hapi Server is ok with the filter. +export const typicalPayload = (): Partial> => ({ + id: 'rule-1', + description: 'Detecting root and admin users', + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: '5m', + name: 'Detect Root/Admin Users', + type: 'query', + from: 'now-6m', + to: 'now', + severity: 'high', + query: 'user.name: root or user.name: admin', + language: 'kuery', +}); export const getUpdateRequest = (): ServerInjectOptions => ({ method: 'PUT', url: '/api/siem/signals', payload: { - id: 'rule-1', - description: 'Detecting root and admin users', - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - interval: '5m', - name: 'Detect Root/Admin Users', - severity: 'high', - type: 'kql', - from: 'now-6m', - to: 'now', - kql: 'user.name: root or user.name: admin', + ...typicalPayload(), }, }); @@ -50,16 +59,7 @@ export const getCreateRequest = (): ServerInjectOptions => ({ method: 'POST', url: '/api/siem/signals', payload: { - id: 'rule-1', - description: 'Detecting root and admin users', - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - interval: '5m', - name: 'Detect Root/Admin Users', - severity: 'high', - type: 'kql', - from: 'now-6m', - to: 'now', - kql: 'user.name: root or user.name: admin', + ...typicalPayload(), }, }); @@ -79,12 +79,13 @@ export const createAlertResult = () => ({ index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], from: 'now-6m', filter: null, - kql: 'user.name: root or user.name: admin', + query: 'user.name: root or user.name: admin', maxSignals: 100, name: 'Detect Root/Admin Users', severity: 'high', to: 'now', - type: 'kql', + type: 'query', + language: 'kuery', references: [], }, interval: '5m', @@ -133,12 +134,13 @@ export const updateAlertResult = () => ({ index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], from: 'now-6m', filter: null, - kql: 'user.name: root or user.name: admin', + query: 'user.name: root or user.name: admin', maxSignals: 100, name: 'Detect Root/Admin Users', severity: 'high', to: 'now', - type: 'kql', + type: 'query', + language: 'kuery', references: [], }, interval: '5m', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_signals.route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_signals_route.test.ts similarity index 65% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_signals.route.test.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_signals_route.test.ts index ca9e50b348220..0fe88cc856d62 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_signals.route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_signals_route.test.ts @@ -18,6 +18,7 @@ import { createActionResult, createAlertResult, getCreateRequest, + typicalPayload, } from './__mocks__/request_responses'; describe('create_signals', () => { @@ -30,7 +31,7 @@ describe('create_signals', () => { }); describe('status codes with actionClient and alertClient', () => { - it('returns 200 when deleting a single signal with a valid actionClient and alertClient', async () => { + test('returns 200 when creating a single signal with a valid actionClient and alertClient', async () => { alertsClient.find.mockResolvedValue(getFindResult()); alertsClient.get.mockResolvedValue(getResult()); actionsClient.create.mockResolvedValue(createActionResult()); @@ -39,21 +40,21 @@ describe('create_signals', () => { expect(statusCode).toBe(200); }); - it('returns 404 if actionClient is not available on the route', async () => { + test('returns 404 if actionClient is not available on the route', async () => { const { serverWithoutActionClient } = createMockServerWithoutActionClientDecoration(); createSignalsRoute(serverWithoutActionClient); const { statusCode } = await serverWithoutActionClient.inject(getCreateRequest()); expect(statusCode).toBe(404); }); - it('returns 404 if alertClient is not available on the route', async () => { + test('returns 404 if alertClient is not available on the route', async () => { const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); createSignalsRoute(serverWithoutAlertClient); const { statusCode } = await serverWithoutAlertClient.inject(getCreateRequest()); expect(statusCode).toBe(404); }); - it('returns 404 if alertClient and actionClient are both not available on the route', async () => { + test('returns 404 if alertClient and actionClient are both not available on the route', async () => { const { serverWithoutActionOrAlertClient, } = createMockServerWithoutActionOrAlertClientDecoration(); @@ -64,100 +65,73 @@ describe('create_signals', () => { }); describe('validation', () => { - it('returns 400 if id is not given', async () => { + test('returns 400 if id is not given', async () => { alertsClient.find.mockResolvedValue(getFindResult()); alertsClient.get.mockResolvedValue(getResult()); actionsClient.create.mockResolvedValue(createActionResult()); alertsClient.create.mockResolvedValue(createAlertResult()); + // missing id should throw a 400 + const { id, ...noId } = typicalPayload(); const request: ServerInjectOptions = { method: 'POST', url: '/api/siem/signals', - payload: { - // missing id should throw a 400 - description: 'Detecting root and admin users', - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - interval: '5m', - name: 'Detect Root/Admin Users', - severity: 'high', - type: 'kql', - from: 'now-6m', - to: 'now', - kql: 'user.name: root or user.name: admin', - }, + payload: noId, }; const { statusCode } = await server.inject(request); expect(statusCode).toBe(400); }); - it('returns 200 if type is kql', async () => { + test('returns 200 if type is query', async () => { alertsClient.find.mockResolvedValue(getFindResult()); alertsClient.get.mockResolvedValue(getResult()); actionsClient.create.mockResolvedValue(createActionResult()); alertsClient.create.mockResolvedValue(createAlertResult()); + const { type, ...noType } = typicalPayload(); const request: ServerInjectOptions = { method: 'POST', url: '/api/siem/signals', payload: { - id: 'rule-1', - description: 'Detecting root and admin users', - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - interval: '5m', - name: 'Detect Root/Admin Users', - severity: 'high', - type: 'kql', - from: 'now-6m', - to: 'now', - kql: 'user.name: root or user.name: admin', + ...noType, + type: 'query', }, }; const { statusCode } = await server.inject(request); expect(statusCode).toBe(200); }); - it('returns 200 if type is filter', async () => { + test('returns 200 if type is filter', async () => { alertsClient.find.mockResolvedValue(getFindResult()); alertsClient.get.mockResolvedValue(getResult()); actionsClient.create.mockResolvedValue(createActionResult()); alertsClient.create.mockResolvedValue(createAlertResult()); - const request: ServerInjectOptions = { + // Cannot type request with a ServerInjectOptions as the type system complains + // about the property filter involving Hapi types, so I left it off for now + const { language, query, type, ...noType } = typicalPayload(); + const request = { method: 'POST', url: '/api/siem/signals', payload: { - id: 'rule-1', - description: 'Detecting root and admin users', - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - interval: '5m', - name: 'Detect Root/Admin Users', - severity: 'high', + ...noType, type: 'filter', - from: 'now-6m', - to: 'now', - kql: 'user.name: root or user.name: admin', + filter: {}, }, }; const { statusCode } = await server.inject(request); expect(statusCode).toBe(200); }); - it('returns 400 if type is not filter or kql', async () => { + test('returns 400 if type is not filter or kql', async () => { alertsClient.find.mockResolvedValue(getFindResult()); alertsClient.get.mockResolvedValue(getResult()); actionsClient.create.mockResolvedValue(createActionResult()); alertsClient.create.mockResolvedValue(createAlertResult()); + const { type, ...noType } = typicalPayload(); const request: ServerInjectOptions = { method: 'POST', url: '/api/siem/signals', payload: { - id: 'rule-1', - description: 'Detecting root and admin users', - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - interval: '5m', - name: 'Detect Root/Admin Users', - severity: 'high', - type: 'something-made-up', // This is a made up type that causes the 400 - from: 'now-6m', - to: 'now', - kql: 'user.name: root or user.name: admin', + ...noType, + type: 'something-made-up', }, }; const { statusCode } = await server.inject(request); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_signals_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_signals_route.ts index 709b4fa55a991..a69523c907b0a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_signals_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/create_signals_route.ts @@ -5,10 +5,10 @@ */ import Hapi from 'hapi'; -import Joi from 'joi'; import { isFunction } from 'lodash/fp'; import { createSignals } from '../alerts/create_signals'; import { SignalsRequest } from '../alerts/types'; +import { createSignalsSchema } from './schemas'; export const createCreateSignalsRoute: Hapi.ServerRoute = { method: 'POST', @@ -19,24 +19,7 @@ export const createCreateSignalsRoute: Hapi.ServerRoute = { options: { abortEarly: false, }, - payload: Joi.object({ - description: Joi.string().required(), - enabled: Joi.boolean().default(true), - filter: Joi.object(), - from: Joi.string().required(), - id: Joi.string().required(), - index: Joi.array().required(), - interval: Joi.string().default('5m'), - kql: Joi.string(), - max_signals: Joi.number().default(100), - name: Joi.string().required(), - severity: Joi.string().required(), - to: Joi.string().required(), - type: Joi.string() - .valid('filter', 'kql') - .required(), - references: Joi.array().default([]), - }).xor('filter', 'kql'), + payload: createSignalsSchema, }, }, async handler(request: SignalsRequest, headers) { @@ -44,8 +27,12 @@ export const createCreateSignalsRoute: Hapi.ServerRoute = { description, enabled, filter, - kql, from, + query, + language, + // eslint-disable-next-line @typescript-eslint/camelcase + saved_id: savedId, + filters, id, index, interval, @@ -59,7 +46,6 @@ export const createCreateSignalsRoute: Hapi.ServerRoute = { } = request.payload; const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; - const actionsClient = isFunction(request.getActionsClient) ? request.getActionsClient() : null; if (!alertsClient || !actionsClient) { @@ -73,10 +59,13 @@ export const createCreateSignalsRoute: Hapi.ServerRoute = { enabled, filter, from, + query, + language, + savedId, + filters, id, index, interval, - kql, maxSignals, name, severity, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_signals_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_signals_route.test.ts index 70a072f466527..db74cf6508be6 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_signals_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/delete_signals_route.test.ts @@ -28,7 +28,7 @@ describe('delete_signals', () => { }); describe('status codes with actionClient and alertClient', () => { - it('returns 200 when deleting a single signal with a valid actionClient and alertClient', async () => { + test('returns 200 when deleting a single signal with a valid actionClient and alertClient', async () => { alertsClient.find.mockResolvedValue(getFindResult()); alertsClient.get.mockResolvedValue(getResult()); alertsClient.delete.mockResolvedValue({}); @@ -36,21 +36,21 @@ describe('delete_signals', () => { expect(statusCode).toBe(200); }); - it('returns 404 if actionClient is not available on the route', async () => { + test('returns 404 if actionClient is not available on the route', async () => { const { serverWithoutActionClient } = createMockServerWithoutActionClientDecoration(); deleteSignalsRoute(serverWithoutActionClient); const { statusCode } = await serverWithoutActionClient.inject(getDeleteRequest()); expect(statusCode).toBe(404); }); - it('returns 404 if alertClient is not available on the route', async () => { + test('returns 404 if alertClient is not available on the route', async () => { const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); deleteSignalsRoute(serverWithoutAlertClient); const { statusCode } = await serverWithoutAlertClient.inject(getDeleteRequest()); expect(statusCode).toBe(404); }); - it('returns 404 if alertClient and actionClient are both not available on the route', async () => { + test('returns 404 if alertClient and actionClient are both not available on the route', async () => { const { serverWithoutActionOrAlertClient, } = createMockServerWithoutActionOrAlertClientDecoration(); @@ -61,7 +61,7 @@ describe('delete_signals', () => { }); describe('validation', () => { - it('returns 404 if given a non-existent id', async () => { + test('returns 404 if given a non-existent id', async () => { alertsClient.find.mockResolvedValue(getFindResult()); alertsClient.get.mockResolvedValue(getResult()); alertsClient.delete.mockResolvedValue({}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_signals_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_signals_route.test.ts index c054f35f93c28..331f8874eb29b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_signals_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_signals_route.test.ts @@ -28,7 +28,7 @@ describe('find_signals', () => { }); describe('status codes with actionClient and alertClient', () => { - it('returns 200 when deleting a single signal with a valid actionClient and alertClient', async () => { + test('returns 200 when finding a single signal with a valid actionClient and alertClient', async () => { alertsClient.find.mockResolvedValue(getFindResult()); actionsClient.find.mockResolvedValue({ page: 1, @@ -41,21 +41,21 @@ describe('find_signals', () => { expect(statusCode).toBe(200); }); - it('returns 404 if actionClient is not available on the route', async () => { + test('returns 404 if actionClient is not available on the route', async () => { const { serverWithoutActionClient } = createMockServerWithoutActionClientDecoration(); findSignalsRoute(serverWithoutActionClient); const { statusCode } = await serverWithoutActionClient.inject(getFindRequest()); expect(statusCode).toBe(404); }); - it('returns 404 if alertClient is not available on the route', async () => { + test('returns 404 if alertClient is not available on the route', async () => { const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); findSignalsRoute(serverWithoutAlertClient); const { statusCode } = await serverWithoutAlertClient.inject(getFindRequest()); expect(statusCode).toBe(404); }); - it('returns 404 if alertClient and actionClient are both not available on the route', async () => { + test('returns 404 if alertClient and actionClient are both not available on the route', async () => { const { serverWithoutActionOrAlertClient, } = createMockServerWithoutActionOrAlertClientDecoration(); @@ -66,7 +66,7 @@ describe('find_signals', () => { }); describe('validation', () => { - it('returns 400 if a bad query parameter is given', async () => { + test('returns 400 if a bad query parameter is given', async () => { alertsClient.find.mockResolvedValue(getFindResult()); alertsClient.get.mockResolvedValue(getResult()); const request: ServerInjectOptions = { @@ -77,7 +77,7 @@ describe('find_signals', () => { expect(statusCode).toBe(400); }); - it('returns 200 if the set of optional query parameters are given', async () => { + test('returns 200 if the set of optional query parameters are given', async () => { alertsClient.find.mockResolvedValue(getFindResult()); alertsClient.get.mockResolvedValue(getResult()); const request: ServerInjectOptions = { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_signals_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_signals_route.ts index a16932d256e5e..e6f4703ff2e63 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_signals_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/find_signals_route.ts @@ -5,10 +5,10 @@ */ import Hapi from 'hapi'; -import Joi from 'joi'; import { isFunction } from 'lodash/fp'; import { findSignals } from '../alerts/find_signals'; import { FindSignalsRequest } from '../alerts/types'; +import { findSignalsSchema } from './schemas'; export const createFindSignalRoute: Hapi.ServerRoute = { method: 'GET', @@ -19,20 +19,7 @@ export const createFindSignalRoute: Hapi.ServerRoute = { options: { abortEarly: false, }, - query: Joi.object() - .keys({ - per_page: Joi.number() - .min(0) - .default(20), - page: Joi.number() - .min(1) - .default(1), - sort_field: Joi.string(), - fields: Joi.array() - .items(Joi.string()) - .single(), - }) - .default(), + query: findSignalsSchema, }, }, async handler(request: FindSignalsRequest, headers) { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_signals_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_signals_route.test.ts index 1010ae7e0c7bd..43c96792606a2 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_signals_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/read_signals_route.test.ts @@ -28,28 +28,28 @@ describe('read_signals', () => { }); describe('status codes with actionClient and alertClient', () => { - it('returns 200 when deleting a single signal with a valid actionClient and alertClient', async () => { + test('returns 200 when reading a single signal with a valid actionClient and alertClient', async () => { alertsClient.find.mockResolvedValue(getFindResult()); alertsClient.get.mockResolvedValue(getResult()); const { statusCode } = await server.inject(getReadRequest()); expect(statusCode).toBe(200); }); - it('returns 404 if actionClient is not available on the route', async () => { + test('returns 404 if actionClient is not available on the route', async () => { const { serverWithoutActionClient } = createMockServerWithoutActionClientDecoration(); readSignalsRoute(serverWithoutActionClient); const { statusCode } = await serverWithoutActionClient.inject(getReadRequest()); expect(statusCode).toBe(404); }); - it('returns 404 if alertClient is not available on the route', async () => { + test('returns 404 if alertClient is not available on the route', async () => { const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); readSignalsRoute(serverWithoutAlertClient); const { statusCode } = await serverWithoutAlertClient.inject(getReadRequest()); expect(statusCode).toBe(404); }); - it('returns 404 if alertClient and actionClient are both not available on the route', async () => { + test('returns 404 if alertClient and actionClient are both not available on the route', async () => { const { serverWithoutActionOrAlertClient, } = createMockServerWithoutActionOrAlertClientDecoration(); @@ -60,7 +60,7 @@ describe('read_signals', () => { }); describe('validation', () => { - it('returns 404 if given a non-existent id', async () => { + test('returns 404 if given a non-existent id', async () => { alertsClient.find.mockResolvedValue(getFindResult()); alertsClient.get.mockResolvedValue(getResult()); alertsClient.delete.mockResolvedValue({}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.test.ts new file mode 100644 index 0000000000000..456a79efe874a --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.test.ts @@ -0,0 +1,1195 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createSignalsSchema, updateSignalSchema, findSignalsSchema } from './schemas'; +import { SignalAlertParamsRest, FindParamsRest } from '../alerts/types'; + +describe('update_signals', () => { + describe('create signals schema', () => { + test('empty objects do not validate', () => { + expect(createSignalsSchema.validate>({}).error).toBeTruthy(); + }); + + test('made up values do not validate', () => { + expect( + createSignalsSchema.validate>({ + madeUp: 'hi', + }).error + ).toBeTruthy(); + }); + + test('[id] does not validate', () => { + expect( + createSignalsSchema.validate>({ + id: 'rule-1', + }).error + ).toBeTruthy(); + }); + + test('[id, description] does not validate', () => { + expect( + createSignalsSchema.validate>({ + id: 'rule-1', + description: 'some description', + }).error + ).toBeTruthy(); + }); + + test('[id, description, from] does not validate', () => { + expect( + createSignalsSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + }).error + ).toBeTruthy(); + }); + + test('[id, description, from, to] does not validate', () => { + expect( + createSignalsSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + }).error + ).toBeTruthy(); + }); + + test('[id, description, from, to, name] does not validate', () => { + expect( + createSignalsSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + }).error + ).toBeTruthy(); + }); + + test('[id, description, from, to, name, severity] does not validate', () => { + expect( + createSignalsSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'severity', + }).error + ).toBeTruthy(); + }); + + test('[id, description, from, to, name, severity, type] does not validate', () => { + expect( + createSignalsSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'severity', + type: 'query', + }).error + ).toBeTruthy(); + }); + + test('[id, description, from, to, name, severity, type, interval] does not validate', () => { + expect( + createSignalsSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + }).error + ).toBeTruthy(); + }); + + test('[id, description, from, to, name, severity, type, interval, index] does not validate', () => { + expect( + createSignalsSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'severity', + type: 'query', + interval: '5m', + index: ['index-1'], + }).error + ).toBeTruthy(); + }); + + test('[id, description, from, to, name, severity, type, query, index, interval] does not validate', () => { + expect( + createSignalsSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'severity', + type: 'query', + query: 'some query', + index: ['index-1'], + interval: '5m', + }).error + ).toBeTruthy(); + }); + + test('[id, description, from, to, index, name, severity, interval, type, query, language] does validate', () => { + expect( + createSignalsSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + query: 'some query', + language: 'kuery', + }).error + ).toBeFalsy(); + }); + + test('[id, description, from, to, index, name, severity, interval, type, filter] does validate', () => { + expect( + createSignalsSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'filter', + filter: {}, + }).error + ).toBeFalsy(); + }); + + test('If filter type is set then filter is required', () => { + expect( + createSignalsSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'filter', + }).error + ).toBeTruthy(); + }); + + test('If filter type is set then query is not allowed', () => { + expect( + createSignalsSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'filter', + filter: {}, + query: 'some query value', + }).error + ).toBeTruthy(); + }); + + test('If filter type is set then language is not allowed', () => { + expect( + createSignalsSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'filter', + filter: {}, + language: 'kuery', + }).error + ).toBeTruthy(); + }); + + test('If filter type is set then filters are not allowed', () => { + expect( + createSignalsSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'filter', + filter: {}, + filters: [], + }).error + ).toBeTruthy(); + }); + + test('allows references to be sent as valid', () => { + expect( + createSignalsSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + }).error + ).toBeFalsy(); + }); + + test('defaults references to an array', () => { + expect( + createSignalsSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + query: 'some-query', + language: 'kuery', + }).value.references + ).toEqual([]); + }); + + test('references cannot be numbers', () => { + expect( + createSignalsSchema.validate< + Partial> & { references: number[] } + >({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + query: 'some-query', + language: 'kuery', + references: [5], + }).error + ).toBeTruthy(); + }); + + test('indexes cannot be numbers', () => { + expect( + createSignalsSchema.validate< + Partial> & { index: number[] } + >({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: [5], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + query: 'some-query', + language: 'kuery', + }).error + ).toBeTruthy(); + }); + + test('defaults interval to 5 min', () => { + expect( + createSignalsSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + type: 'query', + }).value.interval + ).toEqual('5m'); + }); + + test('defaults max signals to 100', () => { + expect( + createSignalsSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + }).value.max_signals + ).toEqual(100); + }); + + test('filter and filters cannot exist together', () => { + expect( + createSignalsSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + filter: {}, + filters: [], + }).error + ).toBeTruthy(); + }); + + test('saved_id is required when type is saved_query and will not validate without out', () => { + expect( + createSignalsSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'saved_query', + }).error + ).toBeTruthy(); + }); + + test('saved_id is required when type is saved_query and validates with it', () => { + expect( + createSignalsSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'saved_query', + saved_id: 'some id', + }).error + ).toBeFalsy(); + }); + + test('saved_query type cannot have filters with it', () => { + expect( + createSignalsSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'saved_query', + saved_id: 'some id', + filters: [], + }).error + ).toBeTruthy(); + }); + + test('saved_query type cannot have filter with it', () => { + expect( + createSignalsSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'saved_query', + saved_id: 'some id', + filter: {}, + }).error + ).toBeTruthy(); + }); + + test('language validates with kuery', () => { + expect( + createSignalsSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + }).error + ).toBeFalsy(); + }); + + test('language validates with lucene', () => { + expect( + createSignalsSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'lucene', + }).error + ).toBeFalsy(); + }); + + test('language does not validate with something made up', () => { + expect( + createSignalsSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'something-made-up', + }).error + ).toBeTruthy(); + }); + + test('max_signals cannot be negative', () => { + expect( + createSignalsSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: -1, + }).error + ).toBeTruthy(); + }); + + test('max_signals cannot be zero', () => { + expect( + createSignalsSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 0, + }).error + ).toBeTruthy(); + }); + + test('max_signals can be 1', () => { + expect( + createSignalsSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + }).error + ).toBeFalsy(); + }); + }); + + describe('update signals schema', () => { + test('empty objects do validate', () => { + expect(updateSignalSchema.validate>({}).error).toBeFalsy(); + }); + + test('made up values do not validate', () => { + expect( + updateSignalSchema.validate>({ + madeUp: 'hi', + }).error + ).toBeTruthy(); + }); + + test('[id] does validate', () => { + expect( + updateSignalSchema.validate>({ + id: 'rule-1', + }).error + ).toBeFalsy(); + }); + + test('[id, description] does validate', () => { + expect( + updateSignalSchema.validate>({ + id: 'rule-1', + description: 'some description', + }).error + ).toBeFalsy(); + }); + + test('[id, description, from] does validate', () => { + expect( + updateSignalSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + }).error + ).toBeFalsy(); + }); + + test('[id, description, from, to] does validate', () => { + expect( + updateSignalSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + }).error + ).toBeFalsy(); + }); + + test('[id, description, from, to, name] does validate', () => { + expect( + updateSignalSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + }).error + ).toBeFalsy(); + }); + + test('[id, description, from, to, name, severity] does validate', () => { + expect( + updateSignalSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'severity', + }).error + ).toBeFalsy(); + }); + + test('[id, description, from, to, name, severity, type] does validate', () => { + expect( + updateSignalSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'severity', + type: 'query', + }).error + ).toBeFalsy(); + }); + + test('[id, description, from, to, name, severity, type, interval] does validate', () => { + expect( + updateSignalSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + }).error + ).toBeFalsy(); + }); + + test('[id, description, from, to, index, name, severity, interval, type] does validate', () => { + expect( + updateSignalSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + }).error + ).toBeFalsy(); + }); + + test('[id, description, from, to, index, name, severity, interval, type, query] does validate', () => { + expect( + updateSignalSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + query: 'some query', + }).error + ).toBeFalsy(); + }); + + test('[id, description, from, to, index, name, severity, interval, type, query, language] does validate', () => { + expect( + updateSignalSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + query: 'some query', + language: 'kuery', + }).error + ).toBeFalsy(); + }); + + test('[id, description, from, to, index, name, severity, type, filter] does validate', () => { + expect( + updateSignalSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'filter', + filter: {}, + }).error + ).toBeFalsy(); + }); + + test('If filter type is set then filter is still not required', () => { + expect( + updateSignalSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'filter', + }).error + ).toBeFalsy(); + }); + + test('If filter type is set then query is not allowed', () => { + expect( + updateSignalSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'filter', + filter: {}, + query: 'some query value', + }).error + ).toBeTruthy(); + }); + + test('If filter type is set then language is not allowed', () => { + expect( + updateSignalSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'filter', + filter: {}, + language: 'kuery', + }).error + ).toBeTruthy(); + }); + + test('If filter type is set then filters are not allowed', () => { + expect( + updateSignalSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'filter', + filter: {}, + filters: [], + }).error + ).toBeTruthy(); + }); + + test('allows references to be sent as a valid value to update with', () => { + expect( + updateSignalSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + }).error + ).toBeFalsy(); + }); + + test('does not default references to an array', () => { + expect( + updateSignalSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + query: 'some-query', + language: 'kuery', + }).value.references + ).toEqual(undefined); + }); + + test('does not default interval', () => { + expect( + updateSignalSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + type: 'query', + }).value.interval + ).toEqual(undefined); + }); + + test('does not default max signal', () => { + expect( + updateSignalSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + }).value.max_signals + ).toEqual(undefined); + }); + + test('references cannot be numbers', () => { + expect( + updateSignalSchema.validate< + Partial> & { references: number[] } + >({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + query: 'some-query', + language: 'kuery', + references: [5], + }).error + ).toBeTruthy(); + }); + + test('indexes cannot be numbers', () => { + expect( + updateSignalSchema.validate< + Partial> & { index: number[] } + >({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: [5], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + query: 'some-query', + language: 'kuery', + }).error + ).toBeTruthy(); + }); + + test('filter and filters cannot exist together', () => { + expect( + updateSignalSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + filter: {}, + filters: [], + }).error + ).toBeTruthy(); + }); + + test('saved_id is not required when type is saved_query and will validate without it', () => { + expect( + updateSignalSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'saved_query', + }).error + ).toBeFalsy(); + }); + + test('saved_id validates with saved_query', () => { + expect( + updateSignalSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'saved_query', + saved_id: 'some id', + }).error + ).toBeFalsy(); + }); + + test('saved_query type cannot have filters with it', () => { + expect( + updateSignalSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'saved_query', + saved_id: 'some id', + filters: [], + }).error + ).toBeTruthy(); + }); + + test('saved_query type cannot have filter with it', () => { + expect( + updateSignalSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'saved_query', + saved_id: 'some id', + filter: {}, + }).error + ).toBeTruthy(); + }); + + test('language validates with kuery', () => { + expect( + updateSignalSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + }).error + ).toBeFalsy(); + }); + + test('language validates with lucene', () => { + expect( + updateSignalSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'lucene', + }).error + ).toBeFalsy(); + }); + + test('language does not validate with something made up', () => { + expect( + updateSignalSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'something-made-up', + }).error + ).toBeTruthy(); + }); + + test('max_signals cannot be negative', () => { + expect( + updateSignalSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: -1, + }).error + ).toBeTruthy(); + }); + + test('max_signals cannot be zero', () => { + expect( + updateSignalSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 0, + }).error + ).toBeTruthy(); + }); + + test('max_signals can be 1', () => { + expect( + updateSignalSchema.validate>({ + id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'severity', + interval: '5m', + type: 'query', + references: ['index-1'], + query: 'some query', + language: 'kuery', + max_signals: 1, + }).error + ).toBeFalsy(); + }); + }); + + describe('find signals schema', () => { + test('empty objects do validate', () => { + expect(findSignalsSchema.validate>({}).error).toBeFalsy(); + }); + + test('all values validate', () => { + expect( + findSignalsSchema.validate>({ + per_page: 5, + page: 1, + sort_field: 'some field', + fields: ['field 1', 'field 2'], + }).error + ).toBeFalsy(); + }); + + test('made up parameters do not validate', () => { + expect( + findSignalsSchema.validate>({ + madeUp: 'hi', + }).error + ).toBeTruthy(); + }); + + test('per_page validates', () => { + expect( + findSignalsSchema.validate>({ per_page: 5 }).error + ).toBeFalsy(); + }); + + test('page validates', () => { + expect(findSignalsSchema.validate>({ page: 5 }).error).toBeFalsy(); + }); + + test('sort_field validates', () => { + expect( + findSignalsSchema.validate>({ sort_field: 'some value' }).error + ).toBeFalsy(); + }); + + test('fields validates with a string', () => { + expect( + findSignalsSchema.validate>({ fields: ['some value'] }).error + ).toBeFalsy(); + }); + + test('fields validates with multiple strings', () => { + expect( + findSignalsSchema.validate>({ + fields: ['some value 1', 'some value 2'], + }).error + ).toBeFalsy(); + }); + + test('fields does not validate with a number', () => { + expect( + findSignalsSchema.validate> & { fields: number[] }>({ + fields: [5], + }).error + ).toBeTruthy(); + }); + + test('per page has a default of 20', () => { + expect(findSignalsSchema.validate>({}).value.per_page).toEqual(20); + }); + + test('page has a default of 1', () => { + expect(findSignalsSchema.validate>({}).value.page).toEqual(1); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.ts new file mode 100644 index 0000000000000..374105331672f --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.ts @@ -0,0 +1,104 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Joi from 'joi'; + +/* eslint-disable @typescript-eslint/camelcase */ +const description = Joi.string(); +const enabled = Joi.boolean(); +const filter = Joi.object(); +const filters = Joi.array(); +const from = Joi.string(); +const id = Joi.string(); +const index = Joi.array() + .items(Joi.string()) + .single(); +const interval = Joi.string(); +const query = Joi.string(); +const language = Joi.string().valid('kuery', 'lucene'); +const saved_id = Joi.string(); +const max_signals = Joi.number().greater(0); +const name = Joi.string(); +const severity = Joi.string(); +const to = Joi.string(); +const type = Joi.string().valid('filter', 'query', 'saved_query'); +const references = Joi.array() + .items(Joi.string()) + .single(); +const per_page = Joi.number() + .min(0) + .default(20); +const page = Joi.number() + .min(1) + .default(1); +const sort_field = Joi.string(); +const fields = Joi.array() + .items(Joi.string()) + .single(); +/* eslint-enable @typescript-eslint/camelcase */ + +export const createSignalsSchema = Joi.object({ + description: description.required(), + enabled: enabled.default(true), + filter: filter.when('type', { is: 'filter', then: Joi.required(), otherwise: Joi.forbidden() }), + filters: filters.when('type', { is: 'query', then: Joi.optional(), otherwise: Joi.forbidden() }), + from: from.required(), + id: id.required(), + index: index.required(), + interval: interval.default('5m'), + query: query.when('type', { is: 'query', then: Joi.required(), otherwise: Joi.forbidden() }), + language: language.when('type', { + is: 'query', + then: Joi.required(), + otherwise: Joi.forbidden(), + }), + saved_id: saved_id.when('type', { + is: 'saved_query', + then: Joi.required(), + otherwise: Joi.forbidden(), + }), + max_signals: max_signals.default(100), + name: name.required(), + severity: severity.required(), + to: to.required(), + type: type.required(), + references: references.default([]), +}); + +export const updateSignalSchema = Joi.object({ + description, + enabled, + filter: filter.when('type', { is: 'filter', then: Joi.optional(), otherwise: Joi.forbidden() }), + filters: filters.when('type', { is: 'query', then: Joi.optional(), otherwise: Joi.forbidden() }), + from, + id, + index, + interval, + query: query.when('type', { is: 'query', then: Joi.optional(), otherwise: Joi.forbidden() }), + language: language.when('type', { + is: 'query', + then: Joi.optional(), + otherwise: Joi.forbidden(), + }), + saved_id: saved_id.when('type', { + is: 'saved_query', + then: Joi.optional(), + otherwise: Joi.forbidden(), + }), + max_signals, + name, + severity, + to, + type, + references, +}); + +export const findSignalsSchema = Joi.object({ + per_page, + page, + sort_field, + fields, +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_signals_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_signals_route.test.ts index 05c6528337608..c553a8bd40973 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_signals_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_signals_route.test.ts @@ -19,6 +19,7 @@ import { updateActionResult, updateAlertResult, getUpdateRequest, + typicalPayload, } from './__mocks__/request_responses'; describe('update_signals', () => { @@ -31,7 +32,7 @@ describe('update_signals', () => { }); describe('status codes with actionClient and alertClient', () => { - it('returns 200 when deleting a single signal with a valid actionClient and alertClient', async () => { + test('returns 200 when updating a single signal with a valid actionClient and alertClient', async () => { alertsClient.find.mockResolvedValue(getFindResult()); alertsClient.get.mockResolvedValue(getResult()); actionsClient.update.mockResolvedValue(updateActionResult()); @@ -40,21 +41,21 @@ describe('update_signals', () => { expect(statusCode).toBe(200); }); - it('returns 404 if actionClient is not available on the route', async () => { + test('returns 404 if actionClient is not available on the route', async () => { const { serverWithoutActionClient } = createMockServerWithoutActionClientDecoration(); updateSignalsRoute(serverWithoutActionClient); const { statusCode } = await serverWithoutActionClient.inject(getUpdateRequest()); expect(statusCode).toBe(404); }); - it('returns 404 if alertClient is not available on the route', async () => { + test('returns 404 if alertClient is not available on the route', async () => { const { serverWithoutAlertClient } = createMockServerWithoutAlertClientDecoration(); updateSignalsRoute(serverWithoutAlertClient); const { statusCode } = await serverWithoutAlertClient.inject(getUpdateRequest()); expect(statusCode).toBe(404); }); - it('returns 404 if alertClient and actionClient are both not available on the route', async () => { + test('returns 404 if alertClient and actionClient are both not available on the route', async () => { const { serverWithoutActionOrAlertClient, } = createMockServerWithoutActionOrAlertClientDecoration(); @@ -65,124 +66,86 @@ describe('update_signals', () => { }); describe('validation', () => { - it('returns 400 if id is not given in either the body or the url', async () => { + test('returns 400 if id is not given in either the body or the url', async () => { alertsClient.find.mockResolvedValue(getFindResult()); alertsClient.get.mockResolvedValue(getResult()); + const { id, ...noId } = typicalPayload(); const request: ServerInjectOptions = { method: 'PUT', url: '/api/siem/signals', payload: { - // missing id should throw a 400 - description: 'Detecting root and admin users', - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - interval: '5m', - name: 'Detect Root/Admin Users', - severity: 'high', - type: 'kql', - from: 'now-6m', - to: 'now', - kql: 'user.name: root or user.name: admin', + payload: noId, }, }; const { statusCode } = await server.inject(request); expect(statusCode).toBe(400); }); - it('returns 200 if type is kql', async () => { + test('returns 200 if type is query', async () => { alertsClient.find.mockResolvedValue(getFindResult()); alertsClient.get.mockResolvedValue(getResult()); actionsClient.update.mockResolvedValue(updateActionResult()); alertsClient.update.mockResolvedValue(updateAlertResult()); + const { type, ...noType } = typicalPayload(); const request: ServerInjectOptions = { method: 'PUT', url: '/api/siem/signals', payload: { - id: 'rule-1', - description: 'Detecting root and admin users', - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - interval: '5m', - name: 'Detect Root/Admin Users', - severity: 'high', - type: 'kql', - from: 'now-6m', - to: 'now', - kql: 'user.name: root or user.name: admin', + ...noType, + type: 'query', }, }; const { statusCode } = await server.inject(request); expect(statusCode).toBe(200); }); - it('returns 200 if type is filter', async () => { + test('returns 200 if type is filter', async () => { alertsClient.find.mockResolvedValue(getFindResult()); alertsClient.get.mockResolvedValue(getResult()); actionsClient.update.mockResolvedValue(updateActionResult()); alertsClient.update.mockResolvedValue(updateAlertResult()); + const { language, query, type, ...noType } = typicalPayload(); const request: ServerInjectOptions = { method: 'PUT', url: '/api/siem/signals', payload: { - id: 'rule-1', - description: 'Detecting root and admin users', - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - interval: '5m', - name: 'Detect Root/Admin Users', - severity: 'high', + ...noType, type: 'filter', - from: 'now-6m', - to: 'now', - kql: 'user.name: root or user.name: admin', }, }; const { statusCode } = await server.inject(request); expect(statusCode).toBe(200); }); - it('returns 400 if type is not filter or kql', async () => { + test('returns 400 if type is not filter or kql', async () => { alertsClient.find.mockResolvedValue(getFindResult()); alertsClient.get.mockResolvedValue(getResult()); actionsClient.update.mockResolvedValue(updateActionResult()); alertsClient.update.mockResolvedValue(updateAlertResult()); + const { type, ...noType } = typicalPayload(); const request: ServerInjectOptions = { method: 'PUT', url: '/api/siem/signals', payload: { - id: 'rule-1', - description: 'Detecting root and admin users', - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - interval: '5m', - name: 'Detect Root/Admin Users', - severity: 'high', - type: 'something-made-up', // This is a made up type that causes the 400 - from: 'now-6m', - to: 'now', - kql: 'user.name: root or user.name: admin', + ...noType, + type: 'something-made-up', }, }; const { statusCode } = await server.inject(request); expect(statusCode).toBe(400); }); - it('returns 200 if id is given in the url but not the payload', async () => { + test('returns 200 if id is given in the url but not the payload', async () => { alertsClient.find.mockResolvedValue(getFindResult()); alertsClient.get.mockResolvedValue(getResult()); actionsClient.update.mockResolvedValue(updateActionResult()); alertsClient.update.mockResolvedValue(updateAlertResult()); + // missing id should throw a 400 + const { id, ...noId } = typicalPayload(); const request: ServerInjectOptions = { method: 'PUT', url: '/api/siem/signals/rule-1', - payload: { - // missing id should throw a 400 - description: 'Detecting root and admin users', - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - interval: '5m', - name: 'Detect Root/Admin Users', - severity: 'high', - type: 'kql', - from: 'now-6m', - to: 'now', - kql: 'user.name: root or user.name: admin', - }, + payload: noId, }; const { statusCode } = await server.inject(request); expect(statusCode).toBe(200); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_signals_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_signals_route.ts index df4f860e19561..e11f566f9720c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_signals_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/update_signals_route.ts @@ -9,6 +9,7 @@ import Joi from 'joi'; import { isFunction } from 'lodash/fp'; import { updateSignal } from '../alerts/update_signals'; import { SignalsRequest } from '../alerts/types'; +import { updateSignalSchema } from './schemas'; export const createUpdateSignalsRoute: Hapi.ServerRoute = { method: 'PUT', @@ -26,22 +27,7 @@ export const createUpdateSignalsRoute: Hapi.ServerRoute = { otherwise: Joi.string().required(), }), }, - payload: Joi.object({ - description: Joi.string(), - enabled: Joi.boolean(), - filter: Joi.object(), - from: Joi.string(), - id: Joi.string(), - index: Joi.array(), - interval: Joi.string(), - kql: Joi.string(), - max_signals: Joi.number().default(100), - name: Joi.string(), - severity: Joi.string(), - to: Joi.string(), - type: Joi.string().valid('filter', 'kql'), - references: Joi.array().default([]), - }).nand('filter', 'kql'), + payload: updateSignalSchema, }, }, async handler(request: SignalsRequest, headers) { @@ -49,8 +35,12 @@ export const createUpdateSignalsRoute: Hapi.ServerRoute = { description, enabled, filter, - kql, from, + query, + language, + // eslint-disable-next-line @typescript-eslint/camelcase + saved_id: savedId, + filters, id, index, interval, @@ -77,10 +67,13 @@ export const createUpdateSignalsRoute: Hapi.ServerRoute = { enabled, filter, from, + query, + language, + savedId, + filters, id: request.params.id ? request.params.id : id, index, interval, - kql, maxSignals, name, severity, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_x_signals.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_x_signals.sh index 27f6a40a16df7..4736fbeda3cf4 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_x_signals.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_x_signals.sh @@ -28,10 +28,11 @@ do { \"interval\": \"24h\", \"name\": \"Detect Root/Admin Users\", \"severity\": \"high\", - \"type\": \"kql\", + \"type\": \"query\", \"from\": \"now-6m\", \"to\": \"now\", - \"kql\": \"user.name: root or user.name: admin\" + \"query\": \"user.name: root or user.name: admin\" + \"language\": \"kuery\" }" \ | jq .; } & diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_1.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_1.json index 7bbe7ab163460..0b6d222451303 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_1.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_1.json @@ -5,9 +5,10 @@ "interval": "5m", "name": "Detect Root/Admin Users", "severity": "high", - "type": "kql", + "type": "query", "from": "now-6m", "to": "now", - "kql": "user.name: root or user.name: admin", + "query": "user.name: root or user.name: admin", + "language": "kuery", "references": ["http://www.example.com", "https://ww.example.com"] } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_2.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_2.json index efbba4e0e46f5..ad154e2904542 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_2.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_2.json @@ -5,8 +5,9 @@ "interval": "24h", "name": "Detect Root/Admin Users over a long period of time", "severity": "high", - "type": "kql", + "type": "query", "from": "now-1y", "to": "now", - "kql": "user.name: root or user.name: admin" + "query": "user.name: root or user.name: admin", + "language": "kuery" } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_3.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_3.json index db4e5174bc757..be98c7757c1e2 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_3.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_3.json @@ -1,12 +1,13 @@ { "id": "rule-3", - "description": "Detecting root and admin users", + "description": "Detecting root and admin users as an empty set", "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "5m", "name": "Detect Root/Admin Users", "severity": "high", - "type": "kql", + "type": "query", "from": "now-16y", "to": "now-15y", - "kql": "user.name: root or user.name: admin" + "query": "user.name: root or user.name: admin", + "language": "kuery" } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_4.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_4.json new file mode 100644 index 0000000000000..3c917af93fca8 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_4.json @@ -0,0 +1,14 @@ +{ + "id": "rule-4", + "description": "Detecting root and admin users with lucene", + "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], + "interval": "5m", + "name": "Detect Root/Admin Users", + "severity": "high", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "user.name: root or user.name: admin", + "language": "lucene", + "references": ["http://www.example.com", "https://ww.example.com"] +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_5.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_5.json new file mode 100644 index 0000000000000..63728186b8f12 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_5.json @@ -0,0 +1,27 @@ +{ + "id": "rule-5", + "description": "Detecting root and admin users over 24 hours on windows", + "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], + "interval": "5m", + "name": "Detect Root/Admin Users", + "severity": "high", + "type": "query", + "from": "now-24h", + "to": "now", + "query": "user.name: root or user.name: admin", + "language": "kuery", + "filters": [ + { + "query": { + "match_phrase": { + "host.name": "siem-windows" + } + } + }, + { + "exists": { + "field": "host.hostname" + } + } + ] +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_6.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_6.json new file mode 100644 index 0000000000000..58aefe12fb2d3 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_6.json @@ -0,0 +1,51 @@ +{ + "id": "rule-6", + "description": "Detecting root and admin users", + "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], + "interval": "5m", + "name": "Detect Root/Admin Users", + "severity": "high", + "type": "query", + "from": "now-24h", + "to": "now", + "query": "user.name: root or user.name: admin", + "language": "kuery", + "filters": [ + { + "$state": { + "store": "appState" + }, + "meta": { + "alias": "custom label here", + "disabled": false, + "key": "host.name", + "negate": false, + "params": { + "query": "siem-windows" + }, + "type": "phrase" + }, + "query": { + "match_phrase": { + "host.name": "siem-windows" + } + } + }, + { + "exists": { + "field": "host.hostname" + }, + "meta": { + "type": "exists", + "disabled": false, + "negate": false, + "alias": "has a hostname", + "key": "host.hostname", + "value": "exists" + }, + "$state": { + "store": "appState" + } + } + ] +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_7.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_7.json new file mode 100644 index 0000000000000..ac0c41dc3d215 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_7.json @@ -0,0 +1,51 @@ +{ + "id": "rule-7", + "description": "Detecting root and admin users", + "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], + "interval": "5m", + "name": "Detect Root/Admin Users", + "severity": "high", + "type": "query", + "from": "now-24h", + "to": "now", + "query": "user.name: root or user.name: admin", + "language": "lucene", + "filters": [ + { + "$state": { + "store": "appState" + }, + "meta": { + "alias": "custom label here", + "disabled": false, + "key": "host.name", + "negate": false, + "params": { + "query": "siem-windows" + }, + "type": "phrase" + }, + "query": { + "match_phrase": { + "host.name": "siem-windows" + } + } + }, + { + "exists": { + "field": "host.hostname" + }, + "meta": { + "type": "exists", + "disabled": false, + "negate": false, + "alias": "has a hostname", + "key": "host.hostname", + "value": "exists" + }, + "$state": { + "store": "appState" + } + } + ] +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_8.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_8.json new file mode 100644 index 0000000000000..66f308fc5e2ff --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_8.json @@ -0,0 +1,15 @@ +{ + "id": "rule-8", + "description": "Detecting root and admin users", + "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], + "interval": "5m", + "name": "Detect Root/Admin Users", + "severity": "high", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "user.name: root or user.name: admin", + "language": "kuery", + "enabled": false, + "references": ["http://www.example.com", "https://ww.example.com"] +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_filter_9998.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_filter_9998.json new file mode 100644 index 0000000000000..fc5f08234368d --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_filter_9998.json @@ -0,0 +1,46 @@ +{ + "id": "rule-9999", + "description": "Detecting root and admin users", + "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], + "interval": "5m", + "name": "Detect Root/Admin Users", + "severity": "high", + "type": "filter", + "from": "now-6m", + "to": "now", + "filter": { + "bool": { + "must": [], + "filter": [ + { + "bool": { + "should": [ + { + "match_phrase": { + "host.name": "siem-windows" + } + } + ], + "minimum_should_match": 1 + } + }, + { + "match_phrase": { + "winlog.event_id": { + "query": "100" + } + } + }, + { + "match_phrase": { + "agent.hostname": { + "query": "siem-windows" + } + } + } + ], + "should": [], + "must_not": [] + } + } +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_1.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_1.json new file mode 100644 index 0000000000000..cd1a6efa73ad0 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_1.json @@ -0,0 +1,12 @@ +{ + "id": "saved-query-1", + "description": "Detecting root and admin users", + "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], + "interval": "5m", + "name": "Detect Root/Admin Users", + "severity": "high", + "type": "saved_query", + "from": "now-6m", + "to": "now", + "saved_id": "Test Query From SIEM" +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_update_1.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_update_1.json index cad577eeefdc2..71d79903a01db 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_update_1.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_update_1.json @@ -1,13 +1,14 @@ { "id": "rule-1", - "description": "Only watch winlogbeat users", - "index": ["winlogbeat-*"], - "interval": "9m", - "name": "Just watch other winlogbeat users", - "severity": "low", - "enabled": false, - "type": "filter", - "from": "now-5d", - "to": "now-1d", - "kql": "user.name: something_else" + "description": "Detecting root and admin users", + "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], + "interval": "5m", + "name": "Detect Root/Admin Users", + "severity": "high", + "type": "query", + "from": "now-6m", + "to": "now", + "query": "user.name: root or user.name: admin", + "language": "kuery", + "references": [] } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/watch_longmont_3.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/watch_longmont.json similarity index 67% rename from x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/watch_longmont_3.json rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/watch_longmont.json index 1ae71b0a4aa17..2f5457c352712 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/watch_longmont_3.json +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/watch_longmont.json @@ -1,12 +1,13 @@ { - "id": "rule-3", + "id": "rule-longmont", "description": "Detect Longmont activity", "index": ["auditbeat-*", "filebeat-*", "packetbeat-*", "winlogbeat-*"], "interval": "24h", "name": "Detect Longmont activity", "severity": "high", - "type": "kql", + "type": "query", "from": "now-1y", "to": "now", - "kql": "source.as.organization.name: \"Longmont Power & Communications\"" + "query": "user.name: root or user.name: admin", + "language": "kuery" }