From 7a755d0e9282679f5d87572ab4907aab0c346d10 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Mon, 28 Oct 2019 19:13:52 -0600 Subject: [PATCH] [SIEM] [Detection Engine] Adds filtering abilities to the KQL REST API (#49451) (#49583) ## Summary * Removes the older beginner KQL type of signal creation in favor of newer version with filtering * Adds ability to create KQL or lucene queries that will work with the UI filters * UI state with the filters are now savable to re-hydrate UI's on the front end * Adds `saved_id` ability so the UI can tether dynamic saved queries with signals * Changed `it` to `test` as `it` is not the alias we use for tests * Updated script which converts older saved searches to work with newer mechanism * Fixed script to accept proper ndjson lines * Adds validation unit tests for the endpoint * Increases validation strictness of the endpoints * Adds more data scripts for testing scenarios * https://github.com/elastic/kibana/issues/47013 ## Testing * Run `./hard_reset.sh` script * Test with both algorithms through this toggle before starting kibana: `export USE_REINDEX_API=true` * Convert older saved searches to compatible new query filters by running: `./convert_saved_search_to_signals.sh ~/projects/saved_searches /tmp/signals` * Post them`./post_signal.sh /tmp/signals/*.json` * Hard reset again * Test smaller set of signals and REST endpoints using the typical scripts of: ```sh ./post_signal.sh ./read_signal.sh ./find_signals.sh ./update_signal.sh ./delete_signal.sh ``` or test using POSTMAN, etc... If you want to test validation. If you see any validation issues let me know as I have validation testing files and can easily fix them add another unit test to the growing large collection we have now. Change in your advanced settings of SIEM to use your signals index you configured for verification that the signals show up. ### Checklist Use ~~strikethroughs~~ to remove checklist items you don't feel are applicable to this PR. ~~- [ ] This was checked for cross-browser compatibility, [including a check against IE11](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility)~~ ~~- [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md)~~ ~~- [ ] [Documentation](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#writing-documentation) was added for features that require explanation or tutorials~~ - [x] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios ~~- [ ] This was checked for [keyboard-only and screenreader accessibility](https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Cross_browser_testing/Accessibility#Accessibility_testing_checklist)~~ ### For maintainers ~~- [ ] This was checked for breaking API changes and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process)~~ ~~- [ ] This includes a feature addition or change that requires a release note and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process)~~ --- .../convert_saved_search_to_signals.js | 91 +- .../alerts/build_events_query.ts | 20 +- .../alerts/build_events_reindex.ts | 22 +- .../detection_engine/alerts/create_signals.ts | 25 +- .../alerts/get_filter.test.ts | 438 ++++++ .../lib/detection_engine/alerts/get_filter.ts | 86 ++ .../alerts/signals_alert_type.ts | 28 +- .../lib/detection_engine/alerts/types.ts | 36 +- .../alerts/update_signals.test.ts | 28 +- .../detection_engine/alerts/update_signals.ts | 27 +- .../routes/__mocks__/request_responses.ts | 50 +- ...e.test.ts => create_signals_route.test.ts} | 74 +- .../routes/create_signals_route.ts | 33 +- .../routes/delete_signals_route.test.ts | 10 +- .../routes/find_signals_route.test.ts | 12 +- .../routes/find_signals_route.ts | 17 +- .../routes/read_signals_route.test.ts | 10 +- .../detection_engine/routes/schemas.test.ts | 1195 +++++++++++++++++ .../lib/detection_engine/routes/schemas.ts | 104 ++ .../routes/update_signals_route.test.ts | 83 +- .../routes/update_signals_route.ts | 29 +- .../scripts/post_x_signals.sh | 5 +- .../scripts/signals/root_or_admin_1.json | 5 +- .../scripts/signals/root_or_admin_2.json | 5 +- .../scripts/signals/root_or_admin_3.json | 7 +- .../scripts/signals/root_or_admin_4.json | 14 + .../scripts/signals/root_or_admin_5.json | 27 + .../scripts/signals/root_or_admin_6.json | 51 + .../scripts/signals/root_or_admin_7.json | 51 + .../scripts/signals/root_or_admin_8.json | 15 + .../signals/root_or_admin_filter_9998.json | 46 + .../signals/root_or_admin_saved_query_1.json | 12 + .../signals/root_or_admin_update_1.json | 21 +- ...ch_longmont_3.json => watch_longmont.json} | 7 +- 34 files changed, 2324 insertions(+), 360 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_filter.test.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/alerts/get_filter.ts rename x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/{create_signals.route.test.ts => create_signals_route.test.ts} (65%) create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.test.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_4.json create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_5.json create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_6.json create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_7.json create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_8.json create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_filter_9998.json create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/root_or_admin_saved_query_1.json rename x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/{watch_longmont_3.json => watch_longmont.json} (67%) 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" }