diff --git a/.buildkite/ftr_configs.yml b/.buildkite/ftr_configs.yml index b0bc12488ea71..5abe469587f30 100644 --- a/.buildkite/ftr_configs.yml +++ b/.buildkite/ftr_configs.yml @@ -32,6 +32,7 @@ disabled: - x-pack/test/security_solution_cypress/response_ops_cli_config.ts - x-pack/test/security_solution_cypress/upgrade_config.ts - x-pack/test/security_solution_cypress/visual_config.ts + - x-pack/test/threat_intelligence_cypress/visual_config.ts - x-pack/test/functional_enterprise_search/with_host_configured.config.ts - x-pack/plugins/apm/ftr_e2e/ftr_config_open.ts - x-pack/plugins/apm/ftr_e2e/ftr_config_run.ts diff --git a/.buildkite/scripts/steps/es_snapshots/build.sh b/.buildkite/scripts/steps/es_snapshots/build.sh index 2e5e55c3f97b6..1ae00af473f40 100755 --- a/.buildkite/scripts/steps/es_snapshots/build.sh +++ b/.buildkite/scripts/steps/es_snapshots/build.sh @@ -14,10 +14,6 @@ mkdir -p "$destination" mkdir -p elasticsearch && cd elasticsearch export ELASTICSEARCH_BRANCH="${ELASTICSEARCH_BRANCH:-$BUILDKITE_BRANCH}" -# Until ES renames their master branch to main... -if [[ "$ELASTICSEARCH_BRANCH" == "main" ]]; then - export ELASTICSEARCH_BRANCH="master" -fi if [[ ! -d .git ]]; then git init diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 16f86540a4de2..d1f72aadb1882 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -589,6 +589,12 @@ x-pack/plugins/session_view @elastic/awp-platform x-pack/plugins/security_solution/public/common/components/sessions_viewer @elastic/awp-platform x-pack/plugins/security_solution/public/kubernetes @elastic/awp-platform +## Security Solution sub teams - Protections Experience +x-pack/plugins/threat_intelligence @elastic/protections-experience +x-pack/plugins/security_solution/public/threat_intelligence @elastic/protections-experience +x-pack/test/threat_intelligence_cypress @elastic/protections-experience + + # Security Intelligence And Analytics /x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules @elastic/security-intelligence-analytics diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 041c0cee57359..d9b2c970b6302 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -639,6 +639,10 @@ Documentation: https://www.elastic.co/guide/en/kibana/master/task-manager-produc |Gathers all usage collection, retrieving them from both: OSS and X-Pack plugins. +|{kib-repo}blob/{branch}/x-pack/plugins/threat_intelligence/README.md[threatIntelligence] +|Elastic Threat Intelligence makes it easy to analyze and investigate potential security threats by aggregating data from multiple sources in one place. You’ll be able to view data from all activated threat intelligence feeds and take action. + + |{kib-repo}blob/{branch}/x-pack/plugins/timelines/README.md[timelines] |Timelines is a plugin that provides a grid component with accompanying server side apis to help users identify events of interest and perform root cause analysis within Kibana. diff --git a/packages/analytics/shippers/elastic_v3/server/src/server_shipper.test.ts b/packages/analytics/shippers/elastic_v3/server/src/server_shipper.test.ts index d7e8ee379e528..668574dbb8f1d 100644 --- a/packages/analytics/shippers/elastic_v3/server/src/server_shipper.test.ts +++ b/packages/analytics/shippers/elastic_v3/server/src/server_shipper.test.ts @@ -47,7 +47,7 @@ describe('ElasticV3ServerShipper', () => { initContext ); // eslint-disable-next-line dot-notation - shipper['firstTimeOffline'] = null; + shipper['firstTimeOffline'] = null; // The tests think connectivity is OK initially for easier testing. }); afterEach(() => { @@ -57,7 +57,7 @@ describe('ElasticV3ServerShipper', () => { test('set optIn should update the isOptedIn$ observable', () => { // eslint-disable-next-line dot-notation - const getInternalOptIn = () => shipper['isOptedIn']; + const getInternalOptIn = () => shipper['isOptedIn$'].value; // Initially undefined expect(getInternalOptIn()).toBeUndefined(); @@ -342,97 +342,242 @@ describe('ElasticV3ServerShipper', () => { }) ); - test( - 'connectivity check is run after report failure', - fakeSchedulers(async (advance) => { - fetchMock.mockRejectedValueOnce(new Error('Failed to fetch')); - shipper.reportEvents(events); - shipper.optIn(true); - const counter = firstValueFrom(shipper.telemetryCounter$); - setLastBatchSent(Date.now() - 10 * MINUTES); - advance(10 * MINUTES); - expect(fetchMock).toHaveBeenNthCalledWith( - 1, - 'https://telemetry-staging.elastic.co/v3/send/test-channel', - { - body: '{"timestamp":"2020-01-01T00:00:00.000Z","event_type":"test-event-type","context":{},"properties":{}}\n', - headers: { - 'content-type': 'application/x-ndjson', - 'x-elastic-cluster-id': 'UNKNOWN', - 'x-elastic-stack-version': '1.2.3', - }, - method: 'POST', - query: { debug: true }, - } + describe('Connectivity Checks', () => { + describe('connectivity check when connectivity is confirmed (firstTimeOffline === null)', () => { + test.each([undefined, false, true])('does not run for opt-in %p', (optInValue) => + fakeSchedulers(async (advance) => { + if (optInValue !== undefined) { + shipper.optIn(optInValue); + } + + // From the start, it doesn't check connectivity because already confirmed + expect(fetchMock).not.toHaveBeenCalledWith( + 'https://telemetry-staging.elastic.co/v3/send/test-channel', + { method: 'OPTIONS' } + ); + + // Wait a big time (1 minute should be enough, but for the sake of tests...) + advance(10 * MINUTES); + await nextTick(); + + expect(fetchMock).not.toHaveBeenCalledWith( + 'https://telemetry-staging.elastic.co/v3/send/test-channel', + { method: 'OPTIONS' } + ); + })() ); - await expect(counter).resolves.toMatchInlineSnapshot(` - Object { - "code": "Failed to fetch", - "count": 1, - "event_type": "test-event-type", - "source": "elastic_v3_server", - "type": "failed", - } - `); - fetchMock.mockRejectedValueOnce(new Error('Failed to fetch')); - advance(1 * MINUTES); - await nextTick(); - expect(fetchMock).toHaveBeenNthCalledWith( - 2, - 'https://telemetry-staging.elastic.co/v3/send/test-channel', - { method: 'OPTIONS' } + }); + + describe('connectivity check with initial unknown state of the connectivity', () => { + beforeEach(() => { + // eslint-disable-next-line dot-notation + shipper['firstTimeOffline'] = undefined; // Initial unknown state of the connectivity + }); + + test.each([undefined, false])('does not run for opt-in %p', (optInValue) => + fakeSchedulers(async (advance) => { + if (optInValue !== undefined) { + shipper.optIn(optInValue); + } + + // From the start, it doesn't check connectivity because already confirmed + expect(fetchMock).not.toHaveBeenCalledWith( + 'https://telemetry-staging.elastic.co/v3/send/test-channel', + { method: 'OPTIONS' } + ); + + // Wait a big time (1 minute should be enough, but for the sake of tests...) + advance(10 * MINUTES); + await nextTick(); + + expect(fetchMock).not.toHaveBeenCalledWith( + 'https://telemetry-staging.elastic.co/v3/send/test-channel', + { method: 'OPTIONS' } + ); + })() ); - fetchMock.mockResolvedValueOnce({ ok: false }); - advance(2 * MINUTES); - await nextTick(); - expect(fetchMock).toHaveBeenNthCalledWith( - 3, - 'https://telemetry-staging.elastic.co/v3/send/test-channel', - { method: 'OPTIONS' } + + test('runs as soon as opt-in is set to true', () => { + shipper.optIn(true); + + // From the start, it doesn't check connectivity because opt-in is not true + expect(fetchMock).toHaveBeenNthCalledWith( + 1, + 'https://telemetry-staging.elastic.co/v3/send/test-channel', + { method: 'OPTIONS' } + ); + }); + }); + + describe('connectivity check with the connectivity confirmed to be faulty', () => { + beforeEach(() => { + // eslint-disable-next-line dot-notation + shipper['firstTimeOffline'] = 100; // Failed at some point + }); + + test.each([undefined, false])('does not run for opt-in %p', (optInValue) => + fakeSchedulers(async (advance) => { + if (optInValue !== undefined) { + shipper.optIn(optInValue); + } + + // From the start, it doesn't check connectivity because already confirmed + expect(fetchMock).not.toHaveBeenCalledWith( + 'https://telemetry-staging.elastic.co/v3/send/test-channel', + { method: 'OPTIONS' } + ); + + // Wait a big time (1 minute should be enough, but for the sake of tests...) + advance(10 * MINUTES); + await nextTick(); + + expect(fetchMock).not.toHaveBeenCalledWith( + 'https://telemetry-staging.elastic.co/v3/send/test-channel', + { method: 'OPTIONS' } + ); + })() ); - // let's see the effect of after 24 hours: - shipper.reportEvents(events); - // eslint-disable-next-line dot-notation - expect(shipper['internalQueue'].length).toBe(1); - // eslint-disable-next-line dot-notation - shipper['firstTimeOffline'] = 100; + test('runs as soon as opt-in is set to true', () => { + shipper.optIn(true); - fetchMock.mockResolvedValueOnce({ ok: false }); - advance(4 * MINUTES); - await nextTick(); - expect(fetchMock).toHaveBeenNthCalledWith( - 4, - 'https://telemetry-staging.elastic.co/v3/send/test-channel', - { method: 'OPTIONS' } + // From the start, it doesn't check connectivity because opt-in is not true + expect(fetchMock).toHaveBeenNthCalledWith( + 1, + 'https://telemetry-staging.elastic.co/v3/send/test-channel', + { method: 'OPTIONS' } + ); + }); + }); + + describe('after report failure', () => { + // generate the report failure for each test + beforeEach( + fakeSchedulers(async (advance) => { + fetchMock.mockRejectedValueOnce(new Error('Failed to fetch')); + shipper.reportEvents(events); + shipper.optIn(true); + const counter = firstValueFrom(shipper.telemetryCounter$); + setLastBatchSent(Date.now() - 10 * MINUTES); + advance(10 * MINUTES); + expect(fetchMock).toHaveBeenNthCalledWith( + 1, + 'https://telemetry-staging.elastic.co/v3/send/test-channel', + { + body: '{"timestamp":"2020-01-01T00:00:00.000Z","event_type":"test-event-type","context":{},"properties":{}}\n', + headers: { + 'content-type': 'application/x-ndjson', + 'x-elastic-cluster-id': 'UNKNOWN', + 'x-elastic-stack-version': '1.2.3', + }, + method: 'POST', + query: { debug: true }, + } + ); + await expect(counter).resolves.toMatchInlineSnapshot(` + Object { + "code": "Failed to fetch", + "count": 1, + "event_type": "test-event-type", + "source": "elastic_v3_server", + "type": "failed", + } + `); + }) ); - // eslint-disable-next-line dot-notation - expect(shipper['internalQueue'].length).toBe(0); - // New events are not added to the queue because it's been offline for 24 hours. - shipper.reportEvents(events); - // eslint-disable-next-line dot-notation - expect(shipper['internalQueue'].length).toBe(0); - - // Regains connection - fetchMock.mockResolvedValueOnce({ ok: true }); - advance(8 * MINUTES); - await nextTick(); - expect(fetchMock).toHaveBeenNthCalledWith( - 5, - 'https://telemetry-staging.elastic.co/v3/send/test-channel', - { method: 'OPTIONS' } + test( + 'connectivity check runs periodically', + fakeSchedulers(async (advance) => { + fetchMock.mockRejectedValueOnce(new Error('Failed to fetch')); + advance(1 * MINUTES); + await nextTick(); + expect(fetchMock).toHaveBeenNthCalledWith( + 2, + 'https://telemetry-staging.elastic.co/v3/send/test-channel', + { method: 'OPTIONS' } + ); + fetchMock.mockResolvedValueOnce({ ok: false }); + advance(2 * MINUTES); + await nextTick(); + expect(fetchMock).toHaveBeenNthCalledWith( + 3, + 'https://telemetry-staging.elastic.co/v3/send/test-channel', + { method: 'OPTIONS' } + ); + }) ); - // eslint-disable-next-line dot-notation - expect(shipper['firstTimeOffline']).toBe(null); + }); + + describe('after being offline for longer than 24h', () => { + beforeEach(() => { + shipper.optIn(true); + shipper.reportEvents(events); + // eslint-disable-next-line dot-notation + expect(shipper['internalQueue'].length).toBe(1); + // eslint-disable-next-line dot-notation + shipper['firstTimeOffline'] = 100; + }); - advance(16 * MINUTES); - await nextTick(); - expect(fetchMock).not.toHaveBeenNthCalledWith( - 6, - 'https://telemetry-staging.elastic.co/v3/send/test-channel', - { method: 'OPTIONS' } + test( + 'the following connectivity check clears the queue', + fakeSchedulers(async (advance) => { + fetchMock.mockRejectedValueOnce(new Error('Failed to fetch')); + advance(1 * MINUTES); + await nextTick(); + expect(fetchMock).toHaveBeenNthCalledWith( + 1, + 'https://telemetry-staging.elastic.co/v3/send/test-channel', + { method: 'OPTIONS' } + ); + // eslint-disable-next-line dot-notation + expect(shipper['internalQueue'].length).toBe(0); + }) ); - }) - ); + + test( + 'new events are not added to the queue', + fakeSchedulers(async (advance) => { + fetchMock.mockRejectedValueOnce(new Error('Failed to fetch')); + advance(1 * MINUTES); + await nextTick(); + expect(fetchMock).toHaveBeenNthCalledWith( + 1, + 'https://telemetry-staging.elastic.co/v3/send/test-channel', + { method: 'OPTIONS' } + ); + // eslint-disable-next-line dot-notation + expect(shipper['internalQueue'].length).toBe(0); + + shipper.reportEvents(events); + // eslint-disable-next-line dot-notation + expect(shipper['internalQueue'].length).toBe(0); + }) + ); + + test( + 'regains the connection', + fakeSchedulers(async (advance) => { + fetchMock.mockResolvedValueOnce({ ok: true }); + advance(1 * MINUTES); + await nextTick(); + expect(fetchMock).toHaveBeenNthCalledWith( + 1, + 'https://telemetry-staging.elastic.co/v3/send/test-channel', + { method: 'OPTIONS' } + ); + // eslint-disable-next-line dot-notation + expect(shipper['firstTimeOffline']).toBe(null); + + advance(10 * MINUTES); + await nextTick(); + expect(fetchMock).not.toHaveBeenNthCalledWith( + 2, + 'https://telemetry-staging.elastic.co/v3/send/test-channel', + { method: 'OPTIONS' } + ); + }) + ); + }); + }); }); diff --git a/packages/analytics/shippers/elastic_v3/server/src/server_shipper.ts b/packages/analytics/shippers/elastic_v3/server/src/server_shipper.ts index 1a0b76f13c286..e8cb3c7bff5db 100644 --- a/packages/analytics/shippers/elastic_v3/server/src/server_shipper.ts +++ b/packages/analytics/shippers/elastic_v3/server/src/server_shipper.ts @@ -22,6 +22,8 @@ import { delayWhen, takeUntil, map, + BehaviorSubject, + exhaustMap, } from 'rxjs'; import type { AnalyticsClientInitContext, @@ -30,8 +32,8 @@ import type { IShipper, TelemetryCounter, } from '@kbn/analytics-client'; -import type { ElasticV3ShipperOptions } from '@kbn/analytics-shippers-elastic-v3-common'; import { + type ElasticV3ShipperOptions, buildHeaders, buildUrl, createTelemetryCounterHelper, @@ -62,6 +64,7 @@ export class ElasticV3ServerShipper implements IShipper { private readonly internalQueue: Event[] = []; private readonly shutdown$ = new ReplaySubject(1); + private readonly isOptedIn$ = new BehaviorSubject(undefined); private readonly url: string; @@ -69,7 +72,6 @@ export class ElasticV3ServerShipper implements IShipper { private clusterUuid: string = 'UNKNOWN'; private licenseId?: string; - private isOptedIn?: boolean; /** * Specifies when it went offline: @@ -116,7 +118,7 @@ export class ElasticV3ServerShipper implements IShipper { * @param isOptedIn `true` for resume sending events. `false` to stop. */ public optIn(isOptedIn: boolean) { - this.isOptedIn = isOptedIn; + this.isOptedIn$.next(isOptedIn); if (isOptedIn === false) { this.internalQueue.length = 0; @@ -129,7 +131,7 @@ export class ElasticV3ServerShipper implements IShipper { */ public reportEvents(events: Event[]) { if ( - this.isOptedIn === false || + this.isOptedIn$.value === false || (this.firstTimeOffline && Date.now() - this.firstTimeOffline > 24 * HOUR) ) { return; @@ -157,6 +159,7 @@ export class ElasticV3ServerShipper implements IShipper { public shutdown() { this.shutdown$.next(); this.shutdown$.complete(); + this.isOptedIn$.complete(); } /** @@ -169,11 +172,17 @@ export class ElasticV3ServerShipper implements IShipper { */ private checkConnectivity() { let backoff = 1 * MINUTE; - timer(0, 1 * MINUTE) + merge( + timer(0, 1 * MINUTE), + // Also react to opt-in changes to avoid being stalled for 1 minute for the first connectivity check. + // More details in: https://github.com/elastic/kibana/issues/135647 + this.isOptedIn$ + ) .pipe( takeUntil(this.shutdown$), - filter(() => this.isOptedIn === true && this.firstTimeOffline !== null), - concatMap(async () => { + filter(() => this.isOptedIn$.value === true && this.firstTimeOffline !== null), + // Using exhaustMap here because one request at a time is enough to check the connectivity. + exhaustMap(async () => { const { ok } = await fetch(this.url, { method: 'OPTIONS', }); @@ -215,7 +224,7 @@ export class ElasticV3ServerShipper implements IShipper { ) .pipe( // Only move ahead if it's opted-in and online. - filter(() => this.isOptedIn === true && this.firstTimeOffline === null), + filter(() => this.isOptedIn$.value === true && this.firstTimeOffline === null), // Send the events now if (validations sorted from cheapest to most CPU expensive): // - We are shutting down. @@ -241,6 +250,7 @@ export class ElasticV3ServerShipper implements IShipper { // 2. Skip empty buffers filter((events) => events.length > 0), // 3. Actually send the events + // Using `concatMap` here because we want to send events whenever the emitter says so. Otherwise, it'd skip sending some events. concatMap(async (eventsToSend) => await this.sendEvents(eventsToSend)) ) .subscribe(); diff --git a/packages/kbn-doc-links/src/get_doc_links.ts b/packages/kbn-doc-links/src/get_doc_links.ts index 9f576cd1629de..e98929ce88f03 100644 --- a/packages/kbn-doc-links/src/get_doc_links.ts +++ b/packages/kbn-doc-links/src/get_doc_links.ts @@ -336,13 +336,13 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => { ruleApiOverview: `${SECURITY_SOLUTION_DOCS}rule-api-overview.html`, }, securitySolution: { - trustedApps: `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/trusted-apps-ov.html`, - eventFilters: `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/event-filters.html`, - blocklist: `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/blocklist.html`, + trustedApps: `${SECURITY_SOLUTION_DOCS}trusted-apps-ov.html`, + eventFilters: `${SECURITY_SOLUTION_DOCS}event-filters.html`, + blocklist: `${SECURITY_SOLUTION_DOCS}blocklist.html`, threatIntelInt: `${SECURITY_SOLUTION_DOCS}es-threat-intel-integrations.html`, policyResponseTroubleshooting: { - full_disk_access: `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/deploy-elastic-endpoint.html#enable-fda-endpoint`, - macos_system_ext: `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/deploy-elastic-endpoint.html#system-extension-endpoint`, + full_disk_access: `${SECURITY_SOLUTION_DOCS}deploy-elastic-endpoint.html#enable-fda-endpoint`, + macos_system_ext: `${SECURITY_SOLUTION_DOCS}deploy-elastic-endpoint.html#system-extension-endpoint`, }, }, query: { @@ -653,7 +653,7 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => { rustOverview: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/client/rust-api/${DOC_LINK_VERSION}/overview.html`, }, endpoints: { - troubleshooting: `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/ts-management.html#ts-endpoints`, + troubleshooting: `${SECURITY_SOLUTION_DOCS}ts-management.html#ts-endpoints`, }, legal: { privacyStatement: `${ELASTIC_WEBSITE_URL}legal/privacy-statement`, diff --git a/packages/kbn-doc-links/src/types.ts b/packages/kbn-doc-links/src/types.ts index 8a7a0e788cbc3..bf3132a8c0e94 100644 --- a/packages/kbn-doc-links/src/types.ts +++ b/packages/kbn-doc-links/src/types.ts @@ -249,6 +249,7 @@ export interface DocLinks { full_disk_access: string; macos_system_ext: string; }; + readonly threatIntelInt: string; }; readonly query: { readonly eql: string; diff --git a/packages/kbn-eslint-config/.eslintrc.js b/packages/kbn-eslint-config/.eslintrc.js index 3e52465240800..7eeafb720670d 100644 --- a/packages/kbn-eslint-config/.eslintrc.js +++ b/packages/kbn-eslint-config/.eslintrc.js @@ -108,6 +108,11 @@ module.exports = { to: '@kbn/utility-types-jest', disallowedMessage: `import from @kbn/utility-types-jest instead` }, + { + from: '@kbn/inspector-plugin', + to: '@kbn/inspector-plugin/common', + exact: true, + }, { from: '@kbn/expressions-plugin', to: '@kbn/expressions-plugin/common', diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 174077ea28715..1044cae862fe0 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -58,7 +58,7 @@ pageLoadAssetSize: telemetry: 51957 telemetryManagementSection: 38586 transform: 41007 - triggersActionsUi: 119000 #This is temporary. Check https://github.com/elastic/kibana/pull/130710#issuecomment-1119843458 & https://github.com/elastic/kibana/issues/130728 + triggersActionsUi: 119000 upgradeAssistant: 81241 urlForwarding: 32579 usageCollection: 39762 @@ -132,3 +132,4 @@ pageLoadAssetSize: expressionXY: 36000 kibanaUsageCollection: 16463 kubernetesSecurity: 77234 + threatIntelligence: 29195 diff --git a/packages/kbn-securitysolution-io-ts-types/src/default_csv_array/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/default_csv_array/index.test.ts new file mode 100644 index 0000000000000..1f927565ca626 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-types/src/default_csv_array/index.test.ts @@ -0,0 +1,214 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; +import { defaultCsvArray } from '.'; + +describe('defaultCsvArray', () => { + describe('Creates a schema of an array that works in the following way:', () => { + type TestType = t.TypeOf; + const TestType = t.union( + [t.literal('foo'), t.literal('bar'), t.literal('42'), t.null, t.undefined], + 'TestType' + ); + + const TestCsvArray = defaultCsvArray(TestType); + + describe('Name of the schema', () => { + it('has a default value', () => { + const CsvArray = defaultCsvArray(TestType); + expect(CsvArray.name).toEqual('DefaultCsvArray'); + }); + + it('can be overriden', () => { + const CsvArray = defaultCsvArray(TestType, 'CustomName'); + expect(CsvArray.name).toEqual('CustomName'); + }); + }); + + describe('Validation succeeds', () => { + describe('when input is a single valid string value', () => { + const cases = [{ input: 'foo' }, { input: 'bar' }, { input: '42' }]; + + cases.forEach(({ input }) => { + it(`${input}`, () => { + const decoded = TestCsvArray.decode(input); + const message = pipe(decoded, foldLeftRight); + const expectedOutput = [input]; // note that it's an array after decode + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(expectedOutput); + }); + }); + }); + + describe('when input is an array of valid string values', () => { + const cases = [ + { input: ['foo'] }, + { input: ['foo', 'bar'] }, + { input: ['foo', 'bar', '42'] }, + ]; + + cases.forEach(({ input }) => { + it(`${input}`, () => { + const decoded = TestCsvArray.decode(input); + const message = pipe(decoded, foldLeftRight); + const expectedOutput = input; + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(expectedOutput); + }); + }); + }); + + describe('when input is a string which is a comma-separated array of valid values', () => { + const cases = [ + { + input: 'foo,bar', + expectedOutput: ['foo', 'bar'], + }, + { + input: 'foo,bar,42', + expectedOutput: ['foo', 'bar', '42'], + }, + ]; + + cases.forEach(({ input, expectedOutput }) => { + it(`${input}`, () => { + const decoded = TestCsvArray.decode(input); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(expectedOutput); + }); + }); + }); + }); + + describe('Validation fails', () => { + describe('when input is a single invalid value', () => { + const cases = [ + { + input: 'val', + expectedErrors: ['Invalid value "val" supplied to "DefaultCsvArray"'], + }, + { + input: '5', + expectedErrors: ['Invalid value "5" supplied to "DefaultCsvArray"'], + }, + { + input: 5, + expectedErrors: ['Invalid value "5" supplied to "DefaultCsvArray"'], + }, + { + input: {}, + expectedErrors: ['Invalid value "{}" supplied to "DefaultCsvArray"'], + }, + ]; + + cases.forEach(({ input, expectedErrors }) => { + it(`${input}`, () => { + const decoded = TestCsvArray.decode(input); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(expectedErrors); + expect(message.schema).toEqual({}); + }); + }); + }); + + describe('when input is an array of invalid values', () => { + const cases = [ + { + input: ['value 1', 5], + expectedErrors: [ + 'Invalid value "value 1" supplied to "DefaultCsvArray"', + 'Invalid value "5" supplied to "DefaultCsvArray"', + ], + }, + { + input: ['value 1', 'foo'], + expectedErrors: ['Invalid value "value 1" supplied to "DefaultCsvArray"'], + }, + { + input: ['', 5, {}], + expectedErrors: [ + 'Invalid value "" supplied to "DefaultCsvArray"', + 'Invalid value "5" supplied to "DefaultCsvArray"', + 'Invalid value "{}" supplied to "DefaultCsvArray"', + ], + }, + ]; + + cases.forEach(({ input, expectedErrors }) => { + it(`${input}`, () => { + const decoded = TestCsvArray.decode(input); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(expectedErrors); + expect(message.schema).toEqual({}); + }); + }); + }); + + describe('when input is a string which is a comma-separated array of invalid values', () => { + const cases = [ + { + input: 'value 1,5', + expectedErrors: [ + 'Invalid value "value 1" supplied to "DefaultCsvArray"', + 'Invalid value "5" supplied to "DefaultCsvArray"', + ], + }, + { + input: 'value 1,foo', + expectedErrors: ['Invalid value "value 1" supplied to "DefaultCsvArray"'], + }, + { + input: ',5,{}', + expectedErrors: [ + 'Invalid value "" supplied to "DefaultCsvArray"', + 'Invalid value "5" supplied to "DefaultCsvArray"', + 'Invalid value "{}" supplied to "DefaultCsvArray"', + ], + }, + ]; + + cases.forEach(({ input, expectedErrors }) => { + it(`${input}`, () => { + const decoded = TestCsvArray.decode(input); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(expectedErrors); + expect(message.schema).toEqual({}); + }); + }); + }); + }); + + describe('Validation returns default value (an empty array)', () => { + describe('when input is', () => { + const cases = [{ input: null }, { input: undefined }, { input: '' }, { input: [] }]; + + cases.forEach(({ input }) => { + it(`${input}`, () => { + const decoded = TestCsvArray.decode(input); + const message = pipe(decoded, foldLeftRight); + const expectedOutput: string[] = []; + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(expectedOutput); + }); + }); + }); + }); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-types/src/default_csv_array/index.ts b/packages/kbn-securitysolution-io-ts-types/src/default_csv_array/index.ts new file mode 100644 index 0000000000000..0f9a7b859409b --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-types/src/default_csv_array/index.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; + +/** + * Creates a schema of an array that works in the following way: + * - If input is a CSV string, it will be parsed to an array which will be validated. + * - If input is an array, each item is validated to match `itemSchema`. + * - If input is a single string, it is validated to match `itemSchema`. + * - If input is not specified, the result will be set to [] (empty array): + * - null, undefined, empty string, empty array + * + * In all cases when an input is valid, the resulting decoded value will be an array, + * either an empty one or containing valid items. + * + * @param itemSchema Schema of the array's items. + * @param name (Optional) Name of the resulting schema. + */ +export const defaultCsvArray = ( + itemSchema: t.Type, + name?: string +): t.Type => { + return new t.Type( + name ?? `DefaultCsvArray<${itemSchema.name}>`, + t.array(itemSchema).is, + (input, context): Either => { + if (input == null) { + return t.success([]); + } else if (typeof input === 'string') { + if (input === '') { + return t.success([]); + } else { + return t.array(itemSchema).validate(input.split(','), context); + } + } else { + return t.array(itemSchema).validate(input, context); + } + }, + t.identity + ); +}; diff --git a/packages/kbn-securitysolution-io-ts-types/src/default_value/index.test.ts b/packages/kbn-securitysolution-io-ts-types/src/default_value/index.test.ts new file mode 100644 index 0000000000000..0fc5e7584d944 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-types/src/default_value/index.test.ts @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; +import { defaultValue } from '.'; + +describe('defaultValue', () => { + describe('Creates a schema that sets a default value if the input value is not specified', () => { + type TestType = t.TypeOf; + const TestType = t.union([t.string, t.number, t.null, t.undefined], 'TestType'); + + const DefaultValue = defaultValue(TestType, 42); + + describe('Name of the schema', () => { + it('has a default value', () => { + expect(defaultValue(TestType, 42).name).toEqual('DefaultValue'); + }); + + it('can be overriden', () => { + expect(defaultValue(TestType, 42, 'CustomName').name).toEqual('CustomName'); + }); + }); + + describe('Validation succeeds', () => { + describe('when input is a valid value', () => { + const cases = [ + { input: 'foo' }, + { input: '42' }, + { input: 42 }, + // including all "falsey" values which are not null or undefined + { input: '' }, + { input: 0 }, + ]; + + cases.forEach(({ input }) => { + it(`${input}`, () => { + const decoded = DefaultValue.decode(input); + const message = pipe(decoded, foldLeftRight); + const expectedOutput = input; + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(expectedOutput); + }); + }); + }); + }); + + describe('Validation fails', () => { + describe('when input is an invalid value', () => { + const cases = [ + { + input: {}, + expectedErrors: ['Invalid value "{}" supplied to "DefaultValue"'], + }, + { + input: { foo: 42 }, + expectedErrors: ['Invalid value "{"foo":42}" supplied to "DefaultValue"'], + }, + { + input: [], + expectedErrors: ['Invalid value "[]" supplied to "DefaultValue"'], + }, + { + input: ['foo', 42], + expectedErrors: ['Invalid value "["foo",42]" supplied to "DefaultValue"'], + }, + ]; + + cases.forEach(({ input, expectedErrors }) => { + it(`${input}`, () => { + const decoded = DefaultValue.decode(input); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(expectedErrors); + expect(message.schema).toEqual({}); + }); + }); + }); + }); + + describe('Validation returns specified default value', () => { + describe('when input is', () => { + const cases = [{ input: null }, { input: undefined }]; + + cases.forEach(({ input }) => { + it(`${input}`, () => { + const decoded = DefaultValue.decode(input); + const message = pipe(decoded, foldLeftRight); + const expectedOutput = 42; + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(expectedOutput); + }); + }); + }); + }); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-types/src/default_value/index.ts b/packages/kbn-securitysolution-io-ts-types/src/default_value/index.ts new file mode 100644 index 0000000000000..1785225202820 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-types/src/default_value/index.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import type { Either } from 'fp-ts/lib/Either'; + +/** + * Creates a schema that sets a default value if the input value is not specified. + * + * @param valueSchema Base schema of a value. + * @param value Default value to set. + * @param name (Optional) Name of the resulting schema. + */ +export const defaultValue = ( + valueSchema: t.Type, + value: TValue, + name?: string +): t.Type => { + return new t.Type( + name ?? `DefaultValue<${valueSchema.name}>`, + valueSchema.is, + (input, context): Either => + input == null ? t.success(value) : valueSchema.validate(input, context), + t.identity + ); +}; diff --git a/packages/kbn-securitysolution-io-ts-types/src/index.ts b/packages/kbn-securitysolution-io-ts-types/src/index.ts index 01f9b32ca31af..8d701d6994322 100644 --- a/packages/kbn-securitysolution-io-ts-types/src/index.ts +++ b/packages/kbn-securitysolution-io-ts-types/src/index.ts @@ -9,10 +9,12 @@ export * from './default_array'; export * from './default_boolean_false'; export * from './default_boolean_true'; +export * from './default_csv_array'; export * from './default_empty_string'; export * from './default_string_array'; export * from './default_string_boolean_false'; export * from './default_uuid'; +export * from './default_value'; export * from './default_version_number'; export * from './empty_string_array'; export * from './enumeration'; diff --git a/src/dev/storybook/aliases.ts b/src/dev/storybook/aliases.ts index 7f9c46783412b..89e78848a1366 100644 --- a/src/dev/storybook/aliases.ts +++ b/src/dev/storybook/aliases.ts @@ -39,6 +39,7 @@ export const storybookAliases = { presentation: 'src/plugins/presentation_util/storybook', security_solution: 'x-pack/plugins/security_solution/.storybook', shared_ux: 'packages/kbn-shared-ux-storybook/src/config', + threat_intelligence: 'x-pack/plugins/threat_intelligence/.storybook', ui_actions_enhanced: 'src/plugins/ui_actions_enhanced/.storybook', unified_search: 'src/plugins/unified_search/.storybook', }; diff --git a/src/dev/typescript/projects.ts b/src/dev/typescript/projects.ts index a104b5a171d78..5f34bef2df51a 100644 --- a/src/dev/typescript/projects.ts +++ b/src/dev/typescript/projects.ts @@ -66,21 +66,22 @@ export const PROJECTS = [ createProject('x-pack/plugins/fleet/cypress/tsconfig.json', { name: 'fleet/cypress', }), - createProject('x-pack/plugins/synthetics/e2e/tsconfig.json', { name: 'uptime/synthetics-e2e-tests', disableTypeCheck: true, }), - createProject('x-pack/plugins/ux/e2e/tsconfig.json', { name: 'ux/synthetics-e2e-tests', disableTypeCheck: true, }), - createProject('x-pack/plugins/observability/e2e/tsconfig.json', { name: 'observability/synthetics-e2e-tests', disableTypeCheck: true, }), + createProject('x-pack/plugins/threat_intelligence/cypress/tsconfig.json', { + name: 'threat_intelligence/cypress', + disableTypeCheck: true, + }), // Glob patterns to be all search at once ...findProjects([ diff --git a/src/plugins/discover/public/application/main/components/layout/__stories__/get_layout_props.ts b/src/plugins/discover/public/application/main/components/layout/__stories__/get_layout_props.ts index b0e12b3bcd7d2..7aea5b191a36e 100644 --- a/src/plugins/discover/public/application/main/components/layout/__stories__/get_layout_props.ts +++ b/src/plugins/discover/public/application/main/components/layout/__stories__/get_layout_props.ts @@ -9,7 +9,7 @@ import { DataView, DataViewAttributes, SavedObject } from '@kbn/data-views-plugin/common'; import { SearchSource } from '@kbn/data-plugin/common'; import { BehaviorSubject, Subject } from 'rxjs'; -import { RequestAdapter } from '@kbn/inspector-plugin'; +import { RequestAdapter } from '@kbn/inspector-plugin/common'; import { action } from '@storybook/addon-actions'; import { FetchStatus } from '../../../../types'; import { diff --git a/src/plugins/discover/public/application/main/components/layout/discover_layout.test.tsx b/src/plugins/discover/public/application/main/components/layout/discover_layout.test.tsx index 4aff6e2a78070..e0fc90a83b296 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_layout.test.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_layout.test.tsx @@ -29,7 +29,7 @@ import { } from '../../hooks/use_saved_search'; import { discoverServiceMock } from '../../../../__mocks__/services'; import { FetchStatus } from '../../../types'; -import { RequestAdapter } from '@kbn/inspector-plugin'; +import { RequestAdapter } from '@kbn/inspector-plugin/common'; import { Chart } from '../chart/point_series'; import { DiscoverSidebar } from '../sidebar/discover_sidebar'; import { LocalStorageMock } from '../../../../__mocks__/local_storage_mock'; diff --git a/src/plugins/discover/public/application/main/components/layout/types.ts b/src/plugins/discover/public/application/main/components/layout/types.ts index f381f87c7389d..26bb24c866c78 100644 --- a/src/plugins/discover/public/application/main/components/layout/types.ts +++ b/src/plugins/discover/public/application/main/components/layout/types.ts @@ -10,7 +10,7 @@ import type { Query, TimeRange } from '@kbn/es-query'; import type { SavedObject } from '@kbn/data-plugin/public'; import type { DataView, DataViewAttributes } from '@kbn/data-views-plugin/public'; import { ISearchSource } from '@kbn/data-plugin/public'; -import { RequestAdapter } from '@kbn/inspector-plugin'; +import { RequestAdapter } from '@kbn/inspector-plugin/common'; import { DataTableRecord } from '../../../../types'; import { AppState, GetStateReturn } from '../../services/discover_state'; import { DataRefetch$, SavedSearchData } from '../../hooks/use_saved_search'; diff --git a/src/plugins/discover/public/application/main/utils/fetch_all.test.ts b/src/plugins/discover/public/application/main/utils/fetch_all.test.ts index e33d931c571da..581fe265ea3e8 100644 --- a/src/plugins/discover/public/application/main/utils/fetch_all.test.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_all.test.ts @@ -9,7 +9,7 @@ import { FetchStatus } from '../../types'; import { BehaviorSubject, firstValueFrom, Subject } from 'rxjs'; import { reduce } from 'rxjs/operators'; import { SearchSource } from '@kbn/data-plugin/public'; -import { RequestAdapter } from '@kbn/inspector-plugin'; +import { RequestAdapter } from '@kbn/inspector-plugin/common'; import { savedSearchMock } from '../../../__mocks__/saved_search'; import { ReduxLikeStateContainer } from '@kbn/kibana-utils-plugin/common'; import { AppState } from '../services/discover_state'; diff --git a/src/plugins/discover/public/application/main/utils/fetch_all.ts b/src/plugins/discover/public/application/main/utils/fetch_all.ts index 655027dddbf1e..bf623c3ad07df 100644 --- a/src/plugins/discover/public/application/main/utils/fetch_all.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_all.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ import { DataPublicPluginStart, ISearchSource } from '@kbn/data-plugin/public'; -import { Adapters } from '@kbn/inspector-plugin'; +import { Adapters } from '@kbn/inspector-plugin/common'; import { ReduxLikeStateContainer } from '@kbn/kibana-utils-plugin/common'; import { DataViewType } from '@kbn/data-views-plugin/public'; import { buildDataTableRecord } from '../../../utils/build_data_record'; diff --git a/src/plugins/discover/public/application/main/utils/fetch_chart.test.ts b/src/plugins/discover/public/application/main/utils/fetch_chart.test.ts index 6a5ce76c2e66a..64e203b951190 100644 --- a/src/plugins/discover/public/application/main/utils/fetch_chart.test.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_chart.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ import { of, throwError as throwErrorRx } from 'rxjs'; -import { RequestAdapter } from '@kbn/inspector-plugin'; +import { RequestAdapter } from '@kbn/inspector-plugin/common'; import { savedSearchMockWithTimeField } from '../../../__mocks__/saved_search'; import { fetchChart, updateSearchSource } from './fetch_chart'; import { ReduxLikeStateContainer } from '@kbn/kibana-utils-plugin/common'; diff --git a/src/plugins/discover/public/application/main/utils/fetch_documents.test.ts b/src/plugins/discover/public/application/main/utils/fetch_documents.test.ts index 5796c488dd8fb..2dc34d1a53a23 100644 --- a/src/plugins/discover/public/application/main/utils/fetch_documents.test.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_documents.test.ts @@ -7,7 +7,7 @@ */ import { fetchDocuments } from './fetch_documents'; import { throwError as throwErrorRx, of } from 'rxjs'; -import { RequestAdapter } from '@kbn/inspector-plugin'; +import { RequestAdapter } from '@kbn/inspector-plugin/common'; import { savedSearchMock, savedSearchMockWithTimeField } from '../../../__mocks__/saved_search'; import { discoverServiceMock } from '../../../__mocks__/services'; import { IKibanaSearchResponse } from '@kbn/data-plugin/public'; diff --git a/src/plugins/discover/public/application/main/utils/fetch_total_hits.test.ts b/src/plugins/discover/public/application/main/utils/fetch_total_hits.test.ts index f0e079e146753..f88e9b3d7fc5d 100644 --- a/src/plugins/discover/public/application/main/utils/fetch_total_hits.test.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_total_hits.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ import { throwError as throwErrorRx, of } from 'rxjs'; -import { RequestAdapter } from '@kbn/inspector-plugin'; +import { RequestAdapter } from '@kbn/inspector-plugin/common'; import { savedSearchMock, savedSearchMockWithTimeField } from '../../../__mocks__/saved_search'; import { fetchTotalHits } from './fetch_total_hits'; import { discoverServiceMock } from '../../../__mocks__/services'; diff --git a/src/plugins/vis_types/timeseries/public/request_handler.ts b/src/plugins/vis_types/timeseries/public/request_handler.ts index d963fc76b5ead..e4c4f62fe6641 100644 --- a/src/plugins/vis_types/timeseries/public/request_handler.ts +++ b/src/plugins/vis_types/timeseries/public/request_handler.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ import type { KibanaExecutionContext } from '@kbn/core/public'; -import type { Adapters } from '@kbn/inspector-plugin'; +import type { Adapters } from '@kbn/inspector-plugin/common'; import { KibanaContext, handleResponse } from '@kbn/data-plugin/public'; import { getTimezone } from './application/lib/get_timezone'; import { getUISettings, getDataStart, getCoreStart } from './services'; diff --git a/src/plugins/visualizations/public/vis_types/types.ts b/src/plugins/visualizations/public/vis_types/types.ts index 970a9c1d730fb..4267e6a0d8aef 100644 --- a/src/plugins/visualizations/public/vis_types/types.ts +++ b/src/plugins/visualizations/public/vis_types/types.ts @@ -9,7 +9,7 @@ import type { IconType } from '@elastic/eui'; import type { ReactNode } from 'react'; import type { PaletteOutput } from '@kbn/coloring'; -import type { Adapters } from '@kbn/inspector-plugin'; +import type { Adapters } from '@kbn/inspector-plugin/common'; import type { Query } from '@kbn/es-query'; import type { AggGroupNames, AggParam, AggGroupName } from '@kbn/data-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/public'; diff --git a/tsconfig.base.json b/tsconfig.base.json index c1c1539e7b043..e314c16624201 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -407,6 +407,8 @@ "@kbn/task-manager-plugin/*": ["x-pack/plugins/task_manager/*"], "@kbn/telemetry-collection-xpack-plugin": ["x-pack/plugins/telemetry_collection_xpack"], "@kbn/telemetry-collection-xpack-plugin/*": ["x-pack/plugins/telemetry_collection_xpack/*"], + "@kbn/threat-intelligence-plugin": ["x-pack/plugins/threat_intelligence"], + "@kbn/threat-intelligence-plugin/*": ["x-pack/plugins/threat_intelligence/*"], "@kbn/timelines-plugin": ["x-pack/plugins/timelines"], "@kbn/timelines-plugin/*": ["x-pack/plugins/timelines/*"], "@kbn/transform-plugin": ["x-pack/plugins/transform"], diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index c15d92ff57985..c527e0a3d9556 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -68,7 +68,8 @@ "xpack.urlDrilldown": "plugins/drilldowns/url_drilldown", "xpack.watcher": "plugins/watcher", "xpack.observability": "plugins/observability", - "xpack.banners": "plugins/banners" + "xpack.banners": "plugins/banners", + "xpack.threatIntelligence": "plugins/threat_intelligence" }, "exclude": ["examples"], "translations": [ diff --git a/x-pack/plugins/alerting/server/rules_client/rules_client.ts b/x-pack/plugins/alerting/server/rules_client/rules_client.ts index c6a543a0b826e..7e55471038256 100644 --- a/x-pack/plugins/alerting/server/rules_client/rules_client.ts +++ b/x-pack/plugins/alerting/server/rules_client/rules_client.ts @@ -877,7 +877,6 @@ export class RulesClient { const rule = (await this.get({ id, includeLegacyId: true })) as SanitizedRuleWithLegacyId; try { - // Make sure user has access to this rule await this.authorization.ensureAuthorized({ ruleTypeId: rule.alertTypeId, consumer: rule.consumer, diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_async_with_debug.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_async_with_debug.ts index 241814e433b62..a401acafd01d9 100644 --- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_async_with_debug.ts +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_async_with_debug.ts @@ -9,7 +9,7 @@ import chalk from 'chalk'; import { KibanaRequest } from '@kbn/core/server'; -import { RequestStatus } from '@kbn/inspector-plugin'; +import { RequestStatus } from '@kbn/inspector-plugin/common'; import { WrappedElasticsearchClientError } from '@kbn/observability-plugin/server'; import { getInspectResponse } from '@kbn/observability-plugin/server'; import { inspectableEsQueriesMap } from '../../../routes/apm_routes/register_apm_server_routes'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/csv.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/csv.test.ts index d776e0a14028e..d979d47ceabb0 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/csv.test.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/csv.test.ts @@ -10,7 +10,7 @@ import { functionWrapper } from '@kbn/presentation-util-plugin/common/lib'; import { getFunctionErrors } from '../../../i18n'; import { csv } from './csv'; import { Datatable, ExecutionContext } from '@kbn/expressions-plugin/common'; -import { Adapters } from '@kbn/inspector-plugin'; +import { Adapters } from '@kbn/inspector-plugin/common'; const errors = getFunctionErrors().csv; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/dropdown_control.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/dropdown_control.test.ts index ac72911b15c90..09b8b1f2f7653 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/dropdown_control.test.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/dropdown_control.test.ts @@ -9,7 +9,7 @@ import { functionWrapper } from '@kbn/presentation-util-plugin/common/lib'; import { testTable, relationalTable } from './__fixtures__/test_tables'; import { dropdownControl } from './dropdownControl'; import { ExecutionContext } from '@kbn/expressions-plugin/common'; -import { Adapters } from '@kbn/inspector-plugin'; +import { Adapters } from '@kbn/inspector-plugin/common'; import { SerializableRecord } from '@kbn/utility-types'; describe('dropdownControl', () => { diff --git a/x-pack/plugins/event_log/server/event_log_client.ts b/x-pack/plugins/event_log/server/event_log_client.ts index 9e4d579c54fc1..13aa2e3eadec0 100644 --- a/x-pack/plugins/event_log/server/event_log_client.ts +++ b/x-pack/plugins/event_log/server/event_log_client.ts @@ -32,6 +32,7 @@ const optionalDateFieldSchema = schema.maybe( const sortSchema = schema.object({ sort_field: schema.oneOf([ schema.literal('@timestamp'), + schema.literal('event.sequence'), // can be used as a tiebreaker for @timestamp schema.literal('event.start'), schema.literal('event.end'), schema.literal('event.provider'), diff --git a/x-pack/plugins/fleet/cypress/integration/agent_binary_download_source.spec.ts b/x-pack/plugins/fleet/cypress/integration/agent_binary_download_source.spec.ts new file mode 100644 index 0000000000000..a6fa1c82a29a4 --- /dev/null +++ b/x-pack/plugins/fleet/cypress/integration/agent_binary_download_source.spec.ts @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { + SETTINGS_TAB, + AGENT_BINARY_SOURCES_TABLE, + AGENT_BINARY_SOURCES_TABLE_ACTIONS, + AGENT_BINARY_SOURCES_FLYOUT, + AGENT_POLICY_FORM, + CONFIRM_MODAL_CONFIRM_BUTTON, +} from '../screens/fleet'; +import { cleanupDownloadSources } from '../tasks/cleanup'; +import { FLEET, navigateTo } from '../tasks/navigation'; + +describe('Agent binary download source section', () => { + beforeEach(() => { + cleanupDownloadSources(); + navigateTo(FLEET); + }); + + it('has a default value and allows to edit an existing object', () => { + cy.getBySel(SETTINGS_TAB).click(); + + cy.getBySel(AGENT_BINARY_SOURCES_TABLE).find('tr').should('have.length', '2'); + cy.getBySel(AGENT_BINARY_SOURCES_TABLE_ACTIONS.HOST).contains( + 'https://artifacts.elastic.co/downloads/beats/elastic-agent' + ); + cy.getBySel(AGENT_BINARY_SOURCES_TABLE_ACTIONS.DEFAULT_VALUE).should('exist'); + cy.getBySel(AGENT_BINARY_SOURCES_TABLE_ACTIONS.EDIT).click(); + cy.getBySel(AGENT_BINARY_SOURCES_FLYOUT.NAME_INPUT).clear().type('New Name'); + cy.getBySel(AGENT_BINARY_SOURCES_FLYOUT.HOST_INPUT) + .clear() + .type('https://edited-default-host.co'); + cy.getBySel(AGENT_BINARY_SOURCES_FLYOUT.SUBMIT_BUTTON).click(); + cy.getBySel(CONFIRM_MODAL_CONFIRM_BUTTON).click(); + + cy.intercept('api/fleet/agent_download_sources/fleet-default-download-source', { + host: 'https://edited-default-host.co', + is_default: true, + name: 'New Name', + }); + }); + + it('allows to create new download source objects', () => { + cy.getBySel(SETTINGS_TAB).click(); + + cy.getBySel(AGENT_BINARY_SOURCES_TABLE_ACTIONS.ADD).click(); + cy.getBySel(AGENT_BINARY_SOURCES_FLYOUT.NAME_INPUT).clear().type('New Host'); + cy.getBySel(AGENT_BINARY_SOURCES_FLYOUT.HOST_INPUT).clear().type('https://new-test-host.co'); + cy.getBySel(AGENT_BINARY_SOURCES_FLYOUT.SUBMIT_BUTTON).click(); + cy.getBySel(AGENT_BINARY_SOURCES_TABLE).find('tr').should('have.length', '3'); + cy.intercept('api/fleet/agent_download_sources', { + name: 'New Host', + is_default: false, + host: 'https://new-test-host.co', + }); + + cy.getBySel(AGENT_BINARY_SOURCES_TABLE_ACTIONS.ADD).click(); + cy.getBySel(AGENT_BINARY_SOURCES_FLYOUT.NAME_INPUT).clear().type('New Default Host'); + cy.getBySel(AGENT_BINARY_SOURCES_FLYOUT.HOST_INPUT).clear().type('https://new-default-host.co'); + cy.getBySel(AGENT_BINARY_SOURCES_FLYOUT.IS_DEFAULT_SWITCH).click(); + cy.getBySel(AGENT_BINARY_SOURCES_FLYOUT.SUBMIT_BUTTON).click(); + + cy.intercept('api/fleet/agent_download_sources', { + name: 'New Default Host', + is_default: true, + host: 'https://new-default-host.co', + }); + }); + + it('the download source is displayed in agent policy settings', () => { + cy.request({ + method: 'POST', + url: `api/fleet/agent_download_sources`, + body: { + name: 'Custom Host', + id: 'fleet-local-registry', + host: 'https://new-custom-host.co', + }, + headers: { 'kbn-xsrf': 'kibana' }, + }); + cy.request({ + method: 'POST', + url: '/api/fleet/agent_policies', + body: { + name: 'Test Agent policy', + namespace: 'default', + description: '', + monitoring_enabled: ['logs', 'metrics'], + id: 'new-agent-policy', + download_source_id: 'fleet-local-registry', + }, + headers: { 'kbn-xsrf': 'kibana' }, + }).then((response: any) => { + navigateTo('app/fleet/policies/new-agent-policy/settings'); + cy.getBySel(AGENT_POLICY_FORM.DOWNLOAD_SOURCE_SELECT).contains('Custom Host'); + }); + }); +}); diff --git a/x-pack/plugins/fleet/cypress/integration/integrations_real.spec.ts b/x-pack/plugins/fleet/cypress/integration/integrations_real.spec.ts index 9bfd725497ffc..cbd08b6e8f5d6 100644 --- a/x-pack/plugins/fleet/cypress/integration/integrations_real.spec.ts +++ b/x-pack/plugins/fleet/cypress/integration/integrations_real.spec.ts @@ -27,7 +27,7 @@ import { import { ADD_PACKAGE_POLICY_BTN } from '../screens/fleet'; import { cleanupAgentPolicies } from '../tasks/cleanup'; -describe('Add Integration - Real API', () => { +describe.skip('Add Integration - Real API', () => { const integration = 'Apache'; after(() => { diff --git a/x-pack/plugins/fleet/cypress/screens/fleet.ts b/x-pack/plugins/fleet/cypress/screens/fleet.ts index 25677c823833f..b1083b7c33820 100644 --- a/x-pack/plugins/fleet/cypress/screens/fleet.ts +++ b/x-pack/plugins/fleet/cypress/screens/fleet.ts @@ -24,7 +24,31 @@ export const AGENT_POLICY_SAVE_INTEGRATION = 'saveIntegration'; export const PACKAGE_POLICY_TABLE_LINK = 'PackagePoliciesTableLink'; export const ADD_PACKAGE_POLICY_BTN = 'addPackagePolicyButton'; +export const AGENT_BINARY_SOURCES_TABLE = 'AgentDownloadSourcesTable'; +export const AGENT_BINARY_SOURCES_TABLE_ACTIONS = { + DEFAULT_VALUE: 'editDownloadSourceTable.defaultIcon', + HOST: 'editDownloadSourceTable.host', + ADD: 'addDownloadSourcesBtn', + EDIT: 'editDownloadSourceTable.edit.btn', + DELETE: 'editDownloadSourceTable.delete.btn', + DEFAULT_ICON: 'editDownloadSourceTable.defaultIcon', + HOST_NAME: 'editDownloadSourceTable.host', +}; +export const AGENT_BINARY_SOURCES_FLYOUT = { + NAME_INPUT: 'editDownloadSourcesFlyout.nameInput', + HOST_INPUT: 'editDownloadSourcesFlyout.hostInput', + IS_DEFAULT_SWITCH: 'editDownloadSourcesFlyout.isDefaultSwitch', + SUBMIT_BUTTON: 'editDownloadSourcesFlyout.submitBtn', + CANCEL_BUTTON: 'editDownloadSourcesFlyout.cancelBtn', +}; + export const ADD_AGENT_FLYOUT = { CONFIRM_AGENT_ENROLLMENT_BUTTON: 'ConfirmAgentEnrollmentButton', INCOMING_DATA_CONFIRMED_CALL_OUT: 'IncomingDataConfirmedCallOut', }; +export const CONFIRM_MODAL_CONFIRM_BUTTON = 'confirmModalConfirmButton'; +export const CONFIRM_MODAL_CANCEL_BUTTON = 'confirmModalCancelButton'; + +export const AGENT_POLICY_FORM = { + DOWNLOAD_SOURCE_SELECT: 'agentPolicyForm.downloadSource.select', +}; diff --git a/x-pack/plugins/fleet/cypress/tasks/cleanup.ts b/x-pack/plugins/fleet/cypress/tasks/cleanup.ts index e3ab5684777ca..b0d18fa9cdceb 100644 --- a/x-pack/plugins/fleet/cypress/tasks/cleanup.ts +++ b/x-pack/plugins/fleet/cypress/tasks/cleanup.ts @@ -34,3 +34,17 @@ export function unenrollAgent() { } ); } + +export function cleanupDownloadSources() { + cy.request('/api/fleet/agent_download_sources').then((response: any) => { + response.body.items + .filter((ds: any) => !ds.is_default) + .forEach((ds: any) => { + cy.request({ + method: 'DELETE', + url: `/api/fleet/agent_download_sources/${ds.id}`, + headers: { 'kbn-xsrf': 'kibana' }, + }); + }); + }); +} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/download_source_table/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/download_source_table/index.tsx index c4b196bbaf15f..c61acb89c310b 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/download_source_table/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/download_source_table/index.tsx @@ -76,7 +76,9 @@ export const DownloadSourceTable: React.FunctionComponent - downloadSource.is_default ? : null, + downloadSource.is_default ? ( + + ) : null, width: '200px', name: i18n.translate('xpack.fleet.settings.downloadSourcesTable.defaultColumnTitle', { defaultMessage: 'Default', @@ -131,5 +133,11 @@ export const DownloadSourceTable: React.FunctionComponent; + return ( + + ); }; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/settings_page/agent_binary_section.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/settings_page/agent_binary_section.tsx index ae9a42a7b2144..8eb5892e3e9a0 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/settings_page/agent_binary_section.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/settings_page/agent_binary_section.tsx @@ -50,7 +50,7 @@ export const AgentBinarySection: React.FunctionComponent ( + + +   + + +); + export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps) => { const { search } = useLocation(); const history = useHistory(); @@ -101,7 +113,7 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps const canWriteIntegrationPolicies = useAuthz().integrations.writeIntegrationPolicies; const packageAndAgentPolicies = useMemo((): Array<{ - agentPolicy: GetAgentPoliciesResponseItem; + agentPolicy?: GetAgentPoliciesResponseItem; packagePolicy: InMemoryPackagePolicy; }> => { if (!data?.items) { @@ -131,7 +143,7 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps }, [data?.items, updatableIntegrations]); const showAddAgentHelpForPackagePolicyId = packageAndAgentPolicies.find( - ({ agentPolicy }) => agentPolicy.id === showAddAgentHelpForPolicyId + ({ agentPolicy }) => agentPolicy?.id === showAddAgentHelpForPolicyId )?.packagePolicy?.id; // Handle the "add agent" link displayed in post-installation toast notifications in the case // where a user is clicking the link while on the package policies listing page @@ -187,7 +199,7 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps - {packagePolicy.hasUpgrade && ( + {agentPolicy && packagePolicy.hasUpgrade && ( ; + return agentPolicy ? ( + + ) : ( + + ); }, }, { @@ -250,6 +266,9 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps defaultMessage: 'Agents', }), render({ agentPolicy, packagePolicy }: InMemoryPackagePolicyAndAgentPolicy) { + if (!agentPolicy) { + return null; + } return ( ); }, @@ -311,7 +334,7 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps ); } const selectedPolicies = packageAndAgentPolicies.find( - ({ agentPolicy: policy }) => policy.id === flyoutOpenForPolicyId + ({ agentPolicy: policy }) => policy?.id === flyoutOpenForPolicyId ); const agentPolicy = selectedPolicies?.agentPolicy; const packagePolicy = selectedPolicies?.packagePolicy; diff --git a/x-pack/plugins/fleet/public/components/package_policy_actions_menu.test.tsx b/x-pack/plugins/fleet/public/components/package_policy_actions_menu.test.tsx index 7b260f1e27059..b1eff0b4b3b3d 100644 --- a/x-pack/plugins/fleet/public/components/package_policy_actions_menu.test.tsx +++ b/x-pack/plugins/fleet/public/components/package_policy_actions_menu.test.tsx @@ -32,7 +32,7 @@ function renderMenu({ agentPolicy={agentPolicy} packagePolicy={packagePolicy} showAddAgent={showAddAgent} - upgradePackagePolicyHref="" + upgradePackagePolicyHref="/test/upgrade-link" defaultIsOpen={defaultIsOpen} key="test1" /> @@ -95,8 +95,9 @@ test('Should enable upgrade button if package has upgrade', async () => { const agentPolicy = createMockAgentPolicy(); const packagePolicy = createMockPackagePolicy({ hasUpgrade: true }); const { utils } = renderMenu({ agentPolicy, packagePolicy }); + await act(async () => { - const upgradeButton = utils.getByText('Upgrade integration policy').closest('button'); + const upgradeButton = utils.getByTestId('PackagePolicyActionsUpgradeItem'); expect(upgradeButton).not.toBeDisabled(); }); }); diff --git a/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx b/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx index 9ba89394896ad..24525ce709efd 100644 --- a/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx +++ b/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx @@ -19,11 +19,11 @@ import { DangerEuiContextMenuItem } from './danger_eui_context_menu_item'; import { PackagePolicyDeleteProvider } from './package_policy_delete_provider'; export const PackagePolicyActionsMenu: React.FunctionComponent<{ - agentPolicy: AgentPolicy; + agentPolicy?: AgentPolicy; packagePolicy: InMemoryPackagePolicy; showAddAgent?: boolean; defaultIsOpen?: boolean; - upgradePackagePolicyHref: string; + upgradePackagePolicyHref?: string; }> = ({ agentPolicy, packagePolicy, @@ -38,6 +38,9 @@ export const PackagePolicyActionsMenu: React.FunctionComponent<{ const [isActionsMenuOpen, setIsActionsMenuOpen] = useState(defaultIsOpen); const isManaged = Boolean(packagePolicy.is_managed); + const agentPolicyIsManaged = Boolean(agentPolicy?.is_managed); + + const isAddAgentVisible = showAddAgent && agentPolicy && !agentPolicyIsManaged; const onEnrollmentFlyoutClose = useMemo(() => { return () => setIsEnrollmentFlyoutOpen(false); @@ -55,7 +58,7 @@ export const PackagePolicyActionsMenu: React.FunctionComponent<{ // defaultMessage="View integration" // /> // , - ...(showAddAgent && !agentPolicy.is_managed + ...(isAddAgentVisible ? [ , , ]; - if (!agentPolicy.is_managed) { + if (!agentPolicy || !agentPolicyIsManaged) { menuItems.push( {(deletePackagePoliciesPrompt) => { diff --git a/x-pack/plugins/fleet/public/components/package_policy_delete_provider.tsx b/x-pack/plugins/fleet/public/components/package_policy_delete_provider.tsx index 958cfe3bdb439..683ba398f6669 100644 --- a/x-pack/plugins/fleet/public/components/package_policy_delete_provider.tsx +++ b/x-pack/plugins/fleet/public/components/package_policy_delete_provider.tsx @@ -15,7 +15,7 @@ import { AGENT_API_ROUTES, AGENTS_PREFIX } from '../../common/constants'; import type { AgentPolicy } from '../types'; interface Props { - agentPolicy: AgentPolicy; + agentPolicy?: AgentPolicy; children: (deletePackagePoliciesPrompt: DeletePackagePoliciesPrompt) => React.ReactElement; } @@ -43,7 +43,7 @@ export const PackagePolicyDeleteProvider: React.FunctionComponent = ({ const fetchAgentsCount = useMemo( () => async () => { - if (isLoadingAgentsCount || !isFleetEnabled) { + if (isLoadingAgentsCount || !isFleetEnabled || !agentPolicy) { return; } setIsLoadingAgentsCount(true); @@ -59,7 +59,7 @@ export const PackagePolicyDeleteProvider: React.FunctionComponent = ({ setAgentsCount(data?.total || 0); setIsLoadingAgentsCount(false); }, - [agentPolicy.id, isFleetEnabled, isLoadingAgentsCount] + [agentPolicy, isFleetEnabled, isLoadingAgentsCount] ); const deletePackagePoliciesPrompt = useMemo( @@ -200,7 +200,7 @@ export const PackagePolicyDeleteProvider: React.FunctionComponent = ({ id="xpack.fleet.deletePackagePolicy.confirmModal.affectedAgentsMessage" defaultMessage="Fleet has detected that {agentPolicyName} is already in use by some of your agents." values={{ - agentPolicyName: {agentPolicy.name}, + agentPolicyName: {agentPolicy?.name}, }} /> diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts index e9d07abd62f8d..79ce7d3a3656b 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.ts @@ -613,7 +613,7 @@ class AgentPolicyService { id: string, packagePolicyIds: string[], options?: { user?: AuthenticatedUser; force?: boolean } - ): Promise { + ) { const oldAgentPolicy = await this.get(soClient, id, false); if (!oldAgentPolicy) { diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index a21f588359e4b..3164d417a6455 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -484,7 +484,19 @@ class PackagePolicyService implements PackagePolicyServiceInterface { throw new PackagePolicyRestrictionRelatedError(`Cannot delete package policy ${id}`); } - if (!options?.skipUnassignFromAgentPolicies) { + const agentPolicy = await agentPolicyService + .get(soClient, packagePolicy.policy_id) + .catch((err) => { + if (soClient.errors.isNotFoundError(err)) { + appContextService + .getLogger() + .warn(`Agent policy ${packagePolicy.policy_id} not found`); + return null; + } + throw err; + }); + + if (agentPolicy && !options?.skipUnassignFromAgentPolicies) { await agentPolicyService.unassignPackagePolicies( soClient, esClient, diff --git a/x-pack/plugins/kubernetes_security/common/translations.ts b/x-pack/plugins/kubernetes_security/common/translations.ts index 12099c728c5bb..8245bae5191c4 100644 --- a/x-pack/plugins/kubernetes_security/common/translations.ts +++ b/x-pack/plugins/kubernetes_security/common/translations.ts @@ -55,6 +55,20 @@ export const TREE_NAVIGATION_SHOW_MORE = (name: string) => defaultMessage: 'Show more {name}', }); +export const TREE_NAVIGATION_COLLAPSE = i18n.translate( + 'xpack.kubernetesSecurity.treeNavigation.collapse', + { + defaultMessage: 'Collapse Tree Navigation', + } +); + +export const TREE_NAVIGATION_EXPAND = i18n.translate( + 'xpack.kubernetesSecurity.treeNavigation.expand', + { + defaultMessage: 'Expand Tree Navigation', + } +); + export const CHART_TOGGLE_SHOW = i18n.translate('xpack.kubernetesSecurity.chartsToggle.show', { defaultMessage: 'Show charts', }); diff --git a/x-pack/plugins/kubernetes_security/public/components/tree_view_container/dynamic_tree_view/index.test.tsx b/x-pack/plugins/kubernetes_security/public/components/tree_view_container/dynamic_tree_view/index.test.tsx index c61de91b6884d..49e8c218aed9f 100644 --- a/x-pack/plugins/kubernetes_security/public/components/tree_view_container/dynamic_tree_view/index.test.tsx +++ b/x-pack/plugins/kubernetes_security/public/components/tree_view_container/dynamic_tree_view/index.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { waitFor } from '@testing-library/react'; import { AppContextTestRender, createAppRootMockRenderer } from '../../../test'; import { DynamicTreeView } from '.'; -import { clusterResponseMock, nodeResponseMock } from './mocks'; +import { clusterResponseMock, nodeResponseMock } from '../mocks'; describe('DynamicTreeView component', () => { let render: (props?: any) => ReturnType; diff --git a/x-pack/plugins/kubernetes_security/public/components/tree_view_container/index.tsx b/x-pack/plugins/kubernetes_security/public/components/tree_view_container/index.tsx index 7de0969df8e97..00fdd1b6c7110 100644 --- a/x-pack/plugins/kubernetes_security/public/components/tree_view_container/index.tsx +++ b/x-pack/plugins/kubernetes_security/public/components/tree_view_container/index.tsx @@ -6,7 +6,7 @@ */ import React, { useCallback, useMemo, useState } from 'react'; -import { EuiSplitPanel, EuiText } from '@elastic/eui'; +import { EuiSplitPanel } from '@elastic/eui'; import { useStyles } from './styles'; import { IndexPattern, GlobalFilter, TreeNavSelection, KubernetesCollection } from '../../types'; import { TreeNav } from './tree_nav'; @@ -39,14 +39,12 @@ export const TreeViewContainer = ({ return ( - - - + diff --git a/x-pack/plugins/kubernetes_security/public/components/tree_view_container/dynamic_tree_view/mocks.ts b/x-pack/plugins/kubernetes_security/public/components/tree_view_container/mocks.ts similarity index 100% rename from x-pack/plugins/kubernetes_security/public/components/tree_view_container/dynamic_tree_view/mocks.ts rename to x-pack/plugins/kubernetes_security/public/components/tree_view_container/mocks.ts diff --git a/x-pack/plugins/kubernetes_security/public/components/tree_view_container/styles.ts b/x-pack/plugins/kubernetes_security/public/components/tree_view_container/styles.ts index 3aa1f786e8c32..b995b3c876450 100644 --- a/x-pack/plugins/kubernetes_security/public/components/tree_view_container/styles.ts +++ b/x-pack/plugins/kubernetes_security/public/components/tree_view_container/styles.ts @@ -23,10 +23,6 @@ export const useStyles = () => { borderRight: border.thin, }; - const treeViewNav: CSSObject = { - width: '316px', - }; - const sessionsPanel: CSSObject = { overflowX: 'auto', }; @@ -34,7 +30,6 @@ export const useStyles = () => { return { outerPanel, navPanel, - treeViewNav, sessionsPanel, }; }, [euiTheme]); diff --git a/x-pack/plugins/kubernetes_security/public/components/tree_view_container/tree_nav/index.test.tsx b/x-pack/plugins/kubernetes_security/public/components/tree_view_container/tree_nav/index.test.tsx index 6664acb125f11..aa8911951dd79 100644 --- a/x-pack/plugins/kubernetes_security/public/components/tree_view_container/tree_nav/index.test.tsx +++ b/x-pack/plugins/kubernetes_security/public/components/tree_view_container/tree_nav/index.test.tsx @@ -7,12 +7,14 @@ import React from 'react'; import { AppContextTestRender, createAppRootMockRenderer } from '../../../test'; +import { clusterResponseMock } from '../mocks'; import { TreeNav } from '.'; describe('TreeNav component', () => { let render: () => ReturnType; let renderResult: ReturnType; let mockedContext: AppContextTestRender; + let mockedApi: AppContextTestRender['coreStart']['http']['get']; const defaultProps = { globalFilter: { @@ -25,6 +27,8 @@ describe('TreeNav component', () => { beforeEach(() => { mockedContext = createAppRootMockRenderer(); + mockedApi = mockedContext.coreStart.http.get; + mockedApi.mockResolvedValue(clusterResponseMock); }); it('mount with Logical View selected by default', async () => { @@ -49,4 +53,18 @@ describe('TreeNav component', () => { logicViewRadio.click(); expect(renderResult.getByText(logicalViewPath)).toBeInTheDocument(); }); + + it('collapses / expands the tree nav when clicking on collapse button', async () => { + renderResult = mockedContext.render(); + + expect(renderResult.getByText(/cluster/i)).toBeVisible(); + + const collapseButton = await renderResult.getByLabelText(/collapse/i); + collapseButton.click(); + expect(renderResult.getByText(/cluster/i)).not.toBeVisible(); + + const expandButton = await renderResult.getByLabelText(/expand/i); + expandButton.click(); + expect(renderResult.getByText(/cluster/i)).toBeVisible(); + }); }); diff --git a/x-pack/plugins/kubernetes_security/public/components/tree_view_container/tree_nav/index.tsx b/x-pack/plugins/kubernetes_security/public/components/tree_view_container/tree_nav/index.tsx index 699b83a4146d0..a02d183ebac33 100644 --- a/x-pack/plugins/kubernetes_security/public/components/tree_view_container/tree_nav/index.tsx +++ b/x-pack/plugins/kubernetes_security/public/components/tree_view_container/tree_nav/index.tsx @@ -5,11 +5,22 @@ * 2.0. */ import React, { useState, useMemo } from 'react'; -import { EuiButtonGroup, useGeneratedHtmlId, EuiText, EuiSpacer } from '@elastic/eui'; +import { + EuiButtonGroup, + useGeneratedHtmlId, + EuiText, + EuiSpacer, + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiToolTip, +} from '@elastic/eui'; import { TREE_VIEW_INFRASTRUCTURE_VIEW, TREE_VIEW_LOGICAL_VIEW, TREE_VIEW_SWITCHER_LEGEND, + TREE_NAVIGATION_COLLAPSE, + TREE_NAVIGATION_EXPAND, } from '../../../../common/translations'; import { useStyles } from './styles'; import { IndexPattern, GlobalFilter, TreeNavSelection } from '../../../types'; @@ -29,6 +40,16 @@ export const TreeNav = ({ indexPattern, globalFilter, onSelect, hasSelection }: const styles = useStyles(); const [tree, setTree] = useState(TREE_VIEW.logical); const [selected, setSelected] = useState(''); + const [isCollapsed, setIsCollapsed] = useState(false); + const treeNavTypePrefix = useGeneratedHtmlId({ + prefix: 'treeNavType', + }); + const logicalTreeViewPrefix = `${treeNavTypePrefix}${LOGICAL}`; + const [toggleIdSelected, setToggleIdSelected] = useState(logicalTreeViewPrefix); + + const handleToggleCollapse = () => { + setIsCollapsed(!isCollapsed); + }; const filterQueryWithTimeRange = useMemo(() => { return addTimerangeAndDefaultFilterToQuery( @@ -38,26 +59,25 @@ export const TreeNav = ({ indexPattern, globalFilter, onSelect, hasSelection }: ); }, [globalFilter.filterQuery, globalFilter.startDate, globalFilter.endDate]); - const treeNavTypePrefix = useGeneratedHtmlId({ - prefix: 'treeNavType', - }); - - const logicalTreeViewPrefix = `${treeNavTypePrefix}${LOGICAL}`; - - const [toggleIdSelected, setToggleIdSelected] = useState(logicalTreeViewPrefix); + const options: TreeViewOptionsGroup[] = useMemo( + () => [ + { + id: logicalTreeViewPrefix, + label: TREE_VIEW_LOGICAL_VIEW, + value: LOGICAL, + }, + { + id: `${treeNavTypePrefix}${INFRASTRUCTURE}`, + label: TREE_VIEW_INFRASTRUCTURE_VIEW, + value: INFRASTRUCTURE, + }, + ], + [logicalTreeViewPrefix, treeNavTypePrefix] + ); - const options: TreeViewOptionsGroup[] = [ - { - id: logicalTreeViewPrefix, - label: TREE_VIEW_LOGICAL_VIEW, - value: LOGICAL, - }, - { - id: `${treeNavTypePrefix}${INFRASTRUCTURE}`, - label: TREE_VIEW_INFRASTRUCTURE_VIEW, - value: INFRASTRUCTURE, - }, - ]; + const selectedLabel = useMemo(() => { + return options.find((opt) => opt.id === toggleIdSelected)!.label; + }, [options, toggleIdSelected]); const handleTreeViewSwitch = (id: string, value: TreeViewKind) => { setToggleIdSelected(id); @@ -66,43 +86,67 @@ export const TreeNav = ({ indexPattern, globalFilter, onSelect, hasSelection }: return ( <> - - - - {tree.map((t) => t.name).join(' / ')} - - -
- { - const newSelectionDepth = { - ...selectionDepth, - [type]: key, - }; - setSelected( - Object.entries(newSelectionDepth) - .map(([k, v]) => `${k}.${v}`) - .join() - ); - onSelect(newSelectionDepth); - }} - hasSelection={hasSelection} - /> + {isCollapsed && ( + + + + )} +
+ + + + + + + + + + + + + {tree.map((t) => t.name).join(' / ')} + + +
+ { + const newSelectionDepth = { + ...selectionDepth, + [type]: key, + }; + setSelected( + Object.entries(newSelectionDepth) + .map(([k, v]) => `${k}.${v}`) + .join() + ); + onSelect(newSelectionDepth); + }} + hasSelection={hasSelection} + /> +
); diff --git a/x-pack/plugins/kubernetes_security/public/components/tree_view_container/tree_nav/styles.ts b/x-pack/plugins/kubernetes_security/public/components/tree_view_container/tree_nav/styles.ts index 27bab04d90511..d915e73ed21d1 100644 --- a/x-pack/plugins/kubernetes_security/public/components/tree_view_container/tree_nav/styles.ts +++ b/x-pack/plugins/kubernetes_security/public/components/tree_view_container/tree_nav/styles.ts @@ -23,6 +23,7 @@ export const useStyles = () => { const treeViewContainer: CSSObject = { height: '600px', + width: '288px', overflowY: 'auto', }; diff --git a/x-pack/plugins/ml/public/application/trained_models/models_management/start_deployment_setup.tsx b/x-pack/plugins/ml/public/application/trained_models/models_management/start_deployment_setup.tsx index 567e62f3772aa..7f5a19d7aa178 100644 --- a/x-pack/plugins/ml/public/application/trained_models/models_management/start_deployment_setup.tsx +++ b/x-pack/plugins/ml/public/application/trained_models/models_management/start_deployment_setup.tsx @@ -46,7 +46,7 @@ export interface ThreadingParams { threadsPerAllocations: number; } -const THREADS_MAX_EXPONENT = 6; +const THREADS_MAX_EXPONENT = 4; /** * Form for setting threading params. diff --git a/x-pack/plugins/observability/common/utils/get_inspect_response.ts b/x-pack/plugins/observability/common/utils/get_inspect_response.ts index dbd0cd68736db..cfcfceb72752f 100644 --- a/x-pack/plugins/observability/common/utils/get_inspect_response.ts +++ b/x-pack/plugins/observability/common/utils/get_inspect_response.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import type { KibanaRequest } from '@kbn/core/server'; -import type { RequestStatistics, RequestStatus } from '@kbn/inspector-plugin'; +import type { RequestStatistics, RequestStatus } from '@kbn/inspector-plugin/common'; import { InspectResponse } from '../../typings/common'; import { WrappedElasticsearchClientError } from './unwrap_es_response'; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts index d63040ce9305f..3c8c2e5d4bffb 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts @@ -29,10 +29,11 @@ import { TypedLensByValueInput, XYCurveType, XYState, - FormulaPublicApi, YAxisMode, MinIndexPatternColumn, MaxIndexPatternColumn, + FormulaPublicApi, + FormulaIndexPatternColumn, } from '@kbn/lens-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/common'; import { PersistableFilter } from '@kbn/lens-plugin/common'; @@ -47,6 +48,7 @@ import { PERCENTILE, PERCENTILE_RANKS, ReportTypes, + FORMULA_COLUMN, } from './constants'; import { ColumnFilter, @@ -403,10 +405,11 @@ export class LensAttributes { layerConfig: LayerConfig, layerId: string, columnFilter?: string - ): Record { + ): Record { const yAxisColumns = layerConfig.seriesConfig.yAxisColumns; const { sourceField: mainSourceField, label: mainLabel } = yAxisColumns[0]; - const lensColumns: Record = {}; + const lensColumns: Record = + {}; // start at 1, because main y axis will have the first percentile breakdown for (let i = 1; i < PERCENTILE_RANKS.length; i++) { @@ -522,6 +525,7 @@ export class LensAttributes { }) { const { breakdown, seriesConfig } = layerConfig; const { + formula, fieldMeta, columnType, fieldName, @@ -531,6 +535,16 @@ export class LensAttributes { showPercentileAnnotations, } = this.getFieldMeta(sourceField, layerConfig); + if (columnType === FORMULA_COLUMN) { + return getDistributionInPercentageColumn({ + layerId, + formula, + label: columnLabel ?? label, + dataView: layerConfig.indexPattern, + lensFormulaHelper: this.lensFormulaHelper!, + }).main; + } + if (showPercentileAnnotations) { this.addThresholdLayer(fieldName, layerId, layerConfig); } @@ -607,9 +621,11 @@ export class LensAttributes { timeScale, paramFilters, showPercentileAnnotations, + formula, } = parseCustomFieldName(layerConfig.seriesConfig, layerConfig.selectedMetricField); const fieldMeta = layerConfig.indexPattern.getFieldByName(fieldName!); return { + formula, palette, fieldMeta, fieldName, @@ -666,7 +682,10 @@ export class LensAttributes { forAccessorsKeys?: boolean ) { const { breakdown } = layerConfig; - const lensColumns: Record = {}; + const lensColumns: Record< + string, + FieldBasedIndexPatternColumn | SumIndexPatternColumn | FormulaIndexPatternColumn + > = {}; const yAxisColumns = layerConfig.seriesConfig.yAxisColumns; const { sourceField: mainSourceField, label: mainLabel } = yAxisColumns[0]; @@ -680,6 +699,20 @@ export class LensAttributes { }).supportingColumns; } + if (mainSourceField && !forAccessorsKeys) { + const { columnLabel, formula, columnType } = this.getFieldMeta(mainSourceField, layerConfig); + + if (columnType === FORMULA_COLUMN) { + return getDistributionInPercentageColumn({ + label: columnLabel, + layerId, + formula, + dataView: layerConfig.indexPattern, + lensFormulaHelper: this.lensFormulaHelper!, + }).supportingColumns; + } + } + if (yAxisColumns.length === 1 && breakdown === PERCENTILE) { return this.getPercentileBreakdowns(layerConfig, layerId, columnFilter); } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_columns/overall_column.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_columns/overall_column.ts index b0cb9939dacc1..348c45def81ab 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_columns/overall_column.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_columns/overall_column.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { FormulaPublicApi } from '@kbn/lens-plugin/public'; +import { FormulaIndexPatternColumn, FormulaPublicApi } from '@kbn/lens-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/common'; export function getDistributionInPercentageColumn({ @@ -13,19 +13,22 @@ export function getDistributionInPercentageColumn({ dataView, columnFilter, lensFormulaHelper, + formula, }: { label?: string; columnFilter?: string; layerId: string; lensFormulaHelper: FormulaPublicApi; dataView: DataView; + formula?: string; }) { const yAxisColId = `y-axis-column-${layerId}`; - let lensFormula = 'count() / overall_sum(count())'; + let lensFormula = formula ?? 'count() / overall_sum(count())'; if (columnFilter) { - lensFormula = `count(kql='${columnFilter}') / overall_sum(count(kql='${columnFilter}'))`; + lensFormula = + formula ?? `count(kql='${columnFilter}') / overall_sum(count(kql='${columnFilter}'))`; } const { columns } = lensFormulaHelper?.insertOrReplaceFormulaColumn( @@ -49,5 +52,5 @@ export function getDistributionInPercentageColumn({ const { [yAxisColId]: main, ...supportingColumns } = columns; - return { main: columns[yAxisColId], supportingColumns }; + return { main: columns[yAxisColId] as FormulaIndexPatternColumn, supportingColumns }; } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts index aff40f980f25f..6a53e438d280a 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts @@ -12,6 +12,7 @@ import { REPORT_METRIC_FIELD, PERCENTILE, ReportTypes, + FORMULA_COLUMN, } from '../constants'; import { CLS_LABEL, @@ -86,6 +87,12 @@ export function getSyntheticsKPIConfig({ dataView }: ConfigProps): SeriesConfig id: MONITOR_DURATION_US, columnType: OPERATION_COLUMN, }, + { + label: 'Monitor availability', + id: 'monitor_availability', + columnType: FORMULA_COLUMN, + formula: "1- (count(kql='summary.down > 0') / count())", + }, { field: SUMMARY_UP, id: SUMMARY_UP, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/embeddable/embeddable.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/embeddable/embeddable.tsx index f82dfda9f0a8e..f4c438b693d48 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/embeddable/embeddable.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/embeddable/embeddable.tsx @@ -42,6 +42,7 @@ export interface ExploratoryEmbeddableProps { isSingleMetric?: boolean; legendIsVisible?: boolean; legendPosition?: Position; + hideTicks?: boolean; onBrushEnd?: (param: { range: number[] }) => void; caseOwner?: string; reportConfigMap?: ReportConfigMap; @@ -83,6 +84,7 @@ export default function Embeddable({ withActions = true, lensFormulaHelper, align, + hideTicks, }: ExploratoryEmbeddableComponentProps) { const LensComponent = lens?.EmbeddableComponent; const LensSaveModalComponent = lens?.SaveModalComponent; @@ -127,6 +129,14 @@ export default function Embeddable({ (attributesJSON.state.visualization as XYState).legend.position = legendPosition; } + if (hideTicks) { + (attributesJSON.state.visualization as XYState).tickLabelsVisibilitySettings = { + x: false, + yRight: false, + yLeft: false, + }; + } + const actions = useActions({ withActions, attributes, diff --git a/x-pack/plugins/observability/typings/common.ts b/x-pack/plugins/observability/typings/common.ts index 1904728896d82..df51061b56a5c 100644 --- a/x-pack/plugins/observability/typings/common.ts +++ b/x-pack/plugins/observability/typings/common.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { Request } from '@kbn/inspector-plugin'; +import { Request } from '@kbn/inspector-plugin/common'; export type ObservabilityApp = | 'infra_metrics' diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 6f4f1b9d03e1c..63f6b9723e369 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -81,18 +81,28 @@ export enum SecurityPageName { case = 'cases', // must match `CasesDeepLinkId.cases` caseConfigure = 'cases_configure', // must match `CasesDeepLinkId.casesConfigure` caseCreate = 'cases_create', // must match `CasesDeepLinkId.casesCreate` + /* + * Warning: Computed values are not permitted in an enum with string valued members + * All cloud security posture page names must match `CloudSecurityPosturePageId` in x-pack/plugins/cloud_security_posture/public/common/navigation/types.ts + */ + cloudSecurityPostureBenchmarks = 'cloud_security_posture-benchmarks', + cloudSecurityPostureDashboard = 'cloud_security_posture-dashboard', + cloudSecurityPostureFindings = 'cloud_security_posture-findings', + cloudSecurityPostureRules = 'cloud_security_posture-rules', + dashboardsLanding = 'dashboards', detections = 'detections', detectionAndResponse = 'detection_response', endpoints = 'endpoints', eventFilters = 'event_filters', exceptions = 'exceptions', + exploreLanding = 'explore', hostIsolationExceptions = 'host_isolation_exceptions', hosts = 'hosts', hostsAnomalies = 'hosts-anomalies', - hostsExternalAlerts = 'hosts-external_alerts', hostsRisk = 'hosts-risk', hostsEvents = 'hosts-events', investigate = 'investigate', + kubernetes = 'kubernetes', landing = 'get_started', network = 'network', networkAnomalies = 'network-anomalies', @@ -100,34 +110,24 @@ export enum SecurityPageName { networkExternalAlerts = 'network-external_alerts', networkHttp = 'network-http', networkTls = 'network-tls', + noPage = '', overview = 'overview', policies = 'policy', responseActions = 'response_actions', rules = 'rules', rulesCreate = 'rules-create', + sessions = 'sessions', + threatIntelligence = 'threat-intelligence', timelines = 'timelines', timelinesTemplates = 'timelines-templates', trustedApps = 'trusted_apps', uncommonProcesses = 'uncommon_processes', users = 'users', - usersAuthentications = 'users-authentications', usersAnomalies = 'users-anomalies', - usersRisk = 'users-risk', - sessions = 'sessions', + usersAuthentications = 'users-authentications', usersEvents = 'users-events', usersExternalAlerts = 'users-external_alerts', - kubernetes = 'kubernetes', - exploreLanding = 'explore', - dashboardsLanding = 'dashboards', - noPage = '', - /* - * Warning: Computed values are not permitted in an enum with string valued members - * All cloud security posture page names must match `CloudSecurityPosturePageId` in x-pack/plugins/cloud_security_posture/public/common/navigation/types.ts - */ - cloudSecurityPostureDashboard = 'cloud_security_posture-dashboard', - cloudSecurityPostureFindings = 'cloud_security_posture-findings', - cloudSecurityPostureBenchmarks = 'cloud_security_posture-benchmarks', - cloudSecurityPostureRules = 'cloud_security_posture-rules', + usersRisk = 'users-risk', } export const EXPLORE_PATH = '/explore' as const; @@ -156,6 +156,7 @@ export const HOST_ISOLATION_EXCEPTIONS_PATH = `${MANAGEMENT_PATH}/host_isolation_exceptions` as const; export const BLOCKLIST_PATH = `${MANAGEMENT_PATH}/blocklist` as const; export const RESPONSE_ACTIONS_PATH = `${MANAGEMENT_PATH}/response_actions` as const; +export const THREAT_INTELLIGENCE_PATH = '/threat_intelligence' as const; export const APP_OVERVIEW_PATH = `${APP_PATH}${OVERVIEW_PATH}` as const; export const APP_LANDING_PATH = `${APP_PATH}${LANDING_PATH}` as const; @@ -180,6 +181,7 @@ export const APP_HOST_ISOLATION_EXCEPTIONS_PATH = `${APP_PATH}${HOST_ISOLATION_EXCEPTIONS_PATH}` as const; export const APP_BLOCKLIST_PATH = `${APP_PATH}${BLOCKLIST_PATH}` as const; export const APP_RESPONSE_ACTIONS_PATH = `${APP_PATH}${RESPONSE_ACTIONS_PATH}` as const; +export const APP_THREAT_INTELLIGENCE_PATH = `${APP_PATH}${THREAT_INTELLIGENCE_PATH}` as const; // cloud logs to exclude from default index pattern export const EXCLUDE_ELASTIC_CLOUD_INDICES = ['-*elastic-cloud-logs-*']; @@ -229,6 +231,14 @@ export const IP_REPUTATION_LINKS_SETTING_DEFAULT = `[ export const SHOW_RELATED_INTEGRATIONS_SETTING = 'securitySolution:showRelatedIntegrations' as const; +/** This Kibana Advanced Setting enables extended rule execution logging to Event Log */ +export const EXTENDED_RULE_EXECUTION_LOGGING_ENABLED_SETTING = + 'securitySolution:extendedRuleExecutionLoggingEnabled' as const; + +/** This Kibana Advanced Setting sets minimum log level starting from which execution logs will be written to Event Log */ +export const EXTENDED_RULE_EXECUTION_LOGGING_MIN_LEVEL_SETTING = + 'securitySolution:extendedRuleExecutionLoggingMinLevel' as const; + /** * Id for the notifications alerting type * @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function @@ -266,10 +276,6 @@ export const DETECTION_ENGINE_RULES_BULK_UPDATE = * Internal detection engine routes */ export const INTERNAL_DETECTION_ENGINE_URL = '/internal/detection_engine' as const; -export const DETECTION_ENGINE_RULE_EXECUTION_EVENTS_URL = - `${INTERNAL_DETECTION_ENGINE_URL}/rules/{ruleId}/execution/events` as const; -export const detectionEngineRuleExecutionEventsUrl = (ruleId: string) => - `${INTERNAL_DETECTION_ENGINE_URL}/rules/${ruleId}/execution/events` as const; export const DETECTION_ENGINE_INSTALLED_INTEGRATIONS_URL = `${INTERNAL_DETECTION_ENGINE_URL}/fleet/integrations/installed` as const; diff --git a/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/api/get_rule_execution_events/request_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/api/get_rule_execution_events/request_schema.test.ts new file mode 100644 index 0000000000000..c6e02b6f815e3 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/api/get_rule_execution_events/request_schema.test.ts @@ -0,0 +1,168 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; + +import { + GetRuleExecutionEventsRequestParams, + GetRuleExecutionEventsRequestQuery, +} from './request_schema'; + +describe('Request schema of Get rule execution events', () => { + describe('GetRuleExecutionEventsRequestParams', () => { + describe('Validation succeeds', () => { + it('when required parameters are passed', () => { + const input = { + ruleId: 'some id', + }; + + const decoded = GetRuleExecutionEventsRequestParams.decode(input); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual( + expect.objectContaining({ + ruleId: 'some id', + }) + ); + }); + + it('when unknown parameters are passed as well', () => { + const input = { + ruleId: 'some id', + foo: 'bar', // this one is not in the schema and will be stripped + }; + + const decoded = GetRuleExecutionEventsRequestParams.decode(input); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual({ + ruleId: 'some id', + }); + }); + }); + + describe('Validation fails', () => { + const test = (input: unknown) => { + const decoded = GetRuleExecutionEventsRequestParams.decode(input); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors)).length).toBeGreaterThan(0); + expect(message.schema).toEqual({}); + }; + + it('when not all the required parameters are passed', () => { + const input = {}; + test(input); + }); + + it('when ruleId is an empty string', () => { + const input: GetRuleExecutionEventsRequestParams = { + ruleId: '', + }; + + test(input); + }); + }); + }); + + describe('GetRuleExecutionEventsRequestQuery', () => { + describe('Validation succeeds', () => { + it('when valid parameters are passed', () => { + const input = { + event_types: 'message,status-change', + log_levels: 'debug,info,error', + sort_order: 'asc', + page: 42, + per_page: 6, + }; + + const decoded = GetRuleExecutionEventsRequestQuery.decode(input); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual({ + event_types: ['message', 'status-change'], + log_levels: ['debug', 'info', 'error'], + sort_order: 'asc', + page: 42, + per_page: 6, + }); + }); + + it('when unknown parameters are passed as well', () => { + const input = { + event_types: 'message,status-change', + log_levels: 'debug,info,error', + sort_order: 'asc', + page: 42, + per_page: 6, + foo: 'bar', // this one is not in the schema and will be stripped + }; + + const decoded = GetRuleExecutionEventsRequestQuery.decode(input); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual({ + event_types: ['message', 'status-change'], + log_levels: ['debug', 'info', 'error'], + sort_order: 'asc', + page: 42, + per_page: 6, + }); + }); + + it('when no parameters are passed (all are have default values)', () => { + const input = {}; + + const decoded = GetRuleExecutionEventsRequestQuery.decode(input); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(expect.any(Object)); + }); + }); + + describe('Validation fails', () => { + const test = (input: unknown) => { + const decoded = GetRuleExecutionEventsRequestQuery.decode(input); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors)).length).toBeGreaterThan(0); + expect(message.schema).toEqual({}); + }; + + it('when invalid parameters are passed', () => { + test({ + event_types: 'foo,status-change', + }); + }); + }); + + describe('Validation sets default values', () => { + it('when optional parameters are not passed', () => { + const input = {}; + + const decoded = GetRuleExecutionEventsRequestQuery.decode(input); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual({ + event_types: [], + log_levels: [], + sort_order: 'desc', + page: 1, + per_page: 20, + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/api/get_rule_execution_events/request_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/api/get_rule_execution_events/request_schema.ts new file mode 100644 index 0000000000000..9ffa2467e0852 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/api/get_rule_execution_events/request_schema.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as t from 'io-ts'; + +import { DefaultPerPage, DefaultPage } from '@kbn/securitysolution-io-ts-alerting-types'; +import { defaultCsvArray, NonEmptyString } from '@kbn/securitysolution-io-ts-types'; + +import { DefaultSortOrderDesc } from '../../../schemas/common'; +import { TRuleExecutionEventType } from '../../model/execution_event'; +import { TLogLevel } from '../../model/log_level'; + +/** + * Path parameters of the API route. + */ +export type GetRuleExecutionEventsRequestParams = t.TypeOf< + typeof GetRuleExecutionEventsRequestParams +>; +export const GetRuleExecutionEventsRequestParams = t.exact( + t.type({ + ruleId: NonEmptyString, + }) +); + +/** + * Query string parameters of the API route. + */ +export type GetRuleExecutionEventsRequestQuery = t.TypeOf< + typeof GetRuleExecutionEventsRequestQuery +>; +export const GetRuleExecutionEventsRequestQuery = t.exact( + t.type({ + event_types: defaultCsvArray(TRuleExecutionEventType), + log_levels: defaultCsvArray(TLogLevel), + sort_order: DefaultSortOrderDesc, // defaults to 'desc' + page: DefaultPage, // defaults to 1 + per_page: DefaultPerPage, // defaults to 20 + }) +); diff --git a/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/api/get_rule_execution_events/response_schema.mock.ts b/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/api/get_rule_execution_events/response_schema.mock.ts new file mode 100644 index 0000000000000..c4c501f2aeac9 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/api/get_rule_execution_events/response_schema.mock.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ruleExecutionEventMock } from '../../model/execution_event.mock'; +import type { GetRuleExecutionEventsResponse } from './response_schema'; + +const getSomeResponse = (): GetRuleExecutionEventsResponse => { + const events = ruleExecutionEventMock.getSomeEvents(); + return { + events, + pagination: { + page: 1, + per_page: events.length, + total: events.length * 10, + }, + }; +}; + +export const getRuleExecutionEventsResponseMock = { + getSomeResponse, +}; diff --git a/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/api/get_rule_execution_events/response_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/api/get_rule_execution_events/response_schema.ts new file mode 100644 index 0000000000000..8637b3b0411a2 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/api/get_rule_execution_events/response_schema.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as t from 'io-ts'; +import { PaginationResult } from '../../../schemas/common'; +import { RuleExecutionEvent } from '../../model/execution_event'; + +/** + * Response body of the API route. + */ +export type GetRuleExecutionEventsResponse = t.TypeOf; +export const GetRuleExecutionEventsResponse = t.exact( + t.type({ + events: t.array(RuleExecutionEvent), + pagination: PaginationResult, + }) +); diff --git a/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/api/get_rule_execution_results/request_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/api/get_rule_execution_results/request_schema.test.ts new file mode 100644 index 0000000000000..8757084a2ec98 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/api/get_rule_execution_results/request_schema.test.ts @@ -0,0 +1,271 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; + +import { RULE_EXECUTION_STATUSES } from '../../model/execution_status'; +import { DefaultSortField, DefaultRuleExecutionStatusCsvArray } from './request_schema'; + +describe('Request schema of Get rule execution results', () => { + describe('DefaultRuleExecutionStatusCsvArray', () => { + describe('Validation succeeds', () => { + describe('when input is a single rule execution status', () => { + const cases = RULE_EXECUTION_STATUSES.map((supportedStatus) => { + return { input: supportedStatus }; + }); + + cases.forEach(({ input }) => { + it(`${input}`, () => { + const decoded = DefaultRuleExecutionStatusCsvArray.decode(input); + const message = pipe(decoded, foldLeftRight); + const expectedOutput = [input]; // note that it's an array after decode + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(expectedOutput); + }); + }); + }); + + describe('when input is an array of rule execution statuses', () => { + const cases = [ + { input: ['succeeded', 'failed'] }, + { input: ['partial failure', 'going to run', 'running'] }, + ]; + + cases.forEach(({ input }) => { + it(`${input}`, () => { + const decoded = DefaultRuleExecutionStatusCsvArray.decode(input); + const message = pipe(decoded, foldLeftRight); + const expectedOutput = input; + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(expectedOutput); + }); + }); + }); + + describe('when input is a string which is a comma-separated array of statuses', () => { + const cases = [ + { + input: 'succeeded,failed', + expectedOutput: ['succeeded', 'failed'], + }, + { + input: 'partial failure,going to run,running', + expectedOutput: ['partial failure', 'going to run', 'running'], + }, + ]; + + cases.forEach(({ input, expectedOutput }) => { + it(`${input}`, () => { + const decoded = DefaultRuleExecutionStatusCsvArray.decode(input); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(expectedOutput); + }); + }); + }); + }); + + describe('Validation fails', () => { + describe('when input is a single invalid value', () => { + const cases = [ + { + input: 'val', + expectedErrors: [ + 'Invalid value "val" supplied to "DefaultCsvArray"', + ], + }, + { + input: '5', + expectedErrors: [ + 'Invalid value "5" supplied to "DefaultCsvArray"', + ], + }, + { + input: 5, + expectedErrors: [ + 'Invalid value "5" supplied to "DefaultCsvArray"', + ], + }, + { + input: {}, + expectedErrors: [ + 'Invalid value "{}" supplied to "DefaultCsvArray"', + ], + }, + ]; + + cases.forEach(({ input, expectedErrors }) => { + it(`${input}`, () => { + const decoded = DefaultRuleExecutionStatusCsvArray.decode(input); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(expectedErrors); + expect(message.schema).toEqual({}); + }); + }); + }); + + describe('when input is an array of invalid values', () => { + const cases = [ + { + input: ['value 1', 5], + expectedErrors: [ + 'Invalid value "value 1" supplied to "DefaultCsvArray"', + 'Invalid value "5" supplied to "DefaultCsvArray"', + ], + }, + { + input: ['value 1', 'succeeded'], + expectedErrors: [ + 'Invalid value "value 1" supplied to "DefaultCsvArray"', + ], + }, + { + input: ['', 5, {}], + expectedErrors: [ + 'Invalid value "" supplied to "DefaultCsvArray"', + 'Invalid value "5" supplied to "DefaultCsvArray"', + 'Invalid value "{}" supplied to "DefaultCsvArray"', + ], + }, + ]; + + cases.forEach(({ input, expectedErrors }) => { + it(`${input}`, () => { + const decoded = DefaultRuleExecutionStatusCsvArray.decode(input); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(expectedErrors); + expect(message.schema).toEqual({}); + }); + }); + }); + + describe('when input is a string which is a comma-separated array of invalid values', () => { + const cases = [ + { + input: 'value 1,5', + expectedErrors: [ + 'Invalid value "value 1" supplied to "DefaultCsvArray"', + 'Invalid value "5" supplied to "DefaultCsvArray"', + ], + }, + { + input: 'value 1,succeeded', + expectedErrors: [ + 'Invalid value "value 1" supplied to "DefaultCsvArray"', + ], + }, + { + input: ',5,{}', + expectedErrors: [ + 'Invalid value "" supplied to "DefaultCsvArray"', + 'Invalid value "5" supplied to "DefaultCsvArray"', + 'Invalid value "{}" supplied to "DefaultCsvArray"', + ], + }, + ]; + + cases.forEach(({ input, expectedErrors }) => { + it(`${input}`, () => { + const decoded = DefaultRuleExecutionStatusCsvArray.decode(input); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(expectedErrors); + expect(message.schema).toEqual({}); + }); + }); + }); + }); + + describe('Validation returns default value (an empty array)', () => { + describe('when input is', () => { + const cases = [{ input: null }, { input: undefined }, { input: '' }, { input: [] }]; + + cases.forEach(({ input }) => { + it(`${input}`, () => { + const decoded = DefaultRuleExecutionStatusCsvArray.decode(input); + const message = pipe(decoded, foldLeftRight); + const expectedOutput: string[] = []; + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(expectedOutput); + }); + }); + }); + }); + }); + + describe('DefaultSortField', () => { + describe('Validation succeeds', () => { + describe('when input is a valid sort field', () => { + const cases = [ + { input: 'timestamp' }, + { input: 'duration_ms' }, + { input: 'gap_duration_s' }, + { input: 'indexing_duration_ms' }, + { input: 'search_duration_ms' }, + { input: 'schedule_delay_ms' }, + ]; + + cases.forEach(({ input }) => { + it(`${input}`, () => { + const decoded = DefaultSortField.decode(input); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(input); + }); + }); + }); + }); + + describe('Validation fails', () => { + describe('when input is an invalid sort field', () => { + const cases = [ + { input: 'status' }, + { input: 'message' }, + { input: 'es_search_duration_ms' }, + { input: 'security_status' }, + { input: 'security_message' }, + ]; + + cases.forEach(({ input }) => { + it(`${input}`, () => { + const decoded = DefaultSortField.decode(input); + const message = pipe(decoded, foldLeftRight); + const expectedErrors = [`Invalid value "${input}" supplied to "DefaultSortField"`]; + + expect(getPaths(left(message.errors))).toEqual(expectedErrors); + expect(message.schema).toEqual({}); + }); + }); + }); + }); + + describe('Validation returns the default sort field "timestamp"', () => { + describe('when input is', () => { + const cases = [{ input: null }, { input: undefined }]; + + cases.forEach(({ input }) => { + it(`${input}`, () => { + const decoded = DefaultSortField.decode(input); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual('timestamp'); + }); + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/api/get_rule_execution_results/request_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/api/get_rule_execution_results/request_schema.ts new file mode 100644 index 0000000000000..33458ab0a875a --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/api/get_rule_execution_results/request_schema.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as t from 'io-ts'; + +import { DefaultPage, DefaultPerPage } from '@kbn/securitysolution-io-ts-alerting-types'; +import { + defaultCsvArray, + DefaultEmptyString, + defaultValue, + IsoDateString, + NonEmptyString, +} from '@kbn/securitysolution-io-ts-types'; + +import { DefaultSortOrderDesc } from '../../../schemas/common'; +import { SortFieldOfRuleExecutionResult } from '../../model/execution_result'; +import { TRuleExecutionStatus } from '../../model/execution_status'; + +/** + * Types the DefaultRuleExecutionStatusCsvArray as: + * - If not specified, then a default empty array will be set + * - If an array is sent in, then the array will be validated to ensure all elements are a RuleExecutionStatus + * (or that the array is empty) + * - If a CSV string is sent in, then it will be parsed to an array which will be validated + */ +export const DefaultRuleExecutionStatusCsvArray = defaultCsvArray(TRuleExecutionStatus); + +/** + * Types the DefaultSortField as: + * - If undefined, then a default sort field of 'timestamp' will be set + * - If a string is sent in, then the string will be validated to ensure it is as valid sortFields + */ +export const DefaultSortField = defaultValue( + SortFieldOfRuleExecutionResult, + 'timestamp', + 'DefaultSortField' +); + +/** + * Path parameters of the API route. + */ +export type GetRuleExecutionResultsRequestParams = t.TypeOf< + typeof GetRuleExecutionResultsRequestParams +>; +export const GetRuleExecutionResultsRequestParams = t.exact( + t.type({ + ruleId: NonEmptyString, + }) +); + +/** + * Query string parameters of the API route. + */ +export type GetRuleExecutionResultsRequestQuery = t.TypeOf< + typeof GetRuleExecutionResultsRequestQuery +>; +export const GetRuleExecutionResultsRequestQuery = t.exact( + t.type({ + start: IsoDateString, + end: IsoDateString, + query_text: DefaultEmptyString, // defaults to '' + status_filters: DefaultRuleExecutionStatusCsvArray, // defaults to [] + sort_field: DefaultSortField, // defaults to 'timestamp' + sort_order: DefaultSortOrderDesc, // defaults to 'desc' + page: DefaultPage, // defaults to 1 + per_page: DefaultPerPage, // defaults to 20 + }) +); diff --git a/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/api/get_rule_execution_results/response_schema.mock.ts b/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/api/get_rule_execution_results/response_schema.mock.ts new file mode 100644 index 0000000000000..fb6be3eeb62cc --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/api/get_rule_execution_results/response_schema.mock.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ruleExecutionResultMock } from '../../model/execution_result.mock'; +import type { GetRuleExecutionResultsResponse } from './response_schema'; + +const getSomeResponse = (): GetRuleExecutionResultsResponse => { + const results = ruleExecutionResultMock.getSomeResults(); + return { + events: results, + total: results.length, + }; +}; + +export const getRuleExecutionResultsResponseMock = { + getSomeResponse, +}; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/get_rule_execution_events_response.ts b/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/api/get_rule_execution_results/response_schema.ts similarity index 51% rename from x-pack/plugins/security_solution/common/detection_engine/schemas/response/get_rule_execution_events_response.ts rename to x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/api/get_rule_execution_results/response_schema.ts index 10be3a03814a3..7610a21d18181 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/get_rule_execution_events_response.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/api/get_rule_execution_results/response_schema.ts @@ -6,15 +6,15 @@ */ import * as t from 'io-ts'; -import { aggregateRuleExecutionEvent } from '../common'; +import { RuleExecutionResult } from '../../model/execution_result'; -export const GetAggregateRuleExecutionEventsResponse = t.exact( +/** + * Response body of the API route. + */ +export type GetRuleExecutionResultsResponse = t.TypeOf; +export const GetRuleExecutionResultsResponse = t.exact( t.type({ - events: t.array(aggregateRuleExecutionEvent), + events: t.array(RuleExecutionResult), total: t.number, }) ); - -export type GetAggregateRuleExecutionEventsResponse = t.TypeOf< - typeof GetAggregateRuleExecutionEventsResponse ->; diff --git a/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/api/urls.ts b/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/api/urls.ts new file mode 100644 index 0000000000000..595ca6c01d83a --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/api/urls.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { INTERNAL_DETECTION_ENGINE_URL as INTERNAL_URL } from '../../../constants'; + +export const GET_RULE_EXECUTION_EVENTS_URL = + `${INTERNAL_URL}/rules/{ruleId}/execution/events` as const; +export const getRuleExecutionEventsUrl = (ruleId: string) => + `${INTERNAL_URL}/rules/${ruleId}/execution/events` as const; + +export const GET_RULE_EXECUTION_RESULTS_URL = + `${INTERNAL_URL}/rules/{ruleId}/execution/results` as const; +export const getRuleExecutionResultsUrl = (ruleId: string) => + `${INTERNAL_URL}/rules/${ruleId}/execution/results` as const; diff --git a/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/index.ts b/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/index.ts new file mode 100644 index 0000000000000..b7881e2d2f524 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './api/get_rule_execution_events/request_schema'; +export * from './api/get_rule_execution_events/response_schema'; +export * from './api/get_rule_execution_results/request_schema'; +export * from './api/get_rule_execution_results/response_schema'; +export * from './api/urls'; + +export * from './model/execution_event'; +export * from './model/execution_metrics'; +export * from './model/execution_result'; +export * from './model/execution_settings'; +export * from './model/execution_status'; +export * from './model/execution_summary'; +export * from './model/log_level'; diff --git a/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/mocks.ts b/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/mocks.ts new file mode 100644 index 0000000000000..a552a0fc047f6 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/mocks.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './api/get_rule_execution_events/response_schema.mock'; +export * from './api/get_rule_execution_results/response_schema.mock'; + +export * from './model/execution_event.mock'; +export * from './model/execution_result.mock'; +export * from './model/execution_summary.mock'; diff --git a/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/model/execution_event.mock.ts b/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/model/execution_event.mock.ts new file mode 100644 index 0000000000000..0cb49804b13a6 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/model/execution_event.mock.ts @@ -0,0 +1,152 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { RuleExecutionEvent } from './execution_event'; +import { RuleExecutionEventType } from './execution_event'; +import { LogLevel } from './log_level'; + +const DEFAULT_TIMESTAMP = '2021-12-28T10:10:00.806Z'; +const DEFAULT_SEQUENCE_NUMBER = 0; + +const getMessageEvent = (props: Partial = {}): RuleExecutionEvent => { + return { + // Default values + timestamp: DEFAULT_TIMESTAMP, + sequence: DEFAULT_SEQUENCE_NUMBER, + level: LogLevel.debug, + message: 'Some message', + // Overriden values + ...props, + // Mandatory values for this type of event + type: RuleExecutionEventType.message, + }; +}; + +const getRunningStatusChange = (props: Partial = {}): RuleExecutionEvent => { + return { + // Default values + timestamp: DEFAULT_TIMESTAMP, + sequence: DEFAULT_SEQUENCE_NUMBER, + message: 'Rule changed status to "running"', + // Overriden values + ...props, + // Mandatory values for this type of event + level: LogLevel.info, + type: RuleExecutionEventType['status-change'], + }; +}; + +const getPartialFailureStatusChange = ( + props: Partial = {} +): RuleExecutionEvent => { + return { + // Default values + timestamp: DEFAULT_TIMESTAMP, + sequence: DEFAULT_SEQUENCE_NUMBER, + message: 'Rule changed status to "partial failure". Unknown error', + // Overriden values + ...props, + // Mandatory values for this type of event + level: LogLevel.warn, + type: RuleExecutionEventType['status-change'], + }; +}; + +const getFailedStatusChange = (props: Partial = {}): RuleExecutionEvent => { + return { + // Default values + timestamp: DEFAULT_TIMESTAMP, + sequence: DEFAULT_SEQUENCE_NUMBER, + message: 'Rule changed status to "failed". Unknown error', + // Overriden values + ...props, + // Mandatory values for this type of event + level: LogLevel.error, + type: RuleExecutionEventType['status-change'], + }; +}; + +const getSucceededStatusChange = (props: Partial = {}): RuleExecutionEvent => { + return { + // Default values + timestamp: DEFAULT_TIMESTAMP, + sequence: DEFAULT_SEQUENCE_NUMBER, + message: 'Rule changed status to "succeeded". Rule executed successfully', + // Overriden values + ...props, + // Mandatory values for this type of event + level: LogLevel.info, + type: RuleExecutionEventType['status-change'], + }; +}; + +const getExecutionMetricsEvent = (props: Partial = {}): RuleExecutionEvent => { + return { + // Default values + timestamp: DEFAULT_TIMESTAMP, + sequence: DEFAULT_SEQUENCE_NUMBER, + message: '', + // Overriden values + ...props, + // Mandatory values for this type of event + level: LogLevel.debug, + type: RuleExecutionEventType['execution-metrics'], + }; +}; + +const getSomeEvents = (): RuleExecutionEvent[] => [ + getSucceededStatusChange({ + timestamp: '2021-12-28T10:10:09.806Z', + sequence: 9, + }), + getExecutionMetricsEvent({ + timestamp: '2021-12-28T10:10:08.806Z', + sequence: 8, + }), + getRunningStatusChange({ + timestamp: '2021-12-28T10:10:07.806Z', + sequence: 7, + }), + getMessageEvent({ + timestamp: '2021-12-28T10:10:06.806Z', + sequence: 6, + level: LogLevel.debug, + message: 'Rule execution started', + }), + getFailedStatusChange({ + timestamp: '2021-12-28T10:10:05.806Z', + sequence: 5, + }), + getExecutionMetricsEvent({ + timestamp: '2021-12-28T10:10:04.806Z', + sequence: 4, + }), + getPartialFailureStatusChange({ + timestamp: '2021-12-28T10:10:03.806Z', + sequence: 3, + }), + getMessageEvent({ + timestamp: '2021-12-28T10:10:02.806Z', + sequence: 2, + level: LogLevel.error, + message: 'Some error', + }), + getRunningStatusChange({ + timestamp: '2021-12-28T10:10:01.806Z', + sequence: 1, + }), + getMessageEvent({ + timestamp: '2021-12-28T10:10:00.806Z', + sequence: 0, + level: LogLevel.debug, + message: 'Rule execution started', + }), +]; + +export const ruleExecutionEventMock = { + getSomeEvents, +}; diff --git a/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/model/execution_event.ts b/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/model/execution_event.ts new file mode 100644 index 0000000000000..0e269b39ff8f1 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/model/execution_event.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as t from 'io-ts'; +import { enumeration, IsoDateString } from '@kbn/securitysolution-io-ts-types'; +import { enumFromString } from '../../../utils/enum_from_string'; +import { TLogLevel } from './log_level'; + +/** + * Type of a plain rule execution event. + */ +export enum RuleExecutionEventType { + /** + * Simple log message of some log level, such as debug, info or error. + */ + 'message' = 'message', + + /** + * We log an event of this type each time a rule changes its status during an execution. + */ + 'status-change' = 'status-change', + + /** + * We log an event of this type at the end of a rule execution. It contains various execution + * metrics such as search and indexing durations. + */ + 'execution-metrics' = 'execution-metrics', +} + +export const TRuleExecutionEventType = enumeration( + 'RuleExecutionEventType', + RuleExecutionEventType +); + +/** + * An array of supported types of rule execution events. + */ +export const RULE_EXECUTION_EVENT_TYPES = Object.values(RuleExecutionEventType); + +export const ruleExecutionEventTypeFromString = enumFromString(RuleExecutionEventType); + +/** + * Plain rule execution event. A rule can write many of them during each execution. Events can be + * of different types and log levels. + * + * NOTE: This is a read model of rule execution events and it is pretty generic. It contains only a + * subset of their fields: only those fields that are common to all types of execution events. + */ +export type RuleExecutionEvent = t.TypeOf; +export const RuleExecutionEvent = t.type({ + timestamp: IsoDateString, + sequence: t.number, + level: TLogLevel, + type: TRuleExecutionEventType, + message: t.string, +}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/model/execution_metrics.ts b/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/model/execution_metrics.ts new file mode 100644 index 0000000000000..c6bb71970e599 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/model/execution_metrics.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as t from 'io-ts'; +import { PositiveInteger } from '@kbn/securitysolution-io-ts-types'; + +export type DurationMetric = t.TypeOf; +export const DurationMetric = PositiveInteger; + +export type RuleExecutionMetrics = t.TypeOf; +export const RuleExecutionMetrics = t.partial({ + total_search_duration_ms: DurationMetric, + total_indexing_duration_ms: DurationMetric, + execution_gap_duration_s: DurationMetric, +}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/model/execution_result.mock.ts b/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/model/execution_result.mock.ts new file mode 100644 index 0000000000000..4a039ca949c82 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/model/execution_result.mock.ts @@ -0,0 +1,176 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { RuleExecutionResult } from './execution_result'; + +const getSomeResults = (): RuleExecutionResult[] => [ + { + execution_uuid: 'dc45a63c-4872-4964-a2d0-bddd8b2e634d', + timestamp: '2022-04-28T21:19:08.047Z', + duration_ms: 3, + status: 'failure', + message: 'siem.queryRule:a6e61cf0-c737-11ec-9e32-e14913ffdd2d: execution failed', + num_active_alerts: 0, + num_new_alerts: 0, + num_recovered_alerts: 0, + num_triggered_actions: 0, + num_succeeded_actions: 0, + num_errored_actions: 0, + total_search_duration_ms: 0, + es_search_duration_ms: 0, + schedule_delay_ms: 2169, + timed_out: false, + indexing_duration_ms: 0, + search_duration_ms: 0, + gap_duration_s: 0, + security_status: 'failed', + security_message: 'Rule failed to execute because rule ran after it was disabled.', + }, + { + execution_uuid: '0fde9271-05d0-4bfb-8ff8-815756d28350', + timestamp: '2022-04-28T21:19:04.973Z', + duration_ms: 1446, + status: 'success', + message: + "rule executed: siem.queryRule:a6e61cf0-c737-11ec-9e32-e14913ffdd2d: 'Click here for hot fresh alerts!'", + num_active_alerts: 0, + num_new_alerts: 0, + num_recovered_alerts: 0, + num_triggered_actions: 0, + num_succeeded_actions: 0, + num_errored_actions: 0, + total_search_duration_ms: 0, + es_search_duration_ms: 0, + schedule_delay_ms: 2089, + timed_out: false, + indexing_duration_ms: 0, + search_duration_ms: 2, + gap_duration_s: 0, + security_status: 'succeeded', + security_message: 'succeeded', + }, + { + execution_uuid: '5daaa259-ded8-4a52-853e-1e7652d325d5', + timestamp: '2022-04-28T21:19:01.976Z', + duration_ms: 1395, + status: 'success', + message: + "rule executed: siem.queryRule:a6e61cf0-c737-11ec-9e32-e14913ffdd2d: 'Click here for hot fresh alerts!'", + num_active_alerts: 0, + num_new_alerts: 0, + num_recovered_alerts: 0, + num_triggered_actions: 0, + num_succeeded_actions: 0, + num_errored_actions: 0, + total_search_duration_ms: 0, + es_search_duration_ms: 1, + schedule_delay_ms: 2637, + timed_out: false, + indexing_duration_ms: 0, + search_duration_ms: 3, + gap_duration_s: 0, + security_status: 'succeeded', + security_message: 'succeeded', + }, + { + execution_uuid: 'c7223e1c-4264-4a27-8697-0d720243fafc', + timestamp: '2022-04-28T21:18:58.431Z', + duration_ms: 1815, + status: 'success', + message: + "rule executed: siem.queryRule:a6e61cf0-c737-11ec-9e32-e14913ffdd2d: 'Click here for hot fresh alerts!'", + num_active_alerts: 0, + num_new_alerts: 0, + num_recovered_alerts: 0, + num_triggered_actions: 0, + num_succeeded_actions: 0, + num_errored_actions: 0, + total_search_duration_ms: 0, + es_search_duration_ms: 1, + schedule_delay_ms: -255429, + timed_out: false, + indexing_duration_ms: 0, + search_duration_ms: 3, + gap_duration_s: 0, + security_status: 'succeeded', + security_message: 'succeeded', + }, + { + execution_uuid: '1f6ba0c1-cc36-4f45-b919-7790b8a8d670', + timestamp: '2022-04-28T21:18:13.954Z', + duration_ms: 2055, + status: 'success', + message: + "rule executed: siem.queryRule:a6e61cf0-c737-11ec-9e32-e14913ffdd2d: 'Click here for hot fresh alerts!'", + num_active_alerts: 0, + num_new_alerts: 0, + num_recovered_alerts: 0, + num_triggered_actions: 0, + num_succeeded_actions: 0, + num_errored_actions: 0, + total_search_duration_ms: 0, + es_search_duration_ms: 0, + schedule_delay_ms: 2027, + timed_out: false, + indexing_duration_ms: 0, + search_duration_ms: 0, + gap_duration_s: 0, + security_status: 'partial failure', + security_message: + 'Check privileges failed to execute ResponseError: index_not_found_exception: [index_not_found_exception] Reason: no such index [yup] name: "Click here for hot fresh alerts!" id: "a6e61cf0-c737-11ec-9e32-e14913ffdd2d" rule id: "34946b12-88d1-49ef-82b7-9cad45972030" execution id: "1f6ba0c1-cc36-4f45-b919-7790b8a8d670" space ID: "default"', + }, + { + execution_uuid: 'b0f65d64-b229-432b-9d39-f4385a7f9368', + timestamp: '2022-04-28T21:15:43.086Z', + duration_ms: 1205, + status: 'success', + message: + "rule executed: siem.queryRule:a6e61cf0-c737-11ec-9e32-e14913ffdd2d: 'Click here for hot fresh alerts!'", + num_active_alerts: 0, + num_new_alerts: 0, + num_recovered_alerts: 0, + num_triggered_actions: 0, + num_succeeded_actions: 0, + num_errored_actions: 0, + total_search_duration_ms: 0, + es_search_duration_ms: 672, + schedule_delay_ms: 3086, + timed_out: false, + indexing_duration_ms: 140, + search_duration_ms: 684, + gap_duration_s: 0, + security_status: 'succeeded', + security_message: 'succeeded', + }, + { + execution_uuid: '7bfd25b9-c0d8-44b1-982c-485169466a8e', + timestamp: '2022-04-28T21:10:40.135Z', + duration_ms: 6321, + status: 'success', + message: + "rule executed: siem.queryRule:a6e61cf0-c737-11ec-9e32-e14913ffdd2d: 'Click here for hot fresh alerts!'", + num_active_alerts: 0, + num_new_alerts: 0, + num_recovered_alerts: 0, + num_triggered_actions: 0, + num_succeeded_actions: 0, + num_errored_actions: 0, + total_search_duration_ms: 0, + es_search_duration_ms: 930, + schedule_delay_ms: 1222, + timed_out: false, + indexing_duration_ms: 2103, + search_duration_ms: 946, + gap_duration_s: 0, + security_status: 'succeeded', + security_message: 'succeeded', + }, +]; + +export const ruleExecutionResultMock = { + getSomeResults, +}; diff --git a/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/model/execution_result.ts b/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/model/execution_result.ts new file mode 100644 index 0000000000000..3a15121624b75 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/model/execution_result.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as t from 'io-ts'; +import { IsoDateString } from '@kbn/securitysolution-io-ts-types'; + +/** + * Rule execution result is an aggregate that groups plain rule execution events by execution UUID. + * It contains such information as execution UUID, date, status and metrics. + */ +export type RuleExecutionResult = t.TypeOf; +export const RuleExecutionResult = t.type({ + execution_uuid: t.string, + timestamp: IsoDateString, + duration_ms: t.number, + status: t.string, + message: t.string, + num_active_alerts: t.number, + num_new_alerts: t.number, + num_recovered_alerts: t.number, + num_triggered_actions: t.number, + num_succeeded_actions: t.number, + num_errored_actions: t.number, + total_search_duration_ms: t.number, + es_search_duration_ms: t.number, + schedule_delay_ms: t.number, + timed_out: t.boolean, + indexing_duration_ms: t.number, + search_duration_ms: t.number, + gap_duration_s: t.number, + security_status: t.string, + security_message: t.string, +}); + +/** + * We support sorting rule execution results by these fields. + */ +export type SortFieldOfRuleExecutionResult = t.TypeOf; +export const SortFieldOfRuleExecutionResult = t.keyof({ + timestamp: IsoDateString, + duration_ms: t.number, + gap_duration_s: t.number, + indexing_duration_ms: t.number, + search_duration_ms: t.number, + schedule_delay_ms: t.number, +}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/model/execution_settings.ts b/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/model/execution_settings.ts new file mode 100644 index 0000000000000..e1ac664f0b537 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/model/execution_settings.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface RuleExecutionSettings { + extendedLogging: { + isEnabled: boolean; + minLevel: LogLevelSetting; + }; +} + +export enum LogLevelSetting { + 'trace' = 'trace', + 'debug' = 'debug', + 'info' = 'info', + 'warn' = 'warn', + 'error' = 'error', + 'off' = 'off', +} diff --git a/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/model/execution_status.ts b/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/model/execution_status.ts new file mode 100644 index 0000000000000..22e599b18c541 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/model/execution_status.ts @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type * as t from 'io-ts'; +import { enumeration, PositiveInteger } from '@kbn/securitysolution-io-ts-types'; +import { assertUnreachable } from '../../../utility_types'; + +/** + * Custom execution status of Security rules that is different from the status + * used in the Alerting Framework. We merge our custom status with the + * Framework's status to determine the resulting status of a rule. + */ +export enum RuleExecutionStatus { + /** + * @deprecated Replaced by the 'running' status but left for backwards compatibility + * with rule execution events already written to Event Log in the prior versions of Kibana. + * Don't use when writing rule status changes. + */ + 'going to run' = 'going to run', + + /** + * Rule execution started but not reached any intermediate or final status. + */ + 'running' = 'running', + + /** + * Rule can partially fail for various reasons either in the middle of an execution + * (in this case we update its status right away) or in the end of it. So currently + * this status can be both intermediate and final at the same time. + * A typical reason for a partial failure: not all the indices that the rule searches + * over actually exist. + */ + 'partial failure' = 'partial failure', + + /** + * Rule failed to execute due to unhandled exception or a reason defined in the + * business logic of its executor function. + */ + 'failed' = 'failed', + + /** + * Rule executed successfully without any issues. Note: this status is just an indication + * of a rule's "health". The rule might or might not generate any alerts despite of it. + */ + 'succeeded' = 'succeeded', +} + +export const TRuleExecutionStatus = enumeration('RuleExecutionStatus', RuleExecutionStatus); + +/** + * An array of supported rule execution statuses. + */ +export const RULE_EXECUTION_STATUSES = Object.values(RuleExecutionStatus); + +export type RuleExecutionStatusOrder = t.TypeOf; +export const RuleExecutionStatusOrder = PositiveInteger; + +export const ruleExecutionStatusToNumber = ( + status: RuleExecutionStatus +): RuleExecutionStatusOrder => { + switch (status) { + case RuleExecutionStatus.succeeded: + return 0; + case RuleExecutionStatus['going to run']: + return 10; + case RuleExecutionStatus.running: + return 15; + case RuleExecutionStatus['partial failure']: + return 20; + case RuleExecutionStatus.failed: + return 30; + default: + assertUnreachable(status); + return 0; + } +}; diff --git a/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/model/execution_summary.mock.ts b/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/model/execution_summary.mock.ts new file mode 100644 index 0000000000000..3224b84ba2cef --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/model/execution_summary.mock.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { RuleExecutionStatus } from './execution_status'; +import type { RuleExecutionSummary } from './execution_summary'; + +const getSummarySucceeded = (): RuleExecutionSummary => ({ + last_execution: { + date: '2020-02-18T15:26:49.783Z', + status: RuleExecutionStatus.succeeded, + status_order: 0, + message: 'succeeded', + metrics: { + total_search_duration_ms: 200, + total_indexing_duration_ms: 800, + execution_gap_duration_s: 500, + }, + }, +}); + +const getSummaryFailed = (): RuleExecutionSummary => ({ + last_execution: { + date: '2020-02-18T15:15:58.806Z', + status: RuleExecutionStatus.failed, + status_order: 30, + message: + 'Signal rule name: "Query with a rule id Number 1", id: "1ea5a820-4da1-4e82-92a1-2b43a7bece08", rule_id: "query-rule-id-1" has a time gap of 5 days (412682928ms), and could be missing signals within that time. Consider increasing your look behind time or adding more Kibana instances.', + metrics: { + total_search_duration_ms: 200, + total_indexing_duration_ms: 800, + execution_gap_duration_s: 500, + }, + }, +}); + +export const ruleExecutionSummaryMock = { + getSummarySucceeded, + getSummaryFailed, +}; diff --git a/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/model/execution_summary.ts b/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/model/execution_summary.ts new file mode 100644 index 0000000000000..bbe9e9ad5d7e7 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/model/execution_summary.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as t from 'io-ts'; +import { IsoDateString } from '@kbn/securitysolution-io-ts-types'; +import { TRuleExecutionStatus, RuleExecutionStatusOrder } from './execution_status'; +import { RuleExecutionMetrics } from './execution_metrics'; + +export type RuleExecutionSummary = t.TypeOf; +export const RuleExecutionSummary = t.type({ + last_execution: t.type({ + date: IsoDateString, + status: TRuleExecutionStatus, + status_order: RuleExecutionStatusOrder, + message: t.string, + metrics: RuleExecutionMetrics, + }), +}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/model/log_level.ts b/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/model/log_level.ts new file mode 100644 index 0000000000000..b37ce62ad4891 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/rule_monitoring/model/log_level.ts @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { enumeration } from '@kbn/securitysolution-io-ts-types'; +import { enumFromString } from '../../../utils/enum_from_string'; +import { assertUnreachable } from '../../../utility_types'; +import { RuleExecutionStatus } from './execution_status'; + +export enum LogLevel { + 'trace' = 'trace', + 'debug' = 'debug', + 'info' = 'info', + 'warn' = 'warn', + 'error' = 'error', +} + +export const TLogLevel = enumeration('LogLevel', LogLevel); + +/** + * An array of supported log levels. + */ +export const LOG_LEVELS = Object.values(LogLevel); + +export const logLevelToNumber = (level: keyof typeof LogLevel | null | undefined): number => { + if (!level) { + return 0; + } + + switch (level) { + case 'trace': + return 0; + case 'debug': + return 10; + case 'info': + return 20; + case 'warn': + return 30; + case 'error': + return 40; + default: + assertUnreachable(level); + return 0; + } +}; + +export const logLevelFromNumber = (num: number | null | undefined): LogLevel => { + if (num === null || num === undefined || num < 10) { + return LogLevel.trace; + } + if (num < 20) { + return LogLevel.debug; + } + if (num < 30) { + return LogLevel.info; + } + if (num < 40) { + return LogLevel.warn; + } + return LogLevel.error; +}; + +export const logLevelFromString = enumFromString(LogLevel); + +export const logLevelFromExecutionStatus = (status: RuleExecutionStatus): LogLevel => { + switch (status) { + case RuleExecutionStatus['going to run']: + case RuleExecutionStatus.running: + case RuleExecutionStatus.succeeded: + return LogLevel.info; + case RuleExecutionStatus['partial failure']: + return LogLevel.warn; + case RuleExecutionStatus.failed: + return LogLevel.error; + default: + assertUnreachable(status); + return LogLevel.trace; + } +}; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/index.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/index.ts index b7f96bfdafdad..ad8745a8caf21 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/index.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/index.ts @@ -6,6 +6,7 @@ */ export * from './installed_integrations'; -export * from './rule_monitoring'; +export * from './pagination'; export * from './rule_params'; export * from './schemas'; +export * from './sorting'; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/pagination.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/pagination.ts new file mode 100644 index 0000000000000..bed2cade86df4 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/pagination.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as t from 'io-ts'; +import { PositiveInteger, PositiveIntegerGreaterThanZero } from '@kbn/securitysolution-io-ts-types'; + +export type Page = t.TypeOf; +export const Page = PositiveIntegerGreaterThanZero; + +export type PageOrUndefined = t.TypeOf; +export const PageOrUndefined = t.union([Page, t.undefined]); + +export type PerPage = t.TypeOf; +export const PerPage = PositiveInteger; + +export type PerPageOrUndefined = t.TypeOf; +export const PerPageOrUndefined = t.union([PerPage, t.undefined]); + +export type PaginationResult = t.TypeOf; +export const PaginationResult = t.type({ + page: Page, + per_page: PerPage, + total: PositiveInteger, +}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/rule_monitoring.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/rule_monitoring.ts deleted file mode 100644 index af005d1b60a8f..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/rule_monitoring.ts +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as t from 'io-ts'; -import { enumeration, IsoDateString, PositiveInteger } from '@kbn/securitysolution-io-ts-types'; - -// ------------------------------------------------------------------------------------------------- -// Rule execution status - -/** - * Custom execution status of Security rules that is different from the status - * used in the Alerting Framework. We merge our custom status with the - * Framework's status to determine the resulting status of a rule. - */ -export enum RuleExecutionStatus { - /** - * @deprecated Replaced by the 'running' status but left for backwards compatibility - * with rule execution events already written to Event Log in the prior versions of Kibana. - * Don't use when writing rule status changes. - */ - 'going to run' = 'going to run', - - /** - * Rule execution started but not reached any intermediate or final status. - */ - 'running' = 'running', - - /** - * Rule can partially fail for various reasons either in the middle of an execution - * (in this case we update its status right away) or in the end of it. So currently - * this status can be both intermediate and final at the same time. - * A typical reason for a partial failure: not all the indices that the rule searches - * over actually exist. - */ - 'partial failure' = 'partial failure', - - /** - * Rule failed to execute due to unhandled exception or a reason defined in the - * business logic of its executor function. - */ - 'failed' = 'failed', - - /** - * Rule executed successfully without any issues. Note: this status is just an indication - * of a rule's "health". The rule might or might not generate any alerts despite of it. - */ - 'succeeded' = 'succeeded', -} - -export const ruleExecutionStatus = enumeration('RuleExecutionStatus', RuleExecutionStatus); - -export const ruleExecutionStatusOrder = PositiveInteger; -export type RuleExecutionStatusOrder = t.TypeOf; - -export const ruleExecutionStatusOrderByStatus: Record< - RuleExecutionStatus, - RuleExecutionStatusOrder -> = { - [RuleExecutionStatus.succeeded]: 0, - [RuleExecutionStatus['going to run']]: 10, - [RuleExecutionStatus.running]: 15, - [RuleExecutionStatus['partial failure']]: 20, - [RuleExecutionStatus.failed]: 30, -}; - -// ------------------------------------------------------------------------------------------------- -// Rule execution metrics - -export const durationMetric = PositiveInteger; -export type DurationMetric = t.TypeOf; - -export const ruleExecutionMetrics = t.partial({ - total_search_duration_ms: durationMetric, - total_indexing_duration_ms: durationMetric, - execution_gap_duration_s: durationMetric, -}); - -export type RuleExecutionMetrics = t.TypeOf; - -// ------------------------------------------------------------------------------------------------- -// Rule execution summary - -export const ruleExecutionSummary = t.type({ - last_execution: t.type({ - date: IsoDateString, - status: ruleExecutionStatus, - status_order: ruleExecutionStatusOrder, - message: t.string, - metrics: ruleExecutionMetrics, - }), -}); - -export type RuleExecutionSummary = t.TypeOf; - -// ------------------------------------------------------------------------------------------------- -// Rule execution events - -export const ruleExecutionEvent = t.type({ - date: IsoDateString, - status: ruleExecutionStatus, - message: t.string, -}); - -export type RuleExecutionEvent = t.TypeOf; - -// ------------------------------------------------------------------------------------------------- -// Aggregate Rule execution events - -export const aggregateRuleExecutionEvent = t.type({ - execution_uuid: t.string, - timestamp: IsoDateString, - duration_ms: t.number, - status: t.string, - message: t.string, - num_active_alerts: t.number, - num_new_alerts: t.number, - num_recovered_alerts: t.number, - num_triggered_actions: t.number, - num_succeeded_actions: t.number, - num_errored_actions: t.number, - total_search_duration_ms: t.number, - es_search_duration_ms: t.number, - schedule_delay_ms: t.number, - timed_out: t.boolean, - indexing_duration_ms: t.number, - search_duration_ms: t.number, - gap_duration_s: t.number, - security_status: t.string, - security_message: t.string, -}); - -export type AggregateRuleExecutionEvent = t.TypeOf; - -export const executionLogTableSortColumns = t.keyof({ - timestamp: IsoDateString, - duration_ms: t.number, - gap_duration_s: t.number, - indexing_duration_ms: t.number, - search_duration_ms: t.number, - schedule_delay_ms: t.number, -}); - -export type ExecutionLogTableSortColumns = t.TypeOf; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts index 4f583dba23abf..b0cbd63d8db42 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts @@ -193,36 +193,12 @@ export type QueryFilterOrUndefined = t.TypeOf; export const references = t.array(t.string); export type References = t.TypeOf; -export const per_page = PositiveInteger; -export type PerPage = t.TypeOf; - -export const perPageOrUndefined = t.union([per_page, t.undefined]); -export type PerPageOrUndefined = t.TypeOf; - -export const page = PositiveIntegerGreaterThanZero; -export type Page = t.TypeOf; - -export const pageOrUndefined = t.union([page, t.undefined]); -export type PageOrUndefined = t.TypeOf; - export const signal_ids = t.array(t.string); export type SignalIds = t.TypeOf; // TODO: Can this be more strict or is this is the set of all Elastic Queries? export const signal_status_query = t.object; -export const sort_field = t.string; -export type SortField = t.TypeOf; - -export const sortFieldOrUndefined = t.union([sort_field, t.undefined]); -export type SortFieldOrUndefined = t.TypeOf; - -export const sort_order = t.keyof({ asc: null, desc: null }); -export type SortOrder = t.TypeOf; - -export const sortOrderOrUndefined = t.union([sort_order, t.undefined]); -export type SortOrderOrUndefined = t.TypeOf; - export const tags = t.array(t.string); export type Tags = t.TypeOf; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/sorting.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/sorting.test.ts new file mode 100644 index 0000000000000..f0d6638740e32 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/sorting.test.ts @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; +import { DefaultSortOrderAsc, DefaultSortOrderDesc } from './sorting'; + +describe('Common sorting schemas', () => { + describe('DefaultSortOrderAsc', () => { + describe('Validation succeeds', () => { + it('when valid sort order is passed', () => { + const payload = 'desc'; + const decoded = DefaultSortOrderAsc.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + }); + + describe('Validation fails', () => { + it('when invalid sort order is passed', () => { + const payload = 'behind_you'; + const decoded = DefaultSortOrderAsc.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "behind_you" supplied to "DefaultSortOrderAsc"', + ]); + expect(message.schema).toEqual({}); + }); + }); + + describe('Validation sets the default sort order "asc"', () => { + it('when sort order is not passed', () => { + const payload = undefined; + const decoded = DefaultSortOrderAsc.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual('asc'); + }); + }); + }); + + describe('DefaultSortOrderDesc', () => { + describe('Validation succeeds', () => { + it('when valid sort order is passed', () => { + const payload = 'asc'; + const decoded = DefaultSortOrderDesc.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + }); + + describe('Validation fails', () => { + it('when invalid sort order is passed', () => { + const payload = 'behind_you'; + const decoded = DefaultSortOrderDesc.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "behind_you" supplied to "DefaultSortOrderDesc"', + ]); + expect(message.schema).toEqual({}); + }); + }); + + describe('Validation sets the default sort order "desc"', () => { + it('when sort order is not passed', () => { + const payload = null; + const decoded = DefaultSortOrderDesc.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual('desc'); + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/sorting.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/sorting.ts new file mode 100644 index 0000000000000..2cf1712e5ffbc --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/sorting.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as t from 'io-ts'; +import type { Either } from 'fp-ts/lib/Either'; +import { capitalize } from 'lodash'; + +export type SortField = t.TypeOf; +export const SortField = t.string; + +export type SortFieldOrUndefined = t.TypeOf; +export const SortFieldOrUndefined = t.union([SortField, t.undefined]); + +export type SortOrder = t.TypeOf; +export const SortOrder = t.keyof({ asc: null, desc: null }); + +export type SortOrderOrUndefined = t.TypeOf; +export const SortOrderOrUndefined = t.union([SortOrder, t.undefined]); + +const defaultSortOrder = (order: SortOrder): t.Type => { + return new t.Type( + `DefaultSortOrder${capitalize(order)}`, + SortOrder.is, + (input, context): Either => + input == null ? t.success(order) : SortOrder.validate(input, context), + t.identity + ); +}; + +/** + * Types the DefaultSortOrderAsc as: + * - If undefined, then a default sort order of 'asc' will be set + * - If a string is sent in, then the string will be validated to ensure it's a valid SortOrder + */ +export const DefaultSortOrderAsc = defaultSortOrder('asc'); + +/** + * Types the DefaultSortOrderDesc as: + * - If undefined, then a default sort order of 'desc' will be set + * - If a string is sent in, then the string will be validated to ensure it's a valid SortOrder + */ +export const DefaultSortOrderDesc = defaultSortOrder('desc'); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/find_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/find_rules_schema.ts index 885fe22b1ccb2..39f0105a2a88f 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/find_rules_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/find_rules_schema.ts @@ -8,22 +8,22 @@ import * as t from 'io-ts'; import { DefaultPerPage, DefaultPage } from '@kbn/securitysolution-io-ts-alerting-types'; -import type { PerPage, Page } from '../common/schemas'; -import { queryFilter, fields, sort_field, sort_order } from '../common/schemas'; +import type { PerPage, Page } from '../common'; +import { queryFilter, fields, SortField, SortOrder } from '../common'; export const findRulesSchema = t.exact( t.partial({ fields, filter: queryFilter, - per_page: DefaultPerPage, // defaults to "20" if not sent in during decode - page: DefaultPage, // defaults to "1" if not sent in during decode - sort_field, - sort_order, + sort_field: SortField, + sort_order: SortOrder, + page: DefaultPage, // defaults to 1 + per_page: DefaultPerPage, // defaults to 20 }) ); export type FindRulesSchema = t.TypeOf; export type FindRulesSchemaDecoded = Omit & { - per_page: PerPage; page: Page; + per_page: PerPage; }; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/get_rule_execution_events_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/get_rule_execution_events_schema.test.ts deleted file mode 100644 index 05a3c6123c256..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/get_rule_execution_events_schema.test.ts +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { pipe } from 'fp-ts/lib/pipeable'; -import { left } from 'fp-ts/lib/Either'; -import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; -import { - DefaultSortField, - DefaultSortOrder, - DefaultStatusFiltersStringArray, -} from './get_rule_execution_events_schema'; - -describe('get_rule_execution_events_schema', () => { - describe('DefaultStatusFiltersStringArray', () => { - test('it should validate a single ruleExecutionStatus', () => { - const payload = 'succeeded'; - const decoded = DefaultStatusFiltersStringArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual([payload]); - }); - test('it should validate an array of ruleExecutionStatus joined by "\'"', () => { - const payload = ['succeeded', 'failed']; - const decoded = DefaultStatusFiltersStringArray.decode(payload.join(',')); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should not validate an invalid ruleExecutionStatus', () => { - const payload = ['value 1', 5].join(','); - const decoded = DefaultStatusFiltersStringArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "value 1" supplied to "DefaultStatusFiltersStringArray"', - 'Invalid value "5" supplied to "DefaultStatusFiltersStringArray"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should return a default array entry', () => { - const payload = null; - const decoded = DefaultStatusFiltersStringArray.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual([]); - }); - }); - describe('DefaultSortField', () => { - test('it should validate a valid sort field', () => { - const payload = 'duration_ms'; - const decoded = DefaultSortField.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should not validate an invalid sort field', () => { - const payload = 'es_search_duration_ms'; - const decoded = DefaultSortField.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "es_search_duration_ms" supplied to "DefaultSortField"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should return the default sort field "timestamp"', () => { - const payload = null; - const decoded = DefaultSortField.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual('timestamp'); - }); - }); - describe('DefaultSortOrder', () => { - test('it should validate a valid sort order', () => { - const payload = 'asc'; - const decoded = DefaultSortOrder.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual(payload); - }); - - test('it should not validate an invalid sort order', () => { - const payload = 'behind_you'; - const decoded = DefaultSortOrder.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([ - 'Invalid value "behind_you" supplied to "DefaultSortOrder"', - ]); - expect(message.schema).toEqual({}); - }); - - test('it should return the default sort order "desc"', () => { - const payload = null; - const decoded = DefaultSortOrder.decode(payload); - const message = pipe(decoded, foldLeftRight); - - expect(getPaths(left(message.errors))).toEqual([]); - expect(message.schema).toEqual('desc'); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/get_rule_execution_events_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/get_rule_execution_events_schema.ts deleted file mode 100644 index 520f4555fb672..0000000000000 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/get_rule_execution_events_schema.ts +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as t from 'io-ts'; - -import { DefaultPerPage, DefaultPage } from '@kbn/securitysolution-io-ts-alerting-types'; -import { DefaultEmptyString, IsoDateString } from '@kbn/securitysolution-io-ts-types'; - -import type { Either } from 'fp-ts/lib/Either'; -import type { ExecutionLogTableSortColumns, RuleExecutionStatus } from '../common'; -import { executionLogTableSortColumns, ruleExecutionStatus } from '../common'; - -/** - * Types the DefaultStatusFiltersStringArray as: - * - If undefined, then a default array will be set - * - If an array is sent in, then the array will be validated to ensure all elements are a ruleExecutionStatus (or that the array is empty) - */ -export const DefaultStatusFiltersStringArray = new t.Type< - RuleExecutionStatus[], - RuleExecutionStatus[], - unknown ->( - 'DefaultStatusFiltersStringArray', - t.array(ruleExecutionStatus).is, - (input, context): Either => { - if (input == null) { - return t.success([]); - } else if (typeof input === 'string') { - if (input === '') { - return t.success([]); - } else { - return t.array(ruleExecutionStatus).validate(input.split(','), context); - } - } else { - return t.array(ruleExecutionStatus).validate(input, context); - } - }, - t.identity -); - -/** - * Types the DefaultSortField as: - * - If undefined, then a default sort field of 'timestamp' will be set - * - If a string is sent in, then the string will be validated to ensure it is as valid sortFields - */ -export const DefaultSortField = new t.Type< - ExecutionLogTableSortColumns, - ExecutionLogTableSortColumns, - unknown ->( - 'DefaultSortField', - executionLogTableSortColumns.is, - (input, context): Either => - input == null ? t.success('timestamp') : executionLogTableSortColumns.validate(input, context), - t.identity -); - -const sortOrder = t.keyof({ asc: null, desc: null }); -type SortOrder = t.TypeOf; - -/** - * Types the DefaultSortOrder as: - * - If undefined, then a default sort order of 'desc' will be set - * - If a string is sent in, then the string will be validated to ensure it is as valid sortOrder - */ -export const DefaultSortOrder = new t.Type( - 'DefaultSortOrder', - sortOrder.is, - (input, context): Either => - input == null ? t.success('desc') : sortOrder.validate(input, context), - t.identity -); - -/** - * Route Request Params - */ -export const GetRuleExecutionEventsRequestParams = t.exact( - t.type({ - ruleId: t.string, - }) -); - -/** - * Route Query Params (as constructed from the above codecs) - */ -export const GetRuleExecutionEventsQueryParams = t.exact( - t.type({ - start: IsoDateString, - end: IsoDateString, - query_text: DefaultEmptyString, // default to "" if not sent in during decode - status_filters: DefaultStatusFiltersStringArray, // defaults to empty array if not sent in during decode - per_page: DefaultPerPage, // defaults to "20" if not sent in during decode - page: DefaultPage, // defaults to "1" if not sent in during decode - sort_field: DefaultSortField, // defaults to "desc" if not sent in during decode - sort_order: DefaultSortOrder, // defaults to "timestamp" if not sent in during decode - }) -); - -export type GetRuleExecutionEventsRequestParams = t.TypeOf< - typeof GetRuleExecutionEventsRequestParams ->; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/index.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/index.ts index 7722feb5f080d..9c1a2581b2347 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/index.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/index.ts @@ -12,9 +12,9 @@ export * from './find_rules_schema'; export * from './import_rules_schema'; export * from './patch_rules_bulk_schema'; export * from './patch_rules_schema'; +export * from './perform_bulk_action_schema'; export * from './query_rules_schema'; export * from './query_signals_index_schema'; +export * from './rule_schemas'; export * from './set_signal_status_schema'; export * from './update_rules_bulk_schema'; -export * from './rule_schemas'; -export * from './perform_bulk_action_schema'; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts index 95dc6e7f4e65d..fffbbb8078705 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts @@ -29,6 +29,7 @@ import { import { listArray } from '@kbn/securitysolution-io-ts-list-types'; import { version } from '@kbn/securitysolution-io-ts-types'; +import { RuleExecutionSummary } from '../../rule_monitoring'; import { id, index, @@ -70,7 +71,6 @@ import { created_at, created_by, namespace, - ruleExecutionSummary, RelatedIntegrationArray, RequiredFieldArray, SetupGuide, @@ -486,7 +486,7 @@ const responseRequiredFields = { }; const responseOptionalFields = { - execution_summary: ruleExecutionSummary, + execution_summary: RuleExecutionSummary, }; export const fullResponseSchema = t.intersection([ diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/index.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/index.ts index e88f07faa2684..1b688ce641a7a 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/index.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/index.ts @@ -7,7 +7,6 @@ export * from './error_schema'; export * from './get_installed_integrations_response_schema'; -export * from './get_rule_execution_events_response'; export * from './import_rules_schema'; export * from './prepackaged_rules_schema'; export * from './prepackaged_rules_status_schema'; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts index 1566aa3c23858..794ef71bf0536 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts @@ -34,10 +34,11 @@ import { max_signals, } from '@kbn/securitysolution-io-ts-alerting-types'; import { DefaultStringArray, version } from '@kbn/securitysolution-io-ts-types'; - import { DefaultListArray } from '@kbn/securitysolution-io-ts-list-types'; + import { isMlRule } from '../../../machine_learning/helpers'; import { isThresholdRule } from '../../utils'; +import { RuleExecutionSummary } from '../../rule_monitoring'; import { anomaly_threshold, data_view_id, @@ -77,7 +78,6 @@ import { rule_name_override, timestamp_override, namespace, - ruleExecutionSummary, RelatedIntegrationArray, RequiredFieldArray, SetupGuide, @@ -189,7 +189,7 @@ export const partialRulesSchema = t.partial({ namespace, note, uuid: id, // Move to 'required' post-migration - execution_summary: ruleExecutionSummary, + execution_summary: RuleExecutionSummary, }); /** diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts index e7c0a711100fb..828f790364c32 100644 --- a/x-pack/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/plugins/security_solution/common/experimental_features.ts @@ -37,6 +37,7 @@ export const allowedExperimentalValues = Object.freeze({ * Enables the Endpoint response actions console in various areas of the app */ responseActionsConsoleEnabled: true, + /** * Enables the cloud security posture navigation inside the security solution */ @@ -46,6 +47,15 @@ export const allowedExperimentalValues = Object.freeze({ * Enables the insights module for related alerts by process ancestry */ insightsRelatedAlertsByProcessAncestry: false, + + /** + * Enables extended rule execution logging to Event Log. When this setting is enabled: + * - Rules write their console error, info, debug, and trace messages to Event Log, + * in addition to other events they log there (status changes and execution metrics). + * - We add a Kibana Advanced Setting that controls this behavior (on/off and log level). + * - We show a table with plain execution logs on the Rule Details page. + */ + extendedRuleExecutionLoggingEnabled: false, }); type ExperimentalConfigKeys = Array; diff --git a/x-pack/plugins/security_solution/common/types/timeline/index.ts b/x-pack/plugins/security_solution/common/types/timeline/index.ts index 223ec4d24cf00..cd90c314ef10c 100644 --- a/x-pack/plugins/security_solution/common/types/timeline/index.ts +++ b/x-pack/plugins/security_solution/common/types/timeline/index.ts @@ -311,13 +311,11 @@ export type TimelineWithoutExternalRefs = Omit { + enum TestStringEnum { + 'foo' = 'foo', + 'bar' = 'bar', + } + + const testEnumFromString = enumFromString(TestStringEnum); + + it('returns enum if provided with a known value', () => { + expect(testEnumFromString('foo')).toEqual(TestStringEnum.foo); + expect(testEnumFromString('bar')).toEqual(TestStringEnum.bar); + }); + + it('returns null if provided with an unknown value', () => { + expect(testEnumFromString('xyz')).toEqual(null); + expect(testEnumFromString('123')).toEqual(null); + }); + + it('returns null if provided with null', () => { + expect(testEnumFromString(null)).toEqual(null); + }); + + it('returns null if provided with undefined', () => { + expect(testEnumFromString(undefined)).toEqual(null); + }); +}); diff --git a/x-pack/plugins/security_solution/common/utils/enum_from_string.ts b/x-pack/plugins/security_solution/common/utils/enum_from_string.ts new file mode 100644 index 0000000000000..1651fba365bfa --- /dev/null +++ b/x-pack/plugins/security_solution/common/utils/enum_from_string.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +interface Enum { + [s: string]: T; +} + +/** + * WARNING: It works only with string enums. + * https://www.typescriptlang.org/docs/handbook/enums.html#string-enums + * + * Converts a string into a corresponding enum value. + * Returns null if the value is not in the enum. + * + * @param enm Specified enum. + * @returns Enum value or null. + * + * @example + * enum MyEnum { + * 'foo' = 'foo', + * 'bar' = 'bar', + * } + * + * const foo = enumFromString(MyEnum)('foo'); // MyEnum.foo + * const bar = enumFromString(MyEnum)('bar'); // MyEnum.bar + * const unknown = enumFromString(MyEnum)('xyz'); // null + */ +export const enumFromString = (enm: Enum) => { + const supportedEnumValues = Object.values(enm) as unknown as string[]; + return (value: string | null | undefined): T | null => { + return value && supportedEnumValues.includes(value) ? (value as unknown as T) : null; + }; +}; diff --git a/x-pack/plugins/security_solution/common/utils/to_array.ts b/x-pack/plugins/security_solution/common/utils/to_array.ts index fbb2b8d48a250..b6945708ff0db 100644 --- a/x-pack/plugins/security_solution/common/utils/to_array.ts +++ b/x-pack/plugins/security_solution/common/utils/to_array.ts @@ -5,8 +5,9 @@ * 2.0. */ -export const toArray = (value: T | T[] | null): T[] => +export const toArray = (value: T | T[] | null | undefined): T[] => Array.isArray(value) ? value : value == null ? [] : [value]; + export const toStringArray = (value: T | T[] | null): string[] => { if (Array.isArray(value)) { return value.reduce((acc, v) => { @@ -41,6 +42,7 @@ export const toStringArray = (value: T | T[] | null): string[] => { return [`${value}`]; } }; + export const toObjectArrayOfStrings = ( value: T | T[] | null ): Array<{ diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/bulk_edit_rules.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/bulk_edit_rules.spec.ts index 20da46b4a3cf9..dd04d324d3739 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/bulk_edit_rules.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/bulk_edit_rules.spec.ts @@ -74,12 +74,6 @@ const DEFAULT_INDEX_PATTERNS = ['index-1-*', 'index-2-*']; const TAGS = ['cypress-tag-1', 'cypress-tag-2']; const OVERWRITE_INDEX_PATTERNS = ['overwrite-index-1-*', 'overwrite-index-2-*']; -const customRule = { - ...getNewRule(), - index: DEFAULT_INDEX_PATTERNS, - name: RULE_NAME, -}; - const expectedNumberOfCustomRulesToBeEdited = 6; const expectedNumberOfMachineLearningRulesToBeEdited = 1; const numberOfRulesPerPage = 5; @@ -92,7 +86,14 @@ describe('Detection rules, bulk edit', () => { beforeEach(() => { deleteAlertsAndRules(); esArchiverResetKibana(); - createCustomRule(customRule, '1'); + createCustomRule( + { + ...getNewRule(), + name: RULE_NAME, + dataSource: { index: DEFAULT_INDEX_PATTERNS, type: 'indexPatterns' }, + }, + '1' + ); createCustomRule(getExistingRule(), '2'); createCustomRule(getNewOverrideRule(), '3'); createCustomRule(getNewThresholdRule(), '4'); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/custom_query_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/custom_query_rule.spec.ts index 92bd07b8a24ba..ba886993c7433 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/custom_query_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/custom_query_rule.spec.ts @@ -295,10 +295,13 @@ describe('Custom query rules', () => { }); context('Edition', () => { - const expectedEditedtags = getEditedRule().tags.join(''); + const rule = getEditedRule(); + const expectedEditedtags = rule.tags.join(''); const expectedEditedIndexPatterns = - getEditedRule().index && getEditedRule().index.length - ? getEditedRule().index + rule.dataSource.type === 'indexPatterns' && + rule.dataSource.index && + rule.dataSource.index.length + ? rule.dataSource.index : getIndexPatterns(); before(() => { @@ -325,27 +328,32 @@ describe('Custom query rules', () => { }); it('Allows a rule to be edited', () => { + const existingRule = getExistingRule(); + editFirstRule(); // expect define step to populate - cy.get(CUSTOM_QUERY_INPUT).should('have.value', getExistingRule().customQuery); - if (getExistingRule().index && getExistingRule().index.length > 0) { - cy.get(DEFINE_INDEX_INPUT).should('have.text', getExistingRule().index.join('')); + cy.get(CUSTOM_QUERY_INPUT).should('have.value', existingRule.customQuery); + if ( + existingRule.dataSource.type === 'indexPatterns' && + existingRule.dataSource.index.length > 0 + ) { + cy.get(DEFINE_INDEX_INPUT).should('have.text', existingRule.dataSource.index.join('')); } goToAboutStepTab(); // expect about step to populate - cy.get(RULE_NAME_INPUT).invoke('val').should('eql', getExistingRule().name); - cy.get(RULE_DESCRIPTION_INPUT).should('have.text', getExistingRule().description); - cy.get(TAGS_FIELD).should('have.text', getExistingRule().tags.join('')); - cy.get(SEVERITY_DROPDOWN).should('have.text', getExistingRule().severity); - cy.get(DEFAULT_RISK_SCORE_INPUT).invoke('val').should('eql', getExistingRule().riskScore); + cy.get(RULE_NAME_INPUT).invoke('val').should('eql', existingRule.name); + cy.get(RULE_DESCRIPTION_INPUT).should('have.text', existingRule.description); + cy.get(TAGS_FIELD).should('have.text', existingRule.tags.join('')); + cy.get(SEVERITY_DROPDOWN).should('have.text', existingRule.severity); + cy.get(DEFAULT_RISK_SCORE_INPUT).invoke('val').should('eql', existingRule.riskScore); goToScheduleStepTab(); // expect schedule step to populate - const interval = getExistingRule().interval; + const interval = existingRule.interval; const intervalParts = interval != null && interval.match(/[0-9]+|[a-zA-Z]+/g); if (intervalParts) { const [amount, unit] = intervalParts; @@ -381,7 +389,7 @@ describe('Custom query rules', () => { cy.wait('@getRule').then(({ response }) => { cy.wrap(response?.statusCode).should('eql', 200); // ensure that editing rule does not modify max_signals - cy.wrap(response?.body.max_signals).should('eql', getExistingRule().maxSignals); + cy.wrap(response?.body.max_signals).should('eql', existingRule.maxSignals); }); cy.get(RULE_NAME_HEADER).should('contain', `${getEditedRule().name}`); @@ -396,7 +404,7 @@ describe('Custom query rules', () => { cy.get(DEFINITION_DETAILS).within(() => { getDetails(INDEX_PATTERNS_DETAILS).should( 'have.text', - expectedEditedIndexPatterns.join('') + expectedEditedIndexPatterns?.join('') ); getDetails(CUSTOM_QUERY_DETAILS).should('have.text', getEditedRule().customQuery); getDetails(RULE_TYPE_DETAILS).should('have.text', 'Query'); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/custom_query_rule_data_view.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/custom_query_rule_data_view.spec.ts new file mode 100644 index 0000000000000..77a4ae274e6e4 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/custom_query_rule_data_view.spec.ts @@ -0,0 +1,158 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { formatMitreAttackDescription } from '../../helpers/rules'; +import { getDataViewRule } from '../../objects/rule'; +import { ALERT_GRID_CELL, NUMBER_OF_ALERTS } from '../../screens/alerts'; + +import { + CUSTOM_RULES_BTN, + RISK_SCORE, + RULE_NAME, + RULES_ROW, + RULES_TABLE, + RULE_SWITCH, + SEVERITY, +} from '../../screens/alerts_detection_rules'; + +import { + ADDITIONAL_LOOK_BACK_DETAILS, + ABOUT_DETAILS, + ABOUT_INVESTIGATION_NOTES, + ABOUT_RULE_DESCRIPTION, + CUSTOM_QUERY_DETAILS, + DEFINITION_DETAILS, + FALSE_POSITIVES_DETAILS, + removeExternalLinkText, + INDEX_PATTERNS_DETAILS, + INVESTIGATION_NOTES_MARKDOWN, + INVESTIGATION_NOTES_TOGGLE, + MITRE_ATTACK_DETAILS, + REFERENCE_URLS_DETAILS, + RISK_SCORE_DETAILS, + RULE_NAME_HEADER, + RULE_TYPE_DETAILS, + RUNS_EVERY_DETAILS, + SCHEDULE_DETAILS, + SEVERITY_DETAILS, + TAGS_DETAILS, + TIMELINE_TEMPLATE_DETAILS, + DATA_VIEW_DETAILS, +} from '../../screens/rule_details'; + +import { goToRuleDetails } from '../../tasks/alerts_detection_rules'; +import { createTimeline } from '../../tasks/api_calls/timelines'; +import { postDataView } from '../../tasks/common'; +import { + createAndEnableRule, + fillAboutRuleAndContinue, + fillDefineCustomRuleWithImportedQueryAndContinue, + fillScheduleRuleAndContinue, + waitForAlertsToPopulate, + waitForTheRuleToBeExecuted, +} from '../../tasks/create_new_rule'; + +import { esArchiverResetKibana } from '../../tasks/es_archiver'; +import { login, visit } from '../../tasks/login'; +import { getDetails } from '../../tasks/rule_details'; + +import { RULE_CREATION } from '../../urls/navigation'; + +describe('Custom query rules', () => { + before(() => { + login(); + }); + + describe('Custom detection rules creation with data views', () => { + const rule = getDataViewRule(); + const expectedUrls = rule.referenceUrls.join(''); + const expectedFalsePositives = rule.falsePositivesExamples.join(''); + const expectedTags = rule.tags.join(''); + const expectedMitre = formatMitreAttackDescription(rule.mitre); + const expectedNumberOfRules = 1; + + beforeEach(() => { + /* We don't call cleanKibana method on the before hook, instead we call esArchiverReseKibana on the before each. This is because we + are creating a data view we'll use after and cleanKibana does not delete all the data views created, esArchiverReseKibana does. + We don't use esArchiverReseKibana in all the tests because is a time-consuming method and we don't need to perform an exhaustive + cleaning in all the other tests. */ + esArchiverResetKibana(); + createTimeline(rule.timeline).then((response) => { + cy.wrap({ + ...rule, + timeline: { + ...rule.timeline, + id: response.body.data.persistTimeline.timeline.savedObjectId, + }, + }).as('rule'); + }); + if (rule.dataSource.type === 'dataView') { + postDataView(rule.dataSource.dataView); + } + }); + + it('Creates and enables a new rule', function () { + visit(RULE_CREATION); + fillDefineCustomRuleWithImportedQueryAndContinue(this.rule); + fillAboutRuleAndContinue(this.rule); + fillScheduleRuleAndContinue(this.rule); + createAndEnableRule(); + + cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)'); + + cy.get(RULES_TABLE).find(RULES_ROW).should('have.length', expectedNumberOfRules); + cy.get(RULE_NAME).should('have.text', this.rule.name); + cy.get(RISK_SCORE).should('have.text', this.rule.riskScore); + cy.get(SEVERITY).should('have.text', this.rule.severity); + cy.get(RULE_SWITCH).should('have.attr', 'aria-checked', 'true'); + + goToRuleDetails(); + + cy.get(RULE_NAME_HEADER).should('contain', `${this.rule.name}`); + cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', this.rule.description); + cy.get(ABOUT_DETAILS).within(() => { + getDetails(SEVERITY_DETAILS).should('have.text', this.rule.severity); + getDetails(RISK_SCORE_DETAILS).should('have.text', this.rule.riskScore); + getDetails(REFERENCE_URLS_DETAILS).should((details) => { + expect(removeExternalLinkText(details.text())).equal(expectedUrls); + }); + getDetails(FALSE_POSITIVES_DETAILS).should('have.text', expectedFalsePositives); + getDetails(MITRE_ATTACK_DETAILS).should((mitre) => { + expect(removeExternalLinkText(mitre.text())).equal(expectedMitre); + }); + getDetails(TAGS_DETAILS).should('have.text', expectedTags); + }); + cy.get(INVESTIGATION_NOTES_TOGGLE).click({ force: true }); + cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', INVESTIGATION_NOTES_MARKDOWN); + cy.get(DEFINITION_DETAILS).within(() => { + getDetails(DATA_VIEW_DETAILS).should('have.text', this.rule.dataSource.dataView); + getDetails(CUSTOM_QUERY_DETAILS).should('have.text', this.rule.customQuery); + getDetails(RULE_TYPE_DETAILS).should('have.text', 'Query'); + getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None'); + }); + cy.get(DEFINITION_DETAILS).should('not.contain', INDEX_PATTERNS_DETAILS); + cy.get(SCHEDULE_DETAILS).within(() => { + getDetails(RUNS_EVERY_DETAILS).should( + 'have.text', + `${getDataViewRule().runsEvery.interval}${getDataViewRule().runsEvery.type}` + ); + getDetails(ADDITIONAL_LOOK_BACK_DETAILS).should( + 'have.text', + `${getDataViewRule().lookBack.interval}${getDataViewRule().lookBack.type}` + ); + }); + + waitForTheRuleToBeExecuted(); + waitForAlertsToPopulate(); + + cy.get(NUMBER_OF_ALERTS) + .invoke('text') + .should('match', /^[1-9].+$/); + cy.get(ALERT_GRID_CELL).contains(this.rule.name); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts index faa907be8810c..7c3ba64536faf 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts @@ -205,12 +205,12 @@ describe('indicator match', () => { describe('Indicator mapping', () => { beforeEach(() => { + const rule = getNewThreatIndicatorRule(); visitWithoutDateRange(RULE_CREATION); selectIndicatorMatchType(); - fillIndexAndIndicatorIndexPattern( - getNewThreatIndicatorRule().index, - getNewThreatIndicatorRule().indicatorIndexPattern - ); + if (rule.dataSource.type === 'indexPatterns') { + fillIndexAndIndicatorIndexPattern(rule.dataSource.index, rule.indicatorIndexPattern); + } }); it('Does NOT show invalidation text on initial page load', () => { @@ -419,11 +419,12 @@ describe('indicator match', () => { }); it('Creates and enables a new Indicator Match rule', () => { + const rule = getNewThreatIndicatorRule(); visitWithoutDateRange(RULE_CREATION); selectIndicatorMatchType(); - fillDefineIndicatorMatchRuleAndContinue(getNewThreatIndicatorRule()); - fillAboutRuleAndContinue(getNewThreatIndicatorRule()); - fillScheduleRuleAndContinue(getNewThreatIndicatorRule()); + fillDefineIndicatorMatchRuleAndContinue(rule); + fillAboutRuleAndContinue(rule); + fillScheduleRuleAndContinue(rule); createAndEnableRule(); cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)'); @@ -432,22 +433,19 @@ describe('indicator match', () => { cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRules); }); - cy.get(RULE_NAME).should('have.text', getNewThreatIndicatorRule().name); - cy.get(RISK_SCORE).should('have.text', getNewThreatIndicatorRule().riskScore); - cy.get(SEVERITY).should('have.text', getNewThreatIndicatorRule().severity); + cy.get(RULE_NAME).should('have.text', rule.name); + cy.get(RISK_SCORE).should('have.text', rule.riskScore); + cy.get(SEVERITY).should('have.text', rule.severity); cy.get(RULE_SWITCH).should('have.attr', 'aria-checked', 'true'); goToRuleDetails(); - cy.get(RULE_NAME_HEADER).should('contain', `${getNewThreatIndicatorRule().name}`); - cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', getNewThreatIndicatorRule().description); + cy.get(RULE_NAME_HEADER).should('contain', `${rule.name}`); + cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', rule.description); cy.get(ABOUT_DETAILS).within(() => { - getDetails(SEVERITY_DETAILS).should('have.text', getNewThreatIndicatorRule().severity); - getDetails(RISK_SCORE_DETAILS).should('have.text', getNewThreatIndicatorRule().riskScore); - getDetails(INDICATOR_PREFIX_OVERRIDE).should( - 'have.text', - getNewThreatIndicatorRule().threatIndicatorPath - ); + getDetails(SEVERITY_DETAILS).should('have.text', rule.severity); + getDetails(RISK_SCORE_DETAILS).should('have.text', rule.riskScore); + getDetails(INDICATOR_PREFIX_OVERRIDE).should('have.text', rule.threatIndicatorPath); getDetails(REFERENCE_URLS_DETAILS).should((details) => { expect(removeExternalLinkText(details.text())).equal(expectedUrls); }); @@ -461,22 +459,19 @@ describe('indicator match', () => { cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', INVESTIGATION_NOTES_MARKDOWN); cy.get(DEFINITION_DETAILS).within(() => { - getDetails(INDEX_PATTERNS_DETAILS).should( - 'have.text', - getNewThreatIndicatorRule().index.join('') - ); + if (rule.dataSource.type === 'indexPatterns') { + getDetails(INDEX_PATTERNS_DETAILS).should('have.text', rule.dataSource.index?.join('')); + } getDetails(CUSTOM_QUERY_DETAILS).should('have.text', '*:*'); getDetails(RULE_TYPE_DETAILS).should('have.text', 'Indicator Match'); getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None'); getDetails(INDICATOR_INDEX_PATTERNS).should( 'have.text', - getNewThreatIndicatorRule().indicatorIndexPattern.join('') + rule.indicatorIndexPattern.join('') ); getDetails(INDICATOR_MAPPING).should( 'have.text', - `${getNewThreatIndicatorRule().indicatorMappingField} MATCHES ${ - getNewThreatIndicatorRule().indicatorIndexField - }` + `${rule.indicatorMappingField} MATCHES ${rule.indicatorIndexField}` ); getDetails(INDICATOR_INDEX_QUERY).should('have.text', '*:*'); }); @@ -484,15 +479,11 @@ describe('indicator match', () => { cy.get(SCHEDULE_DETAILS).within(() => { getDetails(RUNS_EVERY_DETAILS).should( 'have.text', - `${getNewThreatIndicatorRule().runsEvery.interval}${ - getNewThreatIndicatorRule().runsEvery.type - }` + `${rule.runsEvery.interval}${rule.runsEvery.type}` ); getDetails(ADDITIONAL_LOOK_BACK_DETAILS).should( 'have.text', - `${getNewThreatIndicatorRule().lookBack.interval}${ - getNewThreatIndicatorRule().lookBack.type - }` + `${rule.lookBack.interval}${rule.lookBack.type}` ); }); @@ -500,11 +491,9 @@ describe('indicator match', () => { waitForAlertsToPopulate(); cy.get(NUMBER_OF_ALERTS).should('have.text', expectedNumberOfAlerts); - cy.get(ALERT_RULE_NAME).first().should('have.text', getNewThreatIndicatorRule().name); - cy.get(ALERT_SEVERITY) - .first() - .should('have.text', getNewThreatIndicatorRule().severity.toLowerCase()); - cy.get(ALERT_RISK_SCORE).first().should('have.text', getNewThreatIndicatorRule().riskScore); + cy.get(ALERT_RULE_NAME).first().should('have.text', rule.name); + cy.get(ALERT_SEVERITY).first().should('have.text', rule.severity.toLowerCase()); + cy.get(ALERT_RISK_SCORE).first().should('have.text', rule.riskScore); }); it('Investigate alert in timeline', () => { diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts index 9673de50bbae9..4e2b42c7b7ee1 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts @@ -6,8 +6,7 @@ */ import { formatMitreAttackDescription } from '../../helpers/rules'; -import type { ThresholdRule } from '../../objects/rule'; -import { getIndexPatterns, getNewRule, getNewThresholdRule } from '../../objects/rule'; +import { getIndexPatterns, getNewThresholdRule } from '../../objects/rule'; import { ALERT_GRID_CELL, NUMBER_OF_ALERTS } from '../../screens/alerts'; @@ -20,7 +19,6 @@ import { RULES_TABLE, SEVERITY, } from '../../screens/alerts_detection_rules'; -import { PREVIEW_HEADER_SUBTITLE } from '../../screens/create_new_rule'; import { ABOUT_DETAILS, ABOUT_INVESTIGATION_NOTES, @@ -47,22 +45,14 @@ import { } from '../../screens/rule_details'; import { getDetails } from '../../tasks/rule_details'; -import { goToManageAlertsDetectionRules } from '../../tasks/alerts'; -import { - goToCreateNewRule, - goToRuleDetails, - waitForRulesTableToBeLoaded, -} from '../../tasks/alerts_detection_rules'; -import { createCustomRuleEnabled } from '../../tasks/api_calls/rules'; +import { goToRuleDetails } from '../../tasks/alerts_detection_rules'; import { createTimeline } from '../../tasks/api_calls/timelines'; import { cleanKibana, deleteAlertsAndRules } from '../../tasks/common'; import { createAndEnableRule, fillAboutRuleAndContinue, fillDefineThresholdRuleAndContinue, - fillDefineThresholdRule, fillScheduleRuleAndContinue, - previewResults, selectThresholdRuleType, waitForAlertsToPopulate, waitForTheRuleToBeExecuted, @@ -156,37 +146,4 @@ describe('Detection rules, threshold', () => { cy.get(NUMBER_OF_ALERTS).should(($count) => expect(+$count.text().split(' ')[0]).to.be.lt(100)); cy.get(ALERT_GRID_CELL).contains(rule.name); }); - - it.skip('Preview results of keyword using "host.name"', () => { - rule.index = [...rule.index, '.siem-signals*']; - - createCustomRuleEnabled(getNewRule()); - goToManageAlertsDetectionRules(); - waitForRulesTableToBeLoaded(); - goToCreateNewRule(); - selectThresholdRuleType(); - fillDefineThresholdRule(rule); - previewResults(); - - cy.get(PREVIEW_HEADER_SUBTITLE).should('have.text', '3 unique hits'); - }); - - it.skip('Preview results of "ip" using "source.ip"', () => { - const previewRule: ThresholdRule = { - ...rule, - thresholdField: 'source.ip', - threshold: '1', - }; - previewRule.index = [...previewRule.index, '.siem-signals*']; - - createCustomRuleEnabled(getNewRule()); - goToManageAlertsDetectionRules(); - waitForRulesTableToBeLoaded(); - goToCreateNewRule(); - selectThresholdRuleType(); - fillDefineThresholdRule(previewRule); - previewResults(); - - cy.get(PREVIEW_HEADER_SUBTITLE).should('have.text', '10 unique hits'); - }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/exceptions/add_exception.spec.ts b/x-pack/plugins/security_solution/cypress/integration/exceptions/add_exception.spec.ts index 814f29622f51a..ac2fdf96be022 100644 --- a/x-pack/plugins/security_solution/cypress/integration/exceptions/add_exception.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/exceptions/add_exception.spec.ts @@ -40,7 +40,11 @@ describe('Adds rule exception', () => { beforeEach(() => { deleteAlertsAndRules(); createCustomRuleEnabled( - { ...getNewRule(), customQuery: 'agent.name:*', index: ['exceptions*'] }, + { + ...getNewRule(), + customQuery: 'agent.name:*', + dataSource: { index: ['exceptions*'], type: 'indexPatterns' }, + }, 'rule_testing', '1s' ); diff --git a/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_flyout.spec.ts b/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_flyout.spec.ts index 9e08313e7e73f..8c2e2af4b8bad 100644 --- a/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_flyout.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_flyout.spec.ts @@ -70,7 +70,7 @@ describe('Exceptions flyout', () => { createExceptionList(getExceptionList(), getExceptionList().list_id).then((response) => createCustomRule({ ...getNewRule(), - index: ['exceptions-*'], + dataSource: { index: ['exceptions-*'], type: 'indexPatterns' }, exceptionLists: [ { id: response.body.id, diff --git a/x-pack/plugins/security_solution/cypress/integration/users/users_tabs.spec.ts b/x-pack/plugins/security_solution/cypress/integration/users/users_tabs.spec.ts index a85222a7be77f..a0f3735a80026 100644 --- a/x-pack/plugins/security_solution/cypress/integration/users/users_tabs.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/users/users_tabs.spec.ts @@ -12,10 +12,6 @@ import { AUTHENTICATIONS_TABLE, } from '../../screens/users/user_authentications'; import { EVENTS_TAB, EVENTS_TAB_CONTENT } from '../../screens/users/user_events'; -import { - EXTERNAL_ALERTS_TAB, - EXTERNAL_ALERTS_TAB_CONTENT, -} from '../../screens/users/user_external_alerts'; import { RISK_SCORE_TAB, RISK_SCORE_TAB_CONTENT } from '../../screens/users/user_risk_score'; import { cleanKibana } from '../../tasks/common'; import { esArchiverLoad, esArchiverUnload } from '../../tasks/es_archiver'; @@ -66,12 +62,6 @@ describe('Users stats and tables', () => { cy.get(EVENTS_TAB_CONTENT).should('exist'); }); - it(`renders external alerts tab`, () => { - cy.get(EXTERNAL_ALERTS_TAB).click({ force: true }); - - cy.get(EXTERNAL_ALERTS_TAB_CONTENT).should('exist'); - }); - it(`renders users risk tab`, () => { cy.get(RISK_SCORE_TAB).click({ force: true }); diff --git a/x-pack/plugins/security_solution/cypress/objects/rule.ts b/x-pack/plugins/security_solution/cypress/objects/rule.ts index 17ffdf52486f5..7fd9e7b5f7625 100644 --- a/x-pack/plugins/security_solution/cypress/objects/rule.ts +++ b/x-pack/plugins/security_solution/cypress/objects/rule.ts @@ -37,11 +37,15 @@ interface Interval { type: string; } +export type RuleDataSource = + | { type: 'indexPatterns'; index: string[] } + | { type: 'dataView'; dataView: string }; + export interface CustomRule { customQuery?: string; name: string; description: string; - index: string[]; + dataSource: RuleDataSource; interval?: string; severity: string; riskScore: string; @@ -176,15 +180,39 @@ const getRunsEvery = (): Interval => ({ type: 's', }); +const getRunsEveryFiveMinutes = (): Interval => ({ + interval: '5', + timeType: 'Minutes', + type: 'm', +}); + const getLookBack = (): Interval => ({ interval: '50000', timeType: 'Hours', type: 'h', }); +export const getDataViewRule = (): CustomRule => ({ + customQuery: 'host.name: *', + dataSource: { dataView: 'auditbeat-2022', type: 'dataView' }, + name: 'New Data View Rule', + description: 'The new rule description.', + severity: 'High', + riskScore: '17', + tags: ['test', 'newRule'], + referenceUrls: ['http://example.com/', 'https://example.com/'], + falsePositivesExamples: ['False1', 'False2'], + mitre: [getMitre1(), getMitre2()], + note: '# test markdown', + runsEvery: getRunsEveryFiveMinutes(), + lookBack: getLookBack(), + timeline: getTimeline(), + maxSignals: 100, +}); + export const getNewRule = (): CustomRule => ({ customQuery: 'host.name: *', - index: getIndexPatterns(), + dataSource: { index: getIndexPatterns(), type: 'indexPatterns' }, name: 'New Rule Test', description: 'The new rule description.', severity: 'High', @@ -202,7 +230,7 @@ export const getNewRule = (): CustomRule => ({ export const getBuildingBlockRule = (): CustomRule => ({ customQuery: 'host.name: *', - index: getIndexPatterns(), + dataSource: { index: getIndexPatterns(), type: 'indexPatterns' }, name: 'Building Block Rule Test', description: 'The new rule description.', severity: 'High', @@ -221,7 +249,7 @@ export const getBuildingBlockRule = (): CustomRule => ({ export const getUnmappedRule = (): CustomRule => ({ customQuery: '*:*', - index: ['unmapped*'], + dataSource: { index: ['unmapped*'], type: 'indexPatterns' }, name: 'Rule with unmapped fields', description: 'The new rule description.', severity: 'High', @@ -239,7 +267,7 @@ export const getUnmappedRule = (): CustomRule => ({ export const getUnmappedCCSRule = (): CustomRule => ({ customQuery: '*:*', - index: [`${ccsRemoteName}:unmapped*`], + dataSource: { index: [`${ccsRemoteName}:unmapped*`], type: 'indexPatterns' }, name: 'Rule with unmapped fields', description: 'The new rule description.', severity: 'High', @@ -259,7 +287,7 @@ export const getExistingRule = (): CustomRule => ({ customQuery: 'host.name: *', name: 'Rule 1', description: 'Description for Rule 1', - index: ['auditbeat-*'], + dataSource: { index: ['auditbeat-*'], type: 'indexPatterns' }, interval: '100m', severity: 'High', riskScore: '19', @@ -278,7 +306,7 @@ export const getExistingRule = (): CustomRule => ({ export const getNewOverrideRule = (): OverrideRule => ({ customQuery: 'host.name: *', - index: getIndexPatterns(), + dataSource: { index: getIndexPatterns(), type: 'indexPatterns' }, name: 'Override Rule', description: 'The new rule description.', severity: 'High', @@ -305,7 +333,7 @@ export const getNewOverrideRule = (): OverrideRule => ({ export const getNewThresholdRule = (): ThresholdRule => ({ customQuery: 'host.name: *', - index: getIndexPatterns(), + dataSource: { index: getIndexPatterns(), type: 'indexPatterns' }, name: 'Threshold Rule', description: 'The new rule description.', severity: 'High', @@ -325,7 +353,7 @@ export const getNewThresholdRule = (): ThresholdRule => ({ export const getNewTermsRule = (): NewTermsRule => ({ customQuery: 'host.name: *', - index: getIndexPatterns(), + dataSource: { index: getIndexPatterns(), type: 'indexPatterns' }, name: 'New Terms Rule', description: 'The new rule description.', severity: 'High', @@ -365,7 +393,7 @@ export const getMachineLearningRule = (): MachineLearningRule => ({ export const getEqlRule = (): CustomRule => ({ customQuery: 'any where process.name == "zsh"', name: 'New EQL Rule', - index: getIndexPatterns(), + dataSource: { index: getIndexPatterns(), type: 'indexPatterns' }, description: 'New EQL rule description.', severity: 'High', riskScore: '17', @@ -383,7 +411,7 @@ export const getEqlRule = (): CustomRule => ({ export const getCCSEqlRule = (): CustomRule => ({ customQuery: 'any where process.name == "run-parts"', name: 'New EQL Rule', - index: [`${ccsRemoteName}:run-parts`], + dataSource: { index: [`${ccsRemoteName}:run-parts`], type: 'indexPatterns' }, description: 'New EQL rule description.', severity: 'High', riskScore: '17', @@ -404,7 +432,7 @@ export const getEqlSequenceRule = (): CustomRule => ({ [any where agent.name == "test.local"]\ [any where host.name == "test.local"]', name: 'New EQL Sequence Rule', - index: getIndexPatterns(), + dataSource: { index: getIndexPatterns(), type: 'indexPatterns' }, description: 'New EQL rule description.', severity: 'High', riskScore: '17', @@ -422,7 +450,7 @@ export const getEqlSequenceRule = (): CustomRule => ({ export const getNewThreatIndicatorRule = (): ThreatIndicatorRule => ({ name: 'Threat Indicator Rule Test', description: 'The threat indicator rule description.', - index: ['suspicious-*'], + dataSource: { index: ['suspicious-*'], type: 'indexPatterns' }, severity: 'Critical', riskScore: '20', tags: ['test', 'threat'], diff --git a/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts index 7af92d518db7a..00fb1ad0abe55 100644 --- a/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts +++ b/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts @@ -93,6 +93,11 @@ export const AT_LEAST_ONE_INDEX_PATTERN = 'A minimum of one index pattern is req export const CUSTOM_QUERY_REQUIRED = 'A custom query is required.'; +export const DATA_VIEW_COMBO_BOX = + '[data-test-subj="pick-rule-data-source"] [data-test-subj="comboBoxInput"]'; + +export const DATA_VIEW_OPTION = '[data-test-subj="rule-index-toggle-dataView"]'; + export const DEFINE_CONTINUE_BUTTON = '[data-test-subj="define-continue"]'; export const DEFINE_EDIT_BUTTON = '[data-test-subj="edit-define-rule"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/rule_details.ts b/x-pack/plugins/security_solution/cypress/screens/rule_details.ts index a541cf5cb2bc6..4aed5286ee1f1 100644 --- a/x-pack/plugins/security_solution/cypress/screens/rule_details.ts +++ b/x-pack/plugins/security_solution/cypress/screens/rule_details.ts @@ -22,6 +22,8 @@ export const ANOMALY_SCORE_DETAILS = 'Anomaly score'; export const CUSTOM_QUERY_DETAILS = 'Custom query'; +export const DATA_VIEW_DETAILS = 'Data View'; + export const DEFINITION_DETAILS = '[data-test-subj=definitionRule] [data-test-subj="listItemColumnStepRuleDescription"]'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts b/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts index 88616063c6c36..0ef07195b3309 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts @@ -28,7 +28,11 @@ export const createMachineLearningRule = (rule: MachineLearningRule, ruleId = 'm failOnStatusCode: false, }); -export const createCustomRule = (rule: CustomRule, ruleId = 'rule_testing', interval = '100m') => +export const createCustomRule = ( + rule: CustomRule, + ruleId = 'rule_testing', + interval = '100m' +): Cypress.Chainable> => cy.request({ method: 'POST', url: 'api/detection_engine/rules', @@ -41,7 +45,7 @@ export const createCustomRule = (rule: CustomRule, ruleId = 'rule_testing', inte severity: rule.severity.toLocaleLowerCase(), type: 'query', from: 'now-50000h', - index: rule.index, + index: rule.dataSource.type === 'indexPatterns' ? rule.dataSource.index : '', query: rule.customQuery, language: 'kuery', enabled: false, @@ -51,98 +55,107 @@ export const createCustomRule = (rule: CustomRule, ruleId = 'rule_testing', inte failOnStatusCode: false, }); -export const createEventCorrelationRule = (rule: CustomRule, ruleId = 'rule_testing') => - cy.request({ - method: 'POST', - url: 'api/detection_engine/rules', - body: { - rule_id: ruleId, - risk_score: parseInt(rule.riskScore, 10), - description: rule.description, - interval: `${rule.runsEvery.interval}${rule.runsEvery.type}`, - from: `now-${rule.lookBack.interval}${rule.lookBack.type}`, - name: rule.name, - severity: rule.severity.toLocaleLowerCase(), - type: 'eql', - index: rule.index, - query: rule.customQuery, - language: 'eql', - enabled: true, - }, - headers: { 'kbn-xsrf': 'cypress-creds' }, - }); +export const createEventCorrelationRule = (rule: CustomRule, ruleId = 'rule_testing') => { + if (rule.dataSource.type === 'indexPatterns') { + cy.request({ + method: 'POST', + url: 'api/detection_engine/rules', + body: { + rule_id: ruleId, + risk_score: parseInt(rule.riskScore, 10), + description: rule.description, + interval: `${rule.runsEvery.interval}${rule.runsEvery.type}`, + from: `now-${rule.lookBack.interval}${rule.lookBack.type}`, + name: rule.name, + severity: rule.severity.toLocaleLowerCase(), + type: 'eql', + index: rule.dataSource.index, + query: rule.customQuery, + language: 'eql', + enabled: true, + }, + headers: { 'kbn-xsrf': 'cypress-creds' }, + }); + } +}; -export const createCustomIndicatorRule = (rule: ThreatIndicatorRule, ruleId = 'rule_testing') => - cy.request({ - method: 'POST', - url: 'api/detection_engine/rules', - body: { - rule_id: ruleId, - risk_score: parseInt(rule.riskScore, 10), - description: rule.description, - // Default interval is 1m, our tests config overwrite this to 1s - // See https://github.com/elastic/kibana/pull/125396 for details - interval: '10s', - name: rule.name, - severity: rule.severity.toLocaleLowerCase(), - type: 'threat_match', - timeline_id: rule.timeline.templateTimelineId, - timeline_title: rule.timeline.title, - threat_mapping: [ - { - entries: [ - { - field: rule.indicatorMappingField, - type: 'mapping', - value: rule.indicatorIndexField, - }, - ], - }, - ], - threat_query: '*:*', - threat_language: 'kuery', - threat_filters: [], - threat_index: rule.indicatorIndexPattern, - threat_indicator_path: rule.threatIndicatorPath, - from: 'now-50000h', - index: rule.index, - query: rule.customQuery || '*:*', - language: 'kuery', - enabled: true, - }, - headers: { 'kbn-xsrf': 'cypress-creds' }, - failOnStatusCode: false, - }); +export const createCustomIndicatorRule = (rule: ThreatIndicatorRule, ruleId = 'rule_testing') => { + if (rule.dataSource.type === 'indexPatterns') { + cy.request({ + method: 'POST', + url: 'api/detection_engine/rules', + body: { + rule_id: ruleId, + risk_score: parseInt(rule.riskScore, 10), + description: rule.description, + // Default interval is 1m, our tests config overwrite this to 1s + // See https://github.com/elastic/kibana/pull/125396 for details + interval: '10s', + name: rule.name, + severity: rule.severity.toLocaleLowerCase(), + type: 'threat_match', + timeline_id: rule.timeline.templateTimelineId, + timeline_title: rule.timeline.title, + threat_mapping: [ + { + entries: [ + { + field: rule.indicatorMappingField, + type: 'mapping', + value: rule.indicatorIndexField, + }, + ], + }, + ], + threat_query: '*:*', + threat_language: 'kuery', + threat_filters: [], + threat_index: rule.indicatorIndexPattern, + threat_indicator_path: rule.threatIndicatorPath, + from: 'now-50000h', + index: rule.dataSource.index, + query: rule.customQuery || '*:*', + language: 'kuery', + enabled: true, + }, + headers: { 'kbn-xsrf': 'cypress-creds' }, + failOnStatusCode: false, + }); + } +}; export const createCustomRuleEnabled = ( rule: CustomRule, ruleId = '1', interval = '100m', maxSignals = 500 -) => - cy.request({ - method: 'POST', - url: 'api/detection_engine/rules', - body: { - rule_id: ruleId, - risk_score: parseInt(rule.riskScore, 10), - description: rule.description, - interval, - name: rule.name, - severity: rule.severity.toLocaleLowerCase(), - type: 'query', - from: 'now-50000h', - index: rule.index, - query: rule.customQuery, - language: 'kuery', - enabled: true, - tags: ['rule1'], - max_signals: maxSignals, - building_block_type: rule.buildingBlockType, - }, - headers: { 'kbn-xsrf': 'cypress-creds' }, - failOnStatusCode: false, - }); +) => { + if (rule.dataSource.type === 'indexPatterns') { + cy.request({ + method: 'POST', + url: 'api/detection_engine/rules', + body: { + rule_id: ruleId, + risk_score: parseInt(rule.riskScore, 10), + description: rule.description, + interval, + name: rule.name, + severity: rule.severity.toLocaleLowerCase(), + type: 'query', + from: 'now-50000h', + index: rule.dataSource.index, + query: rule.customQuery, + language: 'kuery', + enabled: true, + tags: ['rule1'], + max_signals: maxSignals, + building_block_type: rule.buildingBlockType, + }, + headers: { 'kbn-xsrf': 'cypress-creds' }, + failOnStatusCode: false, + }); + } +}; export const deleteCustomRule = (ruleId = '1') => { cy.request({ diff --git a/x-pack/plugins/security_solution/cypress/tasks/common.ts b/x-pack/plugins/security_solution/cypress/tasks/common.ts index 437ed1a254ca4..4982053648667 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/common.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/common.ts @@ -179,14 +179,14 @@ export const deleteCases = () => { }); }; -export const postDataView = (indexPattern: string) => { +export const postDataView = (dataSource: string) => { cy.request({ method: 'POST', url: `/api/index_patterns/index_pattern`, body: { index_pattern: { fieldAttrs: '{}', - title: indexPattern, + title: dataSource, timeFieldName: '@timestamp', fields: '{}', }, diff --git a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts index 5e5de7301362b..8d1c1ebc94eb5 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts @@ -94,6 +94,8 @@ import { EMAIL_CONNECTOR_PASSWORD_INPUT, EMAIL_CONNECTOR_SERVICE_SELECTOR, PREVIEW_HISTOGRAM, + DATA_VIEW_COMBO_BOX, + DATA_VIEW_OPTION, NEW_TERMS_TYPE, NEW_TERMS_HISTORY_SIZE, NEW_TERMS_HISTORY_TIME_TYPE, @@ -258,6 +260,10 @@ export const fillAboutRuleWithOverrideAndContinue = (rule: OverrideRule) => { export const fillDefineCustomRuleWithImportedQueryAndContinue = ( rule: CustomRule | OverrideRule ) => { + if (rule.dataSource.type === 'dataView') { + cy.get(DATA_VIEW_OPTION).click(); + cy.get(DATA_VIEW_COMBO_BOX).type(`${rule.dataSource.dataView}{enter}`); + } cy.get(IMPORT_QUERY_FROM_SAVED_TIMELINE_LINK).click(); cy.get(TIMELINE(rule.timeline.id)).click(); cy.get(CUSTOM_QUERY_INPUT).should('have.value', rule.customQuery); @@ -282,9 +288,11 @@ export const fillDefineThresholdRule = (rule: ThresholdRule) => { cy.get(TIMELINE(rule.timeline.id)).click(); cy.get(COMBO_BOX_CLEAR_BTN).first().click(); - rule.index.forEach((index) => { - cy.get(COMBO_BOX_INPUT).first().type(`${index}{enter}`); - }); + if (rule.dataSource.type === 'indexPatterns') { + rule.dataSource.index.forEach((index) => { + cy.get(COMBO_BOX_INPUT).first().type(`${index}{enter}`); + }); + } cy.get(CUSTOM_QUERY_INPUT).should('have.value', rule.customQuery); cy.get(THRESHOLD_INPUT_AREA) @@ -494,7 +502,9 @@ export const getCustomQueryInvalidationText = () => cy.contains(CUSTOM_QUERY_REQ * @param rule The rule to use to fill in everything */ export const fillDefineIndicatorMatchRuleAndContinue = (rule: ThreatIndicatorRule) => { - fillIndexAndIndicatorIndexPattern(rule.index, rule.indicatorIndexPattern); + if (rule.dataSource.type === 'indexPatterns') { + fillIndexAndIndicatorIndexPattern(rule.dataSource.index, rule.indicatorIndexPattern); + } fillIndicatorMatchRow({ indexField: rule.indicatorMappingField, indicatorIndexField: rule.indicatorIndexField, diff --git a/x-pack/plugins/security_solution/kibana.json b/x-pack/plugins/security_solution/kibana.json index cf6ab94cf15be..3744f7ac4b6bc 100644 --- a/x-pack/plugins/security_solution/kibana.json +++ b/x-pack/plugins/security_solution/kibana.json @@ -12,23 +12,24 @@ "actions", "alerting", "cases", + "cloudSecurityPosture", "data", "embeddable", "eventLog", "features", "inspector", + "kubernetesSecurity", "lens", "licensing", "maps", "ruleRegistry", "sessionView", "taskManager", + "threatIntelligence", "timelines", "triggersActionsUi", "uiActions", - "unifiedSearch", - "kubernetesSecurity", - "cloudSecurityPosture" + "unifiedSearch" ], "optionalPlugins": [ "encryptedSavedObjects", diff --git a/x-pack/plugins/security_solution/public/app/deep_links/index.ts b/x-pack/plugins/security_solution/public/app/deep_links/index.ts index 2060d21baaf9c..c08c603b616ad 100644 --- a/x-pack/plugins/security_solution/public/app/deep_links/index.ts +++ b/x-pack/plugins/security_solution/public/app/deep_links/index.ts @@ -45,6 +45,7 @@ import { DASHBOARDS, CREATE_NEW_RULE, RESPONSE_ACTIONS, + THREAT_INTELLIGENCE, } from '../translations'; import { OVERVIEW_PATH, @@ -69,6 +70,7 @@ import { KUBERNETES_PATH, RULES_CREATE_PATH, RESPONSE_ACTIONS_PATH, + THREAT_INTELLIGENCE_PATH, } from '../../../common/constants'; import type { ExperimentalFeatures } from '../../../common/experimental_features'; import { hasCapabilities, subscribeAppLinks } from '../../common/links'; @@ -263,13 +265,6 @@ export const securitySolutionsDeepLinks: SecuritySolutionDeepLink[] = [ }), path: `${HOSTS_PATH}/events`, }, - { - id: SecurityPageName.hostsExternalAlerts, - title: i18n.translate('xpack.securitySolution.search.hosts.externalAlerts', { - defaultMessage: 'External Alerts', - }), - path: `${HOSTS_PATH}/externalAlerts`, - }, { id: SecurityPageName.hostsRisk, title: i18n.translate('xpack.securitySolution.search.hosts.risk', { @@ -318,13 +313,6 @@ export const securitySolutionsDeepLinks: SecuritySolutionDeepLink[] = [ }), path: `${NETWORK_PATH}/tls`, }, - { - id: SecurityPageName.networkExternalAlerts, - title: i18n.translate('xpack.securitySolution.search.network.externalAlerts', { - defaultMessage: 'External Alerts', - }), - path: `${NETWORK_PATH}/external-alerts`, - }, { id: SecurityPageName.networkAnomalies, title: i18n.translate('xpack.securitySolution.search.hosts.anomalies', { @@ -375,13 +363,17 @@ export const securitySolutionsDeepLinks: SecuritySolutionDeepLink[] = [ }), path: `${USERS_PATH}/events`, }, - { - id: SecurityPageName.usersExternalAlerts, - title: i18n.translate('xpack.securitySolution.search.users.externalAlerts', { - defaultMessage: 'External Alerts', - }), - path: `${USERS_PATH}/externalAlerts`, - }, + ], + }, + { + id: SecurityPageName.threatIntelligence, + title: THREAT_INTELLIGENCE, + path: THREAT_INTELLIGENCE_PATH, + navLinkStatus: AppNavLinkStatus.hidden, + keywords: [ + i18n.translate('xpack.securitySolution.search.threatIntelligence', { + defaultMessage: 'Threat Intelligence', + }), ], }, { diff --git a/x-pack/plugins/security_solution/public/app/home/home_navigations.ts b/x-pack/plugins/security_solution/public/app/home/home_navigations.ts index 0d8795dcdf8a7..d732135c5337b 100644 --- a/x-pack/plugins/security_solution/public/app/home/home_navigations.ts +++ b/x-pack/plugins/security_solution/public/app/home/home_navigations.ts @@ -30,6 +30,7 @@ import { APP_KUBERNETES_PATH, APP_LANDING_PATH, APP_RESPONSE_ACTIONS_PATH, + APP_THREAT_INTELLIGENCE_PATH, } from '../../../common/constants'; export const navTabs: SecurityNav = { @@ -173,6 +174,13 @@ export const navTabs: SecurityNav = { disabled: false, urlKey: 'administration', }, + [SecurityPageName.threatIntelligence]: { + id: SecurityPageName.threatIntelligence, + name: i18n.THREAT_INTELLIGENCE, + href: APP_THREAT_INTELLIGENCE_PATH, + disabled: false, + urlKey: 'threat_intelligence', + }, }; export const securityNavGroup: SecurityNavGroup = { diff --git a/x-pack/plugins/security_solution/public/app/translations.ts b/x-pack/plugins/security_solution/public/app/translations.ts index a8817802bcb75..776df745b323f 100644 --- a/x-pack/plugins/security_solution/public/app/translations.ts +++ b/x-pack/plugins/security_solution/public/app/translations.ts @@ -138,3 +138,10 @@ export const NO_PERMISSIONS_MSG = (subPluginKey: string) => export const NO_PERMISSIONS_TITLE = i18n.translate('xpack.securitySolution.noPermissionsTitle', { defaultMessage: 'Privileges required', }); + +export const THREAT_INTELLIGENCE = i18n.translate( + 'xpack.securitySolution.navigation.threatIntelligence', + { + defaultMessage: 'Threat Intelligence', + } +); diff --git a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx deleted file mode 100644 index 60a4f1c8a23d3..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useEffect, useMemo } from 'react'; -import { useDispatch } from 'react-redux'; -import type { Filter } from '@kbn/es-query'; -import type { EntityType } from '@kbn/timelines-plugin/common'; -import { timelineActions } from '../../../timelines/store/timeline'; -import type { TimelineIdLiteral } from '../../../../common/types/timeline'; -import { StatefulEventsViewer } from '../events_viewer'; -import { alertsDefaultModel } from './default_headers'; -import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers'; -import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer'; -import * as i18n from './translations'; -import { defaultCellActions } from '../../lib/cell_actions/default_cell_actions'; -import { useKibana } from '../../lib/kibana'; -import { SourcererScopeName } from '../../store/sourcerer/model'; -import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features'; -import { DEFAULT_COLUMN_MIN_WIDTH } from '../../../timelines/components/timeline/body/constants'; -import { getDefaultControlColumn } from '../../../timelines/components/timeline/body/control_columns'; - -export interface OwnProps { - end: string; - id: string; - start: string; -} - -const defaultAlertsFilters: Filter[] = [ - { - meta: { - alias: null, - negate: false, - disabled: false, - type: 'phrase', - key: 'event.kind', - params: { - query: 'alert', - }, - }, - query: { - bool: { - filter: [ - { - bool: { - should: [ - { - match: { - 'event.kind': 'alert', - }, - }, - ], - minimum_should_match: 1, - }, - }, - ], - }, - }, - }, -]; - -interface Props { - timelineId: TimelineIdLiteral; - endDate: string; - entityType?: EntityType; - startDate: string; - pageFilters?: Filter[]; -} - -const AlertsTableComponent: React.FC = ({ - timelineId, - endDate, - entityType = 'alerts', - startDate, - pageFilters = [], -}) => { - const dispatch = useDispatch(); - const alertsFilter = useMemo(() => [...defaultAlertsFilters, ...pageFilters], [pageFilters]); - const { filterManager } = useKibana().services.data.query; - const ACTION_BUTTON_COUNT = 5; - - const tGridEnabled = useIsExperimentalFeatureEnabled('tGridEnabled'); - - useEffect(() => { - dispatch( - timelineActions.initializeTGridSettings({ - id: timelineId, - documentType: i18n.ALERTS_DOCUMENT_TYPE, - filterManager, - defaultColumns: alertsDefaultModel.columns.map((c) => - !tGridEnabled && c.initialWidth == null - ? { - ...c, - initialWidth: DEFAULT_COLUMN_MIN_WIDTH, - } - : c - ), - excludedRowRendererIds: alertsDefaultModel.excludedRowRendererIds, - footerText: i18n.TOTAL_COUNT_OF_ALERTS, - title: i18n.ALERTS_TABLE_TITLE, - // TODO: avoid passing this through the store - }) - ); - }, [dispatch, filterManager, tGridEnabled, timelineId]); - - const leadingControlColumns = useMemo(() => getDefaultControlColumn(ACTION_BUTTON_COUNT), []); - - return ( - - ); -}; - -export const AlertsTable = React.memo(AlertsTableComponent); diff --git a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/histogram_configs.ts b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/histogram_configs.ts deleted file mode 100644 index 143bfb3d4fe7e..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/histogram_configs.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as i18n from './translations'; -import type { MatrixHistogramOption, MatrixHistogramConfigs } from '../matrix_histogram/types'; -import { MatrixHistogramType } from '../../../../common/search_strategy/security_solution/matrix_histogram'; -import { getExternalAlertLensAttributes } from '../visualization_actions/lens_attributes/common/external_alert'; - -export const alertsStackByOptions: MatrixHistogramOption[] = [ - { - text: 'event.category', - value: 'event.category', - }, - { - text: 'event.module', - value: 'event.module', - }, -]; - -const DEFAULT_STACK_BY = 'event.module'; - -export const histogramConfigs: MatrixHistogramConfigs = { - defaultStackByOption: - alertsStackByOptions.find((o) => o.text === DEFAULT_STACK_BY) ?? alertsStackByOptions[1], - errorMessage: i18n.ERROR_FETCHING_ALERTS_DATA, - histogramType: MatrixHistogramType.alerts, - stackByOptions: alertsStackByOptions, - subtitle: undefined, - title: i18n.ALERTS_GRAPH_TITLE, - getLensAttributes: getExternalAlertLensAttributes, -}; diff --git a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/index.tsx deleted file mode 100644 index e6dde5c8c0cd1..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/index.tsx +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useEffect, useCallback, useMemo } from 'react'; -import numeral from '@elastic/numeral'; - -import { DEFAULT_NUMBER_FORMAT } from '../../../../common/constants'; -import { useGlobalFullScreen } from '../../containers/use_full_screen'; - -import type { AlertsComponentsProps } from './types'; -import { AlertsTable } from './alerts_table'; -import * as i18n from './translations'; -import { useUiSetting$ } from '../../lib/kibana'; -import { MatrixHistogram } from '../matrix_histogram'; -import { histogramConfigs } from './histogram_configs'; -import type { MatrixHistogramConfigs } from '../matrix_histogram/types'; - -const ID = 'alertsHistogramQuery'; - -const AlertsViewComponent: React.FC = ({ - timelineId, - deleteQuery, - endDate, - entityType, - filterQuery, - indexNames, - pageFilters, - setQuery, - startDate, -}) => { - const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); - const { globalFullScreen } = useGlobalFullScreen(); - - const getSubtitle = useCallback( - (totalCount: number) => - `${i18n.SHOWING}: ${numeral(totalCount).format(defaultNumberFormat)} ${i18n.UNIT( - totalCount - )}`, - [defaultNumberFormat] - ); - - const alertsHistogramConfigs: MatrixHistogramConfigs = useMemo( - () => ({ - ...histogramConfigs, - subtitle: getSubtitle, - }), - [getSubtitle] - ); - - useEffect(() => { - return () => { - if (deleteQuery) { - deleteQuery({ id: ID }); - } - }; - }, [deleteQuery]); - - return ( - <> - {!globalFullScreen && ( - - )} - - - ); -}; - -AlertsViewComponent.displayName = 'AlertsViewComponent'; - -export const AlertsView = React.memo(AlertsViewComponent); diff --git a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/translations.ts b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/translations.ts deleted file mode 100644 index ab9cb3f3e3479..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/translations.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; - -export const ALERTS_DOCUMENT_TYPE = i18n.translate( - 'xpack.securitySolution.alertsView.alertsDocumentType', - { - defaultMessage: 'External alerts', - } -); - -export const TOTAL_COUNT_OF_ALERTS = i18n.translate( - 'xpack.securitySolution.alertsView.totalCountOfAlerts', - { - defaultMessage: 'external alerts', - } -); - -export const ALERTS_TABLE_TITLE = i18n.translate( - 'xpack.securitySolution.alertsView.alertsTableTitle', - { - defaultMessage: 'External alerts', - } -); - -export const ALERTS_GRAPH_TITLE = i18n.translate( - 'xpack.securitySolution.alertsView.alertsGraphTitle', - { - defaultMessage: 'External alert trend', - } -); - -export const SHOWING = i18n.translate('xpack.securitySolution.alertsView.showing', { - defaultMessage: 'Showing', -}); - -export const UNIT = (totalCount: number) => - i18n.translate('xpack.securitySolution.alertsView.unit', { - values: { totalCount }, - defaultMessage: `external {totalCount, plural, =1 {alert} other {alerts}}`, - }); - -export const ERROR_FETCHING_ALERTS_DATA = i18n.translate( - 'xpack.securitySolution.alertsView.errorFetchingAlertsData', - { - defaultMessage: 'Failed to query alerts data', - } -); - -export const CATEGORY = i18n.translate('xpack.securitySolution.alertsView.categoryLabel', { - defaultMessage: 'category', -}); diff --git a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/types.ts b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/types.ts deleted file mode 100644 index d212fe21c54a8..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/types.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { Filter } from '@kbn/es-query'; -import type { EntityType } from '@kbn/timelines-plugin/common'; -import type { TimelineIdLiteral } from '../../../../common/types/timeline'; -import type { HostsComponentsQueryProps } from '../../../hosts/pages/navigation/types'; -import type { NetworkComponentQueryProps } from '../../../network/pages/navigation/types'; -import type { MatrixHistogramOption } from '../matrix_histogram/types'; - -type CommonQueryProps = HostsComponentsQueryProps | NetworkComponentQueryProps; - -export interface AlertsComponentsProps - extends Pick< - CommonQueryProps, - 'deleteQuery' | 'endDate' | 'filterQuery' | 'skip' | 'setQuery' | 'startDate' - > { - timelineId: TimelineIdLiteral; - pageFilters: Filter[]; - stackByOptions?: MatrixHistogramOption[]; - defaultFilters?: Filter[]; - defaultStackByOption?: MatrixHistogramOption; - entityType?: EntityType; - indexNames: string[]; -} diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/drag_drop_context_wrapper.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/drag_drop_context_wrapper.tsx index f8d73dd10a779..fa0d2ffda0391 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/drag_drop_context_wrapper.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/drag_drop_context_wrapper.tsx @@ -42,7 +42,7 @@ import { import { useDeepEqualSelector } from '../../hooks/use_selector'; import { useKibana } from '../../lib/kibana'; import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; -import { alertsHeaders } from '../alerts_viewer/default_headers'; +import { defaultAlertsHeaders } from '../events_viewer/default_alert_headers'; // @ts-expect-error window['__react-beautiful-dnd-disable-dev-warnings'] = true; @@ -89,7 +89,7 @@ const onDragEndHandler = ({ } else if (fieldWasDroppedOnTimelineColumns(result)) { addFieldToTimelineColumns({ browserFields, - defaultsHeader: alertsHeaders, + defaultsHeader: defaultAlertsHeaders, dispatch, result, timelineId: getTimelineIdFromColumnDroppableId(result.destination?.droppableId ?? ''), diff --git a/x-pack/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.test.tsx index 5f7d6c226c76f..2058cd4a7c614 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.test.tsx @@ -11,7 +11,7 @@ import { TimelineId } from '../../../../common/types'; import { HostsType } from '../../../hosts/store/model'; import { TestProviders } from '../../mock'; import type { EventsQueryTabBodyComponentProps } from './events_query_tab_body'; -import { EventsQueryTabBody } from './events_query_tab_body'; +import { EventsQueryTabBody, ALERTS_EVENTS_HISTOGRAM_ID } from './events_query_tab_body'; import { useGlobalFullScreen } from '../../containers/use_full_screen'; import * as tGridActions from '@kbn/timelines-plugin/public/store/t_grid/actions'; @@ -33,12 +33,17 @@ jest.mock('../../lib/kibana', () => { }; }); -const FakeStatefulEventsViewer = () =>
{'MockedStatefulEventsViewer'}
; +const FakeStatefulEventsViewer = ({ additionalFilters }: { additionalFilters: JSX.Element }) => ( +
+ {additionalFilters} + {'MockedStatefulEventsViewer'} +
+); jest.mock('../events_viewer', () => ({ StatefulEventsViewer: FakeStatefulEventsViewer })); jest.mock('../../containers/use_full_screen', () => ({ useGlobalFullScreen: jest.fn().mockReturnValue({ - globalFullScreen: true, + globalFullScreen: false, }), })); @@ -52,6 +57,10 @@ describe('EventsQueryTabBody', () => { startDate: new Date('2000').toISOString(), }; + beforeEach(() => { + jest.clearAllMocks(); + }); + it('renders EventsViewer', () => { const { queryByText } = render( @@ -63,7 +72,7 @@ describe('EventsQueryTabBody', () => { }); it('renders the matrix histogram when globalFullScreen is false', () => { - (useGlobalFullScreen as jest.Mock).mockReturnValue({ + (useGlobalFullScreen as jest.Mock).mockReturnValueOnce({ globalFullScreen: false, }); @@ -73,11 +82,11 @@ describe('EventsQueryTabBody', () => { ); - expect(queryByTestId('eventsHistogramQueryPanel')).toBeInTheDocument(); + expect(queryByTestId(`${ALERTS_EVENTS_HISTOGRAM_ID}Panel`)).toBeInTheDocument(); }); it("doesn't render the matrix histogram when globalFullScreen is true", () => { - (useGlobalFullScreen as jest.Mock).mockReturnValue({ + (useGlobalFullScreen as jest.Mock).mockReturnValueOnce({ globalFullScreen: true, }); @@ -87,7 +96,33 @@ describe('EventsQueryTabBody', () => { ); - expect(queryByTestId('eventsHistogramQueryPanel')).not.toBeInTheDocument(); + expect(queryByTestId(`${ALERTS_EVENTS_HISTOGRAM_ID}Panel`)).not.toBeInTheDocument(); + }); + + it('renders the matrix histogram stacked by events default value', () => { + const result = render( + + + + ); + + expect(result.getByTestId('header-section-supplements').querySelector('select')?.value).toEqual( + 'event.action' + ); + }); + + it('renders the matrix histogram stacked by alerts default value', () => { + const result = render( + + + + ); + + result.getByTestId('showExternalAlertsCheckbox').click(); + + expect(result.getByTestId('header-section-supplements').querySelector('select')?.value).toEqual( + 'event.module' + ); }); it('deletes query when unmouting', () => { diff --git a/x-pack/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.tsx b/x-pack/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.tsx index 9227bc1849859..5fc70bd8bd08a 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_tab/events_query_tab_body.tsx @@ -5,69 +5,52 @@ * 2.0. */ -import React, { useEffect, useMemo } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useDispatch } from 'react-redux'; +import { EuiCheckbox } from '@elastic/eui'; import type { Filter } from '@kbn/es-query'; +import type { EntityType } from '@kbn/timelines-plugin/common'; + import type { TimelineId } from '../../../../common/types/timeline'; +import { RowRendererId } from '../../../../common/types/timeline'; import { StatefulEventsViewer } from '../events_viewer'; import { timelineActions } from '../../../timelines/store/timeline'; import { eventsDefaultModel } from '../events_viewer/default_model'; - import { MatrixHistogram } from '../matrix_histogram'; import { useGlobalFullScreen } from '../../containers/use_full_screen'; -import * as i18n from '../../../hosts/pages/translations'; -import { MatrixHistogramType } from '../../../../common/search_strategy/security_solution'; +import * as i18n from './translations'; +import { DEFAULT_NUMBER_FORMAT } from '../../../../common/constants'; +import { + alertsHistogramConfig, + eventsHistogramConfig, + getSubtitleFunction, +} from './histogram_configurations'; import { getDefaultControlColumn } from '../../../timelines/components/timeline/body/control_columns'; import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers'; import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer'; import { SourcererScopeName } from '../../store/sourcerer/model'; import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features'; import { DEFAULT_COLUMN_MIN_WIDTH } from '../../../timelines/components/timeline/body/constants'; -import { getEventsHistogramLensAttributes } from '../visualization_actions/lens_attributes/hosts/events'; import { defaultCellActions } from '../../lib/cell_actions/default_cell_actions'; import type { GlobalTimeArgs } from '../../containers/use_global_time'; -import type { MatrixHistogramConfigs, MatrixHistogramOption } from '../matrix_histogram/types'; import type { QueryTabBodyProps as UserQueryTabBodyProps } from '../../../users/pages/navigation/types'; import type { QueryTabBodyProps as HostQueryTabBodyProps } from '../../../hosts/pages/navigation/types'; +import type { QueryTabBodyProps as NetworkQueryTabBodyProps } from '../../../network/pages/navigation/types'; -const EVENTS_HISTOGRAM_ID = 'eventsHistogramQuery'; - -export const eventsStackByOptions: MatrixHistogramOption[] = [ - { - text: 'event.action', - value: 'event.action', - }, - { - text: 'event.dataset', - value: 'event.dataset', - }, - { - text: 'event.module', - value: 'event.module', - }, -]; - -const DEFAULT_STACK_BY = 'event.action'; -const unit = (n: number) => i18n.EVENTS_UNIT(n); - -export const histogramConfigs: MatrixHistogramConfigs = { - defaultStackByOption: - eventsStackByOptions.find((o) => o.text === DEFAULT_STACK_BY) ?? eventsStackByOptions[0], - errorMessage: i18n.ERROR_FETCHING_EVENTS_DATA, - histogramType: MatrixHistogramType.events, - stackByOptions: eventsStackByOptions, - subtitle: undefined, - title: i18n.NAVIGATION_EVENTS_TITLE, - getLensAttributes: getEventsHistogramLensAttributes, -}; +import { useUiSetting$ } from '../../lib/kibana'; +import { defaultAlertsFilters } from '../events_viewer/external_alerts_filter'; -type QueryTabBodyProps = UserQueryTabBodyProps | HostQueryTabBodyProps; +const ACTION_BUTTON_COUNT = 5; +export const ALERTS_EVENTS_HISTOGRAM_ID = 'alertsOrEventsHistogramQuery'; + +type QueryTabBodyProps = UserQueryTabBodyProps | HostQueryTabBodyProps | NetworkQueryTabBodyProps; export type EventsQueryTabBodyComponentProps = QueryTabBodyProps & { deleteQuery?: GlobalTimeArgs['deleteQuery']; indexNames: string[]; pageFilters?: Filter[]; + externalAlertPageFilters?: Filter[]; setQuery: GlobalTimeArgs['setQuery']; timelineId: TimelineId; }; @@ -77,15 +60,24 @@ const EventsQueryTabBodyComponent: React.FC = endDate, filterQuery, indexNames, - pageFilters, + externalAlertPageFilters = [], + pageFilters = [], setQuery, startDate, timelineId, }) => { const dispatch = useDispatch(); const { globalFullScreen } = useGlobalFullScreen(); - const ACTION_BUTTON_COUNT = 5; const tGridEnabled = useIsExperimentalFeatureEnabled('tGridEnabled'); + const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); + const [showExternalAlerts, setShowExternalAlerts] = useState(false); + const leadingControlColumns = useMemo(() => getDefaultControlColumn(ACTION_BUTTON_COUNT), []); + + const toggleExternalAlerts = useCallback(() => setShowExternalAlerts((s) => !s), []); + const getHistogramSubtitle = useMemo( + () => getSubtitleFunction(defaultNumberFormat, showExternalAlerts), + [defaultNumberFormat, showExternalAlerts] + ); useEffect(() => { dispatch( @@ -99,46 +91,78 @@ const EventsQueryTabBodyComponent: React.FC = } : c ), + excludedRowRendererIds: showExternalAlerts ? Object.values(RowRendererId) : undefined, }) ); - }, [dispatch, tGridEnabled, timelineId]); + }, [dispatch, showExternalAlerts, tGridEnabled, timelineId]); useEffect(() => { return () => { if (deleteQuery) { - deleteQuery({ id: EVENTS_HISTOGRAM_ID }); + deleteQuery({ id: ALERTS_EVENTS_HISTOGRAM_ID }); } }; }, [deleteQuery]); - const leadingControlColumns = useMemo(() => getDefaultControlColumn(ACTION_BUTTON_COUNT), []); + const additionalFilters = useMemo( + () => ( + + ), + [showExternalAlerts, toggleExternalAlerts] + ); + + const defaultModel = useMemo( + () => ({ + ...eventsDefaultModel, + excludedRowRendererIds: showExternalAlerts ? Object.values(RowRendererId) : [], + }), + [showExternalAlerts] + ); + + const composedPageFilters = useMemo( + () => [ + ...pageFilters, + ...(showExternalAlerts ? [defaultAlertsFilters, ...externalAlertPageFilters] : []), + ], + [showExternalAlerts, externalAlertPageFilters, pageFilters] + ); return ( <> {!globalFullScreen && ( )} ); diff --git a/x-pack/plugins/security_solution/public/common/components/events_tab/histogram_configurations.ts b/x-pack/plugins/security_solution/public/common/components/events_tab/histogram_configurations.ts new file mode 100644 index 0000000000000..fce972a013834 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/events_tab/histogram_configurations.ts @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import numeral from '@elastic/numeral'; + +import { MatrixHistogramType } from '../../../../common/search_strategy/security_solution'; +import { getExternalAlertLensAttributes } from '../visualization_actions/lens_attributes/common/external_alert'; +import { getEventsHistogramLensAttributes } from '../visualization_actions/lens_attributes/hosts/events'; +import type { MatrixHistogramConfigs, MatrixHistogramOption } from '../matrix_histogram/types'; +import * as i18n from './translations'; + +const DEFAULT_EVENTS_STACK_BY = 'event.action'; + +export const getSubtitleFunction = + (defaultNumberFormat: string, isAlert: boolean) => (totalCount: number) => + `${i18n.SHOWING}: ${numeral(totalCount).format(defaultNumberFormat)} ${ + isAlert ? i18n.ALERTS_UNIT(totalCount) : i18n.EVENTS_UNIT(totalCount) + }`; + +export const eventsStackByOptions: MatrixHistogramOption[] = [ + { + text: 'event.action', + value: 'event.action', + }, + { + text: 'event.dataset', + value: 'event.dataset', + }, + { + text: 'event.module', + value: 'event.module', + }, +]; + +export const eventsHistogramConfig: MatrixHistogramConfigs = { + defaultStackByOption: + eventsStackByOptions.find((o) => o.text === DEFAULT_EVENTS_STACK_BY) ?? eventsStackByOptions[0], + errorMessage: i18n.ERROR_FETCHING_EVENTS_DATA, + histogramType: MatrixHistogramType.events, + stackByOptions: eventsStackByOptions, + subtitle: undefined, + title: i18n.EVENTS_GRAPH_TITLE, + getLensAttributes: getEventsHistogramLensAttributes, +}; + +export const alertsStackByOptions: MatrixHistogramOption[] = [ + { + text: 'event.category', + value: 'event.category', + }, + { + text: 'event.module', + value: 'event.module', + }, +]; + +const DEFAULT_STACK_BY = 'event.module'; + +export const alertsHistogramConfig: MatrixHistogramConfigs = { + defaultStackByOption: + alertsStackByOptions.find((o) => o.text === DEFAULT_STACK_BY) ?? alertsStackByOptions[0], + errorMessage: i18n.ERROR_FETCHING_ALERTS_DATA, + histogramType: MatrixHistogramType.alerts, + stackByOptions: alertsStackByOptions, + subtitle: undefined, + title: i18n.ALERTS_GRAPH_TITLE, + getLensAttributes: getExternalAlertLensAttributes, +}; diff --git a/x-pack/plugins/security_solution/public/common/components/events_tab/index.ts b/x-pack/plugins/security_solution/public/common/components/events_tab/index.ts new file mode 100644 index 0000000000000..942c633517b48 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/events_tab/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { EventsQueryTabBody } from './events_query_tab_body'; +export { + alertsHistogramConfig, + alertsStackByOptions, + eventsHistogramConfig, + eventsStackByOptions, +} from './histogram_configurations'; diff --git a/x-pack/plugins/security_solution/public/common/components/events_tab/translations.ts b/x-pack/plugins/security_solution/public/common/components/events_tab/translations.ts new file mode 100644 index 0000000000000..abcb1511cb1cc --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/events_tab/translations.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const ALERTS_UNIT = (totalCount: number) => + i18n.translate('xpack.securitySolution.eventsTab.unit', { + values: { totalCount }, + defaultMessage: `external {totalCount, plural, =1 {alert} other {alerts}}`, + }); + +export const EVENTS_UNIT = (totalCount: number) => + i18n.translate('xpack.securitySolution.hosts.navigaton.eventsUnit', { + values: { totalCount }, + defaultMessage: `{totalCount, plural, =1 {event} other {events}}`, + }); + +export const SHOWING = i18n.translate('xpack.securitySolution.eventsTab.showing', { + defaultMessage: 'Showing', +}); + +export const ALERTS_GRAPH_TITLE = i18n.translate( + 'xpack.securitySolution.eventsTab.alertsGraphTitle', + { + defaultMessage: 'External alert trend', + } +); + +export const ERROR_FETCHING_ALERTS_DATA = i18n.translate( + 'xpack.securitySolution.eventsTab.errorFetchingAlertsData', + { + defaultMessage: 'Failed to query alerts data', + } +); + +export const ERROR_FETCHING_EVENTS_DATA = i18n.translate( + 'xpack.securitySolution.eventsTab.errorFetchingEventsData', + { + defaultMessage: 'Failed to query events data', + } +); + +export const SHOW_EXTERNAL_ALERTS = i18n.translate( + 'xpack.securitySolution.eventsTab.showExternalAlerts', + { + defaultMessage: 'Show only external alerts', + } +); + +export const EVENTS_GRAPH_TITLE = i18n.translate('xpack.securitySolution.eventsGraphTitle', { + defaultMessage: 'Events', +}); diff --git a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/default_headers.ts b/x-pack/plugins/security_solution/public/common/components/events_viewer/default_alert_headers.ts similarity index 76% rename from x-pack/plugins/security_solution/public/common/components/alerts_viewer/default_headers.ts rename to x-pack/plugins/security_solution/public/common/components/events_viewer/default_alert_headers.ts index 3bf7ab3e9a4da..ac53ac568d729 100644 --- a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/default_headers.ts +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/default_alert_headers.ts @@ -6,13 +6,10 @@ */ import type { ColumnHeaderOptions } from '../../../../common/types/timeline'; -import { RowRendererId } from '../../../../common/types/timeline'; import { defaultColumnHeaderType } from '../../../timelines/components/timeline/body/column_headers/default_headers'; import { DEFAULT_DATE_COLUMN_MIN_WIDTH } from '../../../timelines/components/timeline/body/constants'; -import type { SubsetTimelineModel } from '../../../timelines/store/timeline/model'; -import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; -export const alertsHeaders: ColumnHeaderOptions[] = [ +export const defaultAlertsHeaders: ColumnHeaderOptions[] = [ { columnHeaderType: defaultColumnHeaderType, id: '@timestamp', @@ -56,9 +53,3 @@ export const alertsHeaders: ColumnHeaderOptions[] = [ id: 'agent.type', }, ]; - -export const alertsDefaultModel: SubsetTimelineModel = { - ...timelineDefaults, - columns: alertsHeaders, - excludedRowRendererIds: Object.values(RowRendererId), -}; diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/default_headers.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/default_event_headers.tsx similarity index 90% rename from x-pack/plugins/security_solution/public/common/components/events_viewer/default_headers.tsx rename to x-pack/plugins/security_solution/public/common/components/events_viewer/default_event_headers.tsx index 0a36d3a8215bb..3e312737018e7 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/default_headers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/default_event_headers.tsx @@ -9,7 +9,7 @@ import type { ColumnHeaderOptions } from '../../../../common/types'; import { defaultColumnHeaderType } from '../../../timelines/components/timeline/body/column_headers/default_headers'; import { DEFAULT_DATE_COLUMN_MIN_WIDTH } from '../../../timelines/components/timeline/body/constants'; -export const defaultHeaders: ColumnHeaderOptions[] = [ +export const defaultEventHeaders: ColumnHeaderOptions[] = [ { columnHeaderType: defaultColumnHeaderType, id: '@timestamp', @@ -29,6 +29,10 @@ export const defaultHeaders: ColumnHeaderOptions[] = [ columnHeaderType: defaultColumnHeaderType, id: 'event.module', }, + { + columnHeaderType: defaultColumnHeaderType, + id: 'agent.type', + }, { columnHeaderType: defaultColumnHeaderType, id: 'event.dataset', diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/default_model.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/default_model.tsx index 385fd43daae8f..7b9097aed3c46 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/default_model.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/default_model.tsx @@ -5,11 +5,11 @@ * 2.0. */ -import { defaultHeaders } from './default_headers'; +import { defaultEventHeaders } from './default_event_headers'; import type { SubsetTimelineModel } from '../../../timelines/store/timeline/model'; import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; export const eventsDefaultModel: SubsetTimelineModel = { ...timelineDefaults, - columns: defaultHeaders, + columns: defaultEventHeaders, }; diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/external_alerts_filter.ts b/x-pack/plugins/security_solution/public/common/components/events_viewer/external_alerts_filter.ts new file mode 100644 index 0000000000000..42f5af73972af --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/external_alerts_filter.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Filter } from '@kbn/es-query'; + +export const defaultAlertsFilters: Filter = { + meta: { + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'event.kind', + params: { + query: 'alert', + }, + }, + query: { + bool: { + filter: [ + { + bool: { + should: [ + { + match: { + 'event.kind': 'alert', + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + }, + }, +}; diff --git a/x-pack/plugins/security_solution/public/common/components/health_truncate_text/index.tsx b/x-pack/plugins/security_solution/public/common/components/health_truncate_text/index.tsx index 830b1776d8fee..57a893cb1eadc 100644 --- a/x-pack/plugins/security_solution/public/common/components/health_truncate_text/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/health_truncate_text/index.tsx @@ -6,9 +6,9 @@ */ import React from 'react'; +import styled from 'styled-components'; import type { EuiHealthProps } from '@elastic/eui'; import { EuiHealth, EuiToolTip } from '@elastic/eui'; -import styled from 'styled-components'; const StatusTextWrapper = styled.div` width: 100%; diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.test.tsx index 68ef266b07b25..49eb8fc3681d6 100644 --- a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.test.tsx @@ -46,7 +46,7 @@ jest.mock('../../utils/route/use_route_spy', () => ({ { detailName: 'mockHost', pageName: 'hosts', - tabName: 'externalAlerts', + tabName: 'events', }, ]), })); @@ -181,7 +181,7 @@ describe('Matrix Histogram Component', () => { { detailName: 'mockHost', pageName: 'hosts', - tabName: 'externalAlerts', + tabName: 'events', }, ]); @@ -240,7 +240,7 @@ describe('Matrix Histogram Component', () => { { detailName: 'mockHost', pageName: 'hosts', - tabName: 'externalAlerts', + tabName: 'events', }, ]); diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx index fb48682f3370c..ae214bd201bf5 100644 --- a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx @@ -138,6 +138,11 @@ export const MatrixHistogramComponent: React.FC = const [isInitialLoading, setIsInitialLoading] = useState(true); const [selectedStackByOption, setSelectedStackByOption] = useState(defaultStackByOption); + + useEffect(() => { + setSelectedStackByOption(defaultStackByOption); + }, [defaultStackByOption]); + const setSelectedChartOptionCallback = useCallback( (event: React.ChangeEvent) => { setSelectedStackByOption( diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/types.ts b/x-pack/plugins/security_solution/public/common/components/navigation/types.ts index 9c5fa4d0d15ad..8980e367efe5b 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/navigation/types.ts @@ -64,6 +64,7 @@ export const securityNavKeys = [ SecurityPageName.trustedApps, SecurityPageName.users, SecurityPageName.kubernetes, + SecurityPageName.threatIntelligence, ] as const; export type SecurityNavKey = typeof securityNavKeys[number]; diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/__snapshots__/index.test.tsx.snap index 4da2600f39292..c02e7adbb6eaa 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/__snapshots__/index.test.tsx.snap @@ -116,6 +116,16 @@ Object { "name": "Users", "onClick": [Function], }, + Object { + "data-href": "securitySolutionUI/threat-intelligence", + "data-test-subj": "navigation-threat-intelligence", + "disabled": false, + "href": "securitySolutionUI/threat-intelligence", + "id": "threat-intelligence", + "isSelected": false, + "name": "Threat Intelligence", + "onClick": [Function], + }, ], "name": "Explore", }, diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx index 12223d9042980..bb15fd45cef9f 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx @@ -109,6 +109,7 @@ function usePrimaryNavigationItemsToDisplay(navTabs: Record) { ...(navTabs[SecurityPageName.users] != null ? [navTabs[SecurityPageName.users]] : []), + navTabs[SecurityPageName.threatIntelligence], ], }, { diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/top_n/helpers.test.tsx index c4e24997ab9bf..fddc69cd1801f 100644 --- a/x-pack/plugins/security_solution/public/common/components/top_n/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/top_n/helpers.test.tsx @@ -27,9 +27,8 @@ const detectionAlertsTimelines = [TimelineId.detectionsPage, TimelineId.detectio /** the following `TimelineId`s are NOT detection alert tables */ const otherTimelines = [ TimelineId.hostsPageEvents, - TimelineId.hostsPageExternalAlerts, TimelineId.hostsPageSessions, - TimelineId.networkPageExternalAlerts, + TimelineId.networkPageEvents, TimelineId.active, TimelineId.casePage, TimelineId.test, diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx index c68fe932fdd62..f2a474e0173e5 100644 --- a/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx @@ -155,7 +155,7 @@ let testProps = { browserFields: mockBrowserFields, field, indexPattern: mockIndexPattern, - timelineId: TimelineId.hostsPageExternalAlerts, + timelineId: TimelineId.hostsPageEvents, toggleTopN: jest.fn(), onFilterAdded: jest.fn(), value, diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/top_n.test.tsx b/x-pack/plugins/security_solution/public/common/components/top_n/top_n.test.tsx index 82247f1c97025..12b72459a7f39 100644 --- a/x-pack/plugins/security_solution/public/common/components/top_n/top_n.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/top_n/top_n.test.tsx @@ -143,8 +143,7 @@ describe('TopN', () => { const nonDetectionAlertTables = [ TimelineId.hostsPageEvents, - TimelineId.hostsPageExternalAlerts, - TimelineId.networkPageExternalAlerts, + TimelineId.networkPageEvents, TimelineId.casePage, ]; diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/constants.ts b/x-pack/plugins/security_solution/public/common/components/url_state/constants.ts index 6e421b57a90c2..dfff0e8d4fe32 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/constants.ts +++ b/x-pack/plugins/security_solution/public/common/components/url_state/constants.ts @@ -42,4 +42,5 @@ export type UrlStateType = | 'rules' | 'timeline' | 'explore' - | 'dashboards'; + | 'dashboards' + | 'threat_intelligence'; diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_lens_attributes.test.tsx b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_lens_attributes.test.tsx index f5baf9f13b519..7db97ab5526a4 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_lens_attributes.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_lens_attributes.test.tsx @@ -34,7 +34,7 @@ jest.mock('../../utils/route/use_route_spy', () => ({ { detailName: 'mockHost', pageName: 'hosts', - tabName: 'externalAlerts', + tabName: 'events', }, ]), })); diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_lens_attributes.tsx b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_lens_attributes.tsx index 8424127bb238b..58220d34e6717 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_lens_attributes.tsx +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/use_lens_attributes.tsx @@ -41,10 +41,7 @@ export const useLensAttributes = ({ const [{ detailName, pageName, tabName }] = useRouteSpy(); const tabsFilters = useMemo(() => { - if ( - pageName === SecurityPageName.hosts && - (tabName === HostsTableType.alerts || tabName === HostsTableType.events) - ) { + if (pageName === SecurityPageName.hosts && tabName === HostsTableType.events) { return hostNameExistsFilter; } diff --git a/x-pack/plugins/security_solution/public/common/images/threat_intelligence.png b/x-pack/plugins/security_solution/public/common/images/threat_intelligence.png new file mode 100644 index 0000000000000..53badc4b84b17 Binary files /dev/null and b/x-pack/plugins/security_solution/public/common/images/threat_intelligence.png differ diff --git a/x-pack/plugins/security_solution/public/common/links/links.test.ts b/x-pack/plugins/security_solution/public/common/links/links.test.ts index 1fe3f11b213fd..c7ab4bec3a537 100644 --- a/x-pack/plugins/security_solution/public/common/links/links.test.ts +++ b/x-pack/plugins/security_solution/public/common/links/links.test.ts @@ -119,13 +119,6 @@ describe('Security app links', () => { ...networkLinkItem, // all its links should be filtered for all different criteria links: [ - { - id: SecurityPageName.networkExternalAlerts, - title: 'external alerts', - path: '/external_alerts', - experimentalKey: - 'flagDisabled' as unknown as keyof typeof mockExperimentalDefaults, - }, { id: SecurityPageName.networkDns, title: 'dns', diff --git a/x-pack/plugins/security_solution/public/common/mock/global_state.ts b/x-pack/plugins/security_solution/public/common/mock/global_state.ts index 8c0ffeab6fb78..027827474c780 100644 --- a/x-pack/plugins/security_solution/public/common/mock/global_state.ts +++ b/x-pack/plugins/security_solution/public/common/mock/global_state.ts @@ -81,7 +81,6 @@ export const mockGlobalState: State = { events: { activePage: 0, limit: 10 }, uncommonProcesses: { activePage: 0, limit: 10 }, anomalies: null, - externalAlerts: { activePage: 0, limit: 10 }, hostRisk: { activePage: 0, limit: 10, @@ -103,7 +102,6 @@ export const mockGlobalState: State = { events: { activePage: 0, limit: 10 }, uncommonProcesses: { activePage: 0, limit: 10 }, anomalies: null, - externalAlerts: { activePage: 0, limit: 10 }, hostRisk: { activePage: 0, limit: 10, @@ -223,14 +221,12 @@ export const mockGlobalState: State = { severitySelection: [], }, [usersModel.UsersTableType.events]: { activePage: 0, limit: 10 }, - [usersModel.UsersTableType.alerts]: { activePage: 0, limit: 10 }, }, }, details: { queries: { [usersModel.UsersTableType.anomalies]: null, [usersModel.UsersTableType.events]: { activePage: 0, limit: 10 }, - [usersModel.UsersTableType.alerts]: { activePage: 0, limit: 10 }, }, }, }, diff --git a/x-pack/plugins/security_solution/public/detection_engine/jest.config.js b/x-pack/plugins/security_solution/public/detection_engine/jest.config.js new file mode 100644 index 0000000000000..9f952742833e0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/jest.config.js @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../../..', + roots: ['/x-pack/plugins/security_solution/public/detection_engine'], + coverageDirectory: + '/target/kibana-coverage/jest/x-pack/plugins/security_solution/public/detection_engine', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/plugins/security_solution/public/detection_engine/**/*.{ts,tsx}', + ], + // See: https://github.com/elastic/kibana/issues/117255, the moduleNameMapper creates mocks to avoid memory leaks from kibana core. + moduleNameMapper: { + 'core/server$': '/x-pack/plugins/security_solution/server/__mocks__/core.mock.ts', + 'task_manager/server$': + '/x-pack/plugins/security_solution/server/__mocks__/task_manager.mock.ts', + 'alerting/server$': '/x-pack/plugins/security_solution/server/__mocks__/alert.mock.ts', + 'actions/server$': '/x-pack/plugins/security_solution/server/__mocks__/action.mock.ts', + }, +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/api/__mocks__/api_client.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/api/__mocks__/api_client.ts new file mode 100644 index 0000000000000..9fb1656ad4603 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/api/__mocks__/api_client.ts @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { + GetRuleExecutionEventsResponse, + GetRuleExecutionResultsResponse, +} from '../../../../../common/detection_engine/rule_monitoring'; +import { + LogLevel, + RuleExecutionEventType, +} from '../../../../../common/detection_engine/rule_monitoring'; + +import type { + FetchRuleExecutionEventsArgs, + FetchRuleExecutionResultsArgs, + IRuleMonitoringApiClient, +} from '../api_client_interface'; + +export const api: jest.Mocked = { + fetchRuleExecutionEvents: jest + .fn, [FetchRuleExecutionEventsArgs]>() + .mockResolvedValue({ + events: [ + { + timestamp: '2021-12-29T10:42:59.996Z', + sequence: 0, + level: LogLevel.info, + type: RuleExecutionEventType['status-change'], + message: 'Rule changed status to "succeeded". Rule execution completed without errors', + }, + ], + pagination: { + page: 1, + per_page: 20, + total: 1, + }, + }), + + fetchRuleExecutionResults: jest + .fn, [FetchRuleExecutionResultsArgs]>() + .mockResolvedValue({ + events: [ + { + duration_ms: 3866, + es_search_duration_ms: 1236, + execution_uuid: '88d15095-7937-462c-8f21-9763e1387cad', + gap_duration_s: 0, + indexing_duration_ms: 95, + message: + "rule executed: siem.queryRule:fb1fc150-a292-11ec-a2cf-c1b28b0392b0: 'Lots of Execution Events'", + num_active_alerts: 0, + num_errored_actions: 0, + num_new_alerts: 0, + num_recovered_alerts: 0, + num_succeeded_actions: 1, + num_triggered_actions: 1, + schedule_delay_ms: -127535, + search_duration_ms: 1255, + security_message: 'succeeded', + security_status: 'succeeded', + status: 'success', + timed_out: false, + timestamp: '2022-03-13T06:04:05.838Z', + total_search_duration_ms: 0, + }, + ], + total: 1, + }), +}; diff --git a/x-pack/plugins/security_solution/cypress/screens/users/user_external_alerts.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/api/__mocks__/index.ts similarity index 60% rename from x-pack/plugins/security_solution/cypress/screens/users/user_external_alerts.ts rename to x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/api/__mocks__/index.ts index bc98b3bc59f37..ebfcff4941a99 100644 --- a/x-pack/plugins/security_solution/cypress/screens/users/user_external_alerts.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/api/__mocks__/index.ts @@ -5,5 +5,4 @@ * 2.0. */ -export const EXTERNAL_ALERTS_TAB = '[data-test-subj="navigation-externalAlerts"]'; -export const EXTERNAL_ALERTS_TAB_CONTENT = '[data-test-subj="events-viewer-panel"]'; +export * from './api_client'; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/api/api_client.test.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/api/api_client.test.ts new file mode 100644 index 0000000000000..9c69b92210146 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/api/api_client.test.ts @@ -0,0 +1,138 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { KibanaServices } from '../../../common/lib/kibana'; + +import type { + GetRuleExecutionEventsResponse, + GetRuleExecutionResultsResponse, +} from '../../../../common/detection_engine/rule_monitoring'; +import { + LogLevel, + RuleExecutionEventType, +} from '../../../../common/detection_engine/rule_monitoring'; + +import { api } from './api_client'; + +jest.mock('../../../common/lib/kibana'); + +describe('Rule Monitoring API Client', () => { + const fetchMock = jest.fn(); + const mockKibanaServices = KibanaServices.get as jest.Mock; + mockKibanaServices.mockReturnValue({ http: { fetch: fetchMock } }); + + const signal = new AbortController().signal; + + describe('fetchRuleExecutionEvents', () => { + const responseMock: GetRuleExecutionEventsResponse = { + events: [], + pagination: { + page: 1, + per_page: 20, + total: 0, + }, + }; + + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(responseMock); + }); + + it('calls API correctly with only rule id specified', async () => { + await api.fetchRuleExecutionEvents({ ruleId: '42', signal }); + + expect(fetchMock).toHaveBeenCalledWith( + '/internal/detection_engine/rules/42/execution/events', + { + method: 'GET', + query: {}, + signal, + } + ); + }); + + it('calls API correctly with filter and pagination options', async () => { + await api.fetchRuleExecutionEvents({ + ruleId: '42', + eventTypes: [RuleExecutionEventType.message], + logLevels: [LogLevel.warn, LogLevel.error], + sortOrder: 'asc', + page: 42, + perPage: 146, + signal, + }); + + expect(fetchMock).toHaveBeenCalledWith( + '/internal/detection_engine/rules/42/execution/events', + { + method: 'GET', + query: { + event_types: 'message', + log_levels: 'warn,error', + sort_order: 'asc', + page: 42, + per_page: 146, + }, + signal, + } + ); + }); + }); + + describe('fetchRuleExecutionResults', () => { + const responseMock: GetRuleExecutionResultsResponse = { + events: [], + total: 0, + }; + + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(responseMock); + }); + + it('calls API with correct parameters', async () => { + await api.fetchRuleExecutionResults({ + ruleId: '42', + start: '2001-01-01T17:00:00.000Z', + end: '2001-01-02T17:00:00.000Z', + queryText: '', + statusFilters: [], + signal, + }); + + expect(fetchMock).toHaveBeenCalledWith( + '/internal/detection_engine/rules/42/execution/results', + { + method: 'GET', + query: { + end: '2001-01-02T17:00:00.000Z', + page: undefined, + per_page: undefined, + query_text: '', + sort_field: undefined, + sort_order: undefined, + start: '2001-01-01T17:00:00.000Z', + status_filters: '', + }, + signal, + } + ); + }); + + it('returns API response as is', async () => { + const response = await api.fetchRuleExecutionResults({ + ruleId: '42', + start: 'now-30', + end: 'now', + queryText: '', + statusFilters: [], + signal, + }); + expect(response).toEqual(responseMock); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/api/api_client.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/api/api_client.ts new file mode 100644 index 0000000000000..01bc89b7a6be9 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/api/api_client.ts @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import dateMath from '@kbn/datemath'; + +import { KibanaServices } from '../../../common/lib/kibana'; + +import type { + GetRuleExecutionEventsResponse, + GetRuleExecutionResultsResponse, +} from '../../../../common/detection_engine/rule_monitoring'; +import { + getRuleExecutionEventsUrl, + getRuleExecutionResultsUrl, +} from '../../../../common/detection_engine/rule_monitoring'; + +import type { + FetchRuleExecutionEventsArgs, + FetchRuleExecutionResultsArgs, + IRuleMonitoringApiClient, +} from './api_client_interface'; + +export const api: IRuleMonitoringApiClient = { + fetchRuleExecutionEvents: ( + args: FetchRuleExecutionEventsArgs + ): Promise => { + const { ruleId, eventTypes, logLevels, sortOrder, page, perPage, signal } = args; + + const url = getRuleExecutionEventsUrl(ruleId); + + return http().fetch(url, { + method: 'GET', + query: { + event_types: eventTypes?.join(','), + log_levels: logLevels?.join(','), + sort_order: sortOrder, + page, + per_page: perPage, + }, + signal, + }); + }, + + fetchRuleExecutionResults: ( + args: FetchRuleExecutionResultsArgs + ): Promise => { + const { + ruleId, + start, + end, + queryText, + statusFilters, + page, + perPage, + sortField, + sortOrder, + signal, + } = args; + + const url = getRuleExecutionResultsUrl(ruleId); + const startDate = dateMath.parse(start); + const endDate = dateMath.parse(end, { roundUp: true }); + + return http().fetch(url, { + method: 'GET', + query: { + start: startDate?.utc().toISOString(), + end: endDate?.utc().toISOString(), + query_text: queryText, + status_filters: statusFilters?.sort()?.join(','), + sort_field: sortField, + sort_order: sortOrder, + page, + per_page: perPage, + }, + signal, + }); + }, +}; + +const http = () => KibanaServices.get().http; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/api/api_client_interface.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/api/api_client_interface.ts new file mode 100644 index 0000000000000..b6136a15e2366 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/api/api_client_interface.ts @@ -0,0 +1,124 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { SortOrder } from '../../../../common/detection_engine/schemas/common'; +import type { + GetRuleExecutionEventsResponse, + GetRuleExecutionResultsResponse, + LogLevel, + RuleExecutionEventType, + RuleExecutionResult, + RuleExecutionStatus, +} from '../../../../common/detection_engine/rule_monitoring'; + +export interface IRuleMonitoringApiClient { + /** + * Fetches plain rule execution events (status changes, metrics, generic events) from Event Log. + * @throws An error if response is not OK. + */ + fetchRuleExecutionEvents( + args: FetchRuleExecutionEventsArgs + ): Promise; + + /** + * Fetches aggregated rule execution results (events grouped by execution UUID) from Event Log. + * @throws An error if response is not OK. + */ + fetchRuleExecutionResults( + args: FetchRuleExecutionResultsArgs + ): Promise; +} + +export interface FetchRuleExecutionEventsArgs { + /** + * Saved Object ID of the rule (`rule.id`, not static `rule.rule_id`). + */ + ruleId: string; + + /** + * Filter by event type. If set, result will include only events matching any of these. + */ + eventTypes?: RuleExecutionEventType[]; + + /** + * Filter by log level. If set, result will include only events matching any of these. + */ + logLevels?: LogLevel[]; + + /** + * What order to sort by (e.g. `asc` or `desc`). + */ + sortOrder?: SortOrder; + + /** + * Current page to fetch. + */ + page?: number; + + /** + * Number of results to fetch per page. + */ + perPage?: number; + + /** + * Optional signal for cancelling the request. + */ + signal?: AbortSignal; +} + +export interface FetchRuleExecutionResultsArgs { + /** + * Saved Object ID of the rule (`rule.id`, not static `rule.rule_id`). + */ + ruleId: string; + + /** + * Start daterange either in UTC ISO8601 or as datemath string (e.g. `2021-12-29T02:44:41.653Z` or `now-30`). + */ + start: string; + + /** + * End daterange either in UTC ISO8601 or as datemath string (e.g. `2021-12-29T02:44:41.653Z` or `now/w`). + */ + end: string; + + /** + * Search string in querystring format, e.g. + * `event.duration > 1000 OR kibana.alert.rule.execution.metrics.execution_gap_duration_s > 100`. + */ + queryText?: string; + + /** + * Array of `statusFilters` (e.g. `succeeded,failed,partial failure`). + */ + statusFilters?: RuleExecutionStatus[]; + + /** + * Keyof AggregateRuleExecutionEvent field to sort by. + */ + sortField?: keyof RuleExecutionResult; + + /** + * What order to sort by (e.g. `asc` or `desc`). + */ + sortOrder?: SortOrder; + + /** + * Current page to fetch. + */ + page?: number; + + /** + * Number of results to fetch per page. + */ + perPage?: number; + + /** + * Optional signal for cancelling the request. + */ + signal?: AbortSignal; +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/api/index.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/api/index.ts new file mode 100644 index 0000000000000..f20ead0b41939 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/api/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './api_client_interface'; +export * from './api_client'; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/filters/event_type_filter/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/filters/event_type_filter/index.tsx new file mode 100644 index 0000000000000..ba3776fd92983 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/filters/event_type_filter/index.tsx @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback } from 'react'; + +import type { RuleExecutionEventType } from '../../../../../../../common/detection_engine/rule_monitoring'; +import { RULE_EXECUTION_EVENT_TYPES } from '../../../../../../../common/detection_engine/rule_monitoring'; +import { EventTypeIndicator } from '../../indicators/event_type_indicator'; +import { MultiselectFilter } from '../multiselect_filter'; + +import * as i18n from './translations'; + +interface EventTypeFilterProps { + selectedItems: RuleExecutionEventType[]; + onChange: (selectedItems: RuleExecutionEventType[]) => void; +} + +const EventTypeFilterComponent: React.FC = ({ selectedItems, onChange }) => { + const renderItem = useCallback((item: RuleExecutionEventType) => { + return ; + }, []); + + return ( + + dataTestSubj="eventTypeFilter" + title={i18n.FILTER_TITLE} + items={RULE_EXECUTION_EVENT_TYPES} + selectedItems={selectedItems} + onSelectionChange={onChange} + renderItem={renderItem} + /> + ); +}; + +export const EventTypeFilter = React.memo(EventTypeFilterComponent); +EventTypeFilter.displayName = 'EventTypeFilter'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_messages.mock.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/filters/event_type_filter/translations.ts similarity index 54% rename from x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_messages.mock.ts rename to x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/filters/event_type_filter/translations.ts index 3fcd1125ab956..3097b1b789cc8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_messages.mock.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/filters/event_type_filter/translations.ts @@ -5,11 +5,11 @@ * 2.0. */ -import { buildRuleMessageFactory } from './rule_messages'; +import { i18n } from '@kbn/i18n'; -export const buildRuleMessageMock = buildRuleMessageFactory({ - id: 'fake id', - ruleId: 'fake rule id', - index: 'fakeindex', - name: 'fake name', -}); +export const FILTER_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleMonitoring.eventTypeFilter.filterTitle', + { + defaultMessage: 'Event type', + } +); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/filters/execution_status_filter/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/filters/execution_status_filter/index.tsx new file mode 100644 index 0000000000000..15ee510f8a414 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/filters/execution_status_filter/index.tsx @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback } from 'react'; + +import type { RuleExecutionStatus } from '../../../../../../../common/detection_engine/rule_monitoring'; +import { ExecutionStatusIndicator } from '../../indicators/execution_status_indicator'; +import { MultiselectFilter } from '../multiselect_filter'; + +import * as i18n from './translations'; + +interface ExecutionStatusFilterProps { + items: RuleExecutionStatus[]; + selectedItems: RuleExecutionStatus[]; + onChange: (selectedItems: RuleExecutionStatus[]) => void; +} + +const ExecutionStatusFilterComponent: React.FC = ({ + items, + selectedItems, + onChange, +}) => { + const renderItem = useCallback((item: RuleExecutionStatus) => { + return ; + }, []); + + return ( + + dataTestSubj="ExecutionStatusFilter" + title={i18n.FILTER_TITLE} + items={items} + selectedItems={selectedItems} + onSelectionChange={onChange} + renderItem={renderItem} + /> + ); +}; + +export const ExecutionStatusFilter = React.memo(ExecutionStatusFilterComponent); +ExecutionStatusFilter.displayName = 'ExecutionStatusFilter'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/build_rule_message.mock.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/filters/execution_status_filter/translations.ts similarity index 54% rename from x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/build_rule_message.mock.ts rename to x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/filters/execution_status_filter/translations.ts index f43142d1d0264..003942bd680d5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/build_rule_message.mock.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/filters/execution_status_filter/translations.ts @@ -5,11 +5,11 @@ * 2.0. */ -import { buildRuleMessageFactory } from '../rule_messages'; +import { i18n } from '@kbn/i18n'; -export const mockBuildRuleMessage = buildRuleMessageFactory({ - id: 'fake id', - ruleId: 'fake rule id', - index: 'fakeindex', - name: 'fake name', -}); +export const FILTER_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleMonitoring.executionStatusFilter.filterTitle', + { + defaultMessage: 'Status', + } +); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/filters/log_level_filter/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/filters/log_level_filter/index.tsx new file mode 100644 index 0000000000000..8aeeee71cd8de --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/filters/log_level_filter/index.tsx @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback } from 'react'; + +import type { LogLevel } from '../../../../../../../common/detection_engine/rule_monitoring'; +import { LOG_LEVELS } from '../../../../../../../common/detection_engine/rule_monitoring'; +import { LogLevelIndicator } from '../../indicators/log_level_indicator'; +import { MultiselectFilter } from '../multiselect_filter'; + +import * as i18n from './translations'; + +interface LogLevelFilterProps { + selectedItems: LogLevel[]; + onChange: (selectedItems: LogLevel[]) => void; +} + +const LogLevelFilterComponent: React.FC = ({ selectedItems, onChange }) => { + const renderItem = useCallback((item: LogLevel) => { + return ; + }, []); + + return ( + + dataTestSubj="logLevelFilter" + title={i18n.FILTER_TITLE} + items={LOG_LEVELS} + selectedItems={selectedItems} + onSelectionChange={onChange} + renderItem={renderItem} + /> + ); +}; + +export const LogLevelFilter = React.memo(LogLevelFilterComponent); +LogLevelFilter.displayName = 'LogLevelFilter'; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/filters/log_level_filter/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/filters/log_level_filter/translations.ts new file mode 100644 index 0000000000000..0c7f6c7c2f285 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/filters/log_level_filter/translations.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const FILTER_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleMonitoring.logLevelFilter.filterTitle', + { + defaultMessage: 'Log level', + } +); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/filters/multiselect_filter/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/filters/multiselect_filter/index.tsx new file mode 100644 index 0000000000000..c666c09561f08 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/filters/multiselect_filter/index.tsx @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useMemo } from 'react'; +import { noop } from 'lodash'; +import { EuiPopover, EuiFilterGroup, EuiFilterButton, EuiFilterSelectItem } from '@elastic/eui'; +import { useBoolState } from '../../../../../../common/hooks/use_bool_state'; + +export interface MultiselectFilterProps { + dataTestSubj?: string; + title: string; + items: T[]; + selectedItems: T[]; + onSelectionChange?: (selectedItems: T[]) => void; + renderItem?: (item: T) => React.ReactChild; + renderLabel?: (item: T) => string; +} + +const MultiselectFilterComponent = (props: MultiselectFilterProps) => { + const { dataTestSubj, title, items, selectedItems, onSelectionChange, renderItem, renderLabel } = + initializeProps(props); + + const [isPopoverOpen, , closePopover, togglePopover] = useBoolState(); + + const handleItemClick = useCallback( + (item: T) => { + const newSelectedItems = selectedItems.includes(item) + ? selectedItems.filter((i) => i !== item) + : [...selectedItems, item]; + onSelectionChange(newSelectedItems); + }, + [selectedItems, onSelectionChange] + ); + + const filterItemElements = useMemo(() => { + return items.map((item, index) => { + const itemLabel = renderLabel(item); + const itemElement = renderItem(item); + return ( + handleItemClick(item)} + > + {itemElement} + + ); + }); + }, [dataTestSubj, items, selectedItems, renderItem, renderLabel, handleItemClick]); + + return ( + + 0} + isSelected={isPopoverOpen} + onClick={togglePopover} + > + {title} + + } + isOpen={isPopoverOpen} + closePopover={closePopover} + panelPaddingSize="none" + repositionOnScroll + > + {filterItemElements} + + + ); +}; + +// We have to wrap it in a function and cast to original type because React.memo +// returns a component type which is not generic. +const enhanceMultiselectFilterComponent = () => { + const Component = React.memo(MultiselectFilterComponent); + Component.displayName = 'MultiselectFilter'; + return Component as typeof MultiselectFilterComponent; +}; + +export const MultiselectFilter = enhanceMultiselectFilterComponent(); + +const initializeProps = ( + props: MultiselectFilterProps +): Required> => { + const onSelectionChange: (selectedItems: T[]) => void = props.onSelectionChange ?? noop; + const renderLabel: (item: T) => string = props.renderLabel ?? String; + const renderItem: (item: T) => React.ReactChild = props.renderItem ?? renderLabel; + + return { + dataTestSubj: props.dataTestSubj ?? 'multiselectFilter', + title: props.title, + items: props.items, + selectedItems: props.selectedItems, + onSelectionChange, + renderLabel, + renderItem, + }; +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/indicators/event_type_indicator/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/indicators/event_type_indicator/index.tsx new file mode 100644 index 0000000000000..506fddad4559f --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/indicators/event_type_indicator/index.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiBadge } from '@elastic/eui'; +import type { RuleExecutionEventType } from '../../../../../../../common/detection_engine/rule_monitoring'; +import { getBadgeIcon, getBadgeText } from './utils'; + +interface EventTypeIndicatorProps { + type: RuleExecutionEventType; +} + +const EventTypeIndicatorComponent: React.FC = ({ type }) => { + const icon = getBadgeIcon(type); + const text = getBadgeText(type); + + return ( + + {text} + + ); +}; + +export const EventTypeIndicator = React.memo(EventTypeIndicatorComponent); +EventTypeIndicator.displayName = 'EventTypeIndicator'; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/indicators/event_type_indicator/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/indicators/event_type_indicator/translations.ts new file mode 100644 index 0000000000000..554e1c4f248ca --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/indicators/event_type_indicator/translations.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const TYPE_MESSAGE = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleMonitoring.eventTypeIndicator.messageText', + { + defaultMessage: 'Message', + } +); + +export const TYPE_STATUS_CHANGE = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleMonitoring.eventTypeIndicator.statusChangeText', + { + defaultMessage: 'Status', + } +); + +export const TYPE_EXECUTION_METRICS = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleMonitoring.eventTypeIndicator.executionMetricsText', + { + defaultMessage: 'Metrics', + } +); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/indicators/event_type_indicator/utils.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/indicators/event_type_indicator/utils.ts new file mode 100644 index 0000000000000..9fd383b759e11 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/indicators/event_type_indicator/utils.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { IconType } from '@elastic/eui'; +import { RuleExecutionEventType } from '../../../../../../../common/detection_engine/rule_monitoring'; +import { assertUnreachable } from '../../../../../../../common/utility_types'; + +import * as i18n from './translations'; + +export const getBadgeIcon = (type: RuleExecutionEventType): IconType => { + switch (type) { + case RuleExecutionEventType.message: + return 'console'; + case RuleExecutionEventType['status-change']: + return 'dot'; + case RuleExecutionEventType['execution-metrics']: + return 'gear'; + default: + return assertUnreachable(type, 'Unknown rule execution event type'); + } +}; + +export const getBadgeText = (type: RuleExecutionEventType): string => { + switch (type) { + case RuleExecutionEventType.message: + return i18n.TYPE_MESSAGE; + case RuleExecutionEventType['status-change']: + return i18n.TYPE_STATUS_CHANGE; + case RuleExecutionEventType['execution-metrics']: + return i18n.TYPE_EXECUTION_METRICS; + default: + return assertUnreachable(type, 'Unknown rule execution event type'); + } +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/indicators/execution_status_indicator/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/indicators/execution_status_indicator/index.tsx new file mode 100644 index 0000000000000..ead104c05f969 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/indicators/execution_status_indicator/index.tsx @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiHealth } from '@elastic/eui'; + +import type { RuleExecutionStatus } from '../../../../../../../common/detection_engine/rule_monitoring'; +import { getEmptyTagValue } from '../../../../../../common/components/empty_value'; +import { RuleStatusBadge } from '../../../../../../detections/components/rules/rule_execution_status'; +import { + getCapitalizedStatusText, + getStatusColor, +} from '../../../../../../detections/components/rules/rule_execution_status/utils'; + +const EMPTY_STATUS_TEXT = getEmptyTagValue(); + +interface ExecutionStatusIndicatorProps { + status?: RuleExecutionStatus | null | undefined; + showTooltip?: boolean; +} + +const ExecutionStatusIndicatorComponent: React.FC = ({ + status, + showTooltip = false, +}) => { + const statusText = getCapitalizedStatusText(status) ?? EMPTY_STATUS_TEXT; + const statusColor = getStatusColor(status); + + return showTooltip ? ( + + ) : ( + {statusText} + ); +}; + +export const ExecutionStatusIndicator = React.memo(ExecutionStatusIndicatorComponent); +ExecutionStatusIndicator.displayName = 'ExecutionStatusIndicator'; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/indicators/log_level_indicator/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/indicators/log_level_indicator/index.tsx new file mode 100644 index 0000000000000..1e825651c9e1a --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/indicators/log_level_indicator/index.tsx @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiBadge } from '@elastic/eui'; +import type { LogLevel } from '../../../../../../../common/detection_engine/rule_monitoring'; +import { getBadgeColor, getBadgeText } from './utils'; + +interface LogLevelIndicatorProps { + logLevel: LogLevel; +} + +const LogLevelIndicatorComponent: React.FC = ({ logLevel }) => { + const color = getBadgeColor(logLevel); + const text = getBadgeText(logLevel); + + return {text}; +}; + +export const LogLevelIndicator = React.memo(LogLevelIndicatorComponent); +LogLevelIndicator.displayName = 'LogLevelIndicator'; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/indicators/log_level_indicator/utils.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/indicators/log_level_indicator/utils.ts new file mode 100644 index 0000000000000..757d66d99b2a4 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/indicators/log_level_indicator/utils.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { upperCase } from 'lodash'; +import type { IconColor } from '@elastic/eui'; +import { LogLevel } from '../../../../../../../common/detection_engine/rule_monitoring'; +import { assertUnreachable } from '../../../../../../../common/utility_types'; + +export const getBadgeColor = (logLevel: LogLevel): IconColor => { + switch (logLevel) { + case LogLevel.trace: + return 'hollow'; + case LogLevel.debug: + return 'hollow'; + case LogLevel.info: + return 'default'; + case LogLevel.warn: + return 'warning'; + case LogLevel.error: + return 'danger'; + default: + return assertUnreachable(logLevel, 'Unknown log level'); + } +}; + +export const getBadgeText = (logLevel: LogLevel): string => { + return upperCase(logLevel); +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/tables/use_expandable_rows.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/tables/use_expandable_rows.tsx new file mode 100644 index 0000000000000..099359e30a467 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/tables/use_expandable_rows.tsx @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type React from 'react'; +import { useCallback, useMemo, useState } from 'react'; + +type TableItem = Record; +type TableItemId = string; +type TableItemRowMap = Record; + +interface UseExpandableRowsArgs { + getItemId: (item: T) => TableItemId; + renderItem: (item: T) => React.ReactChild; +} + +export const useExpandableRows = (args: UseExpandableRowsArgs) => { + const { getItemId, renderItem } = args; + + const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState({}); + + const toggleRowExpanded = useCallback( + (item: T) => { + const itemIdToExpandedRowMapValues = { ...itemIdToExpandedRowMap }; + const itemId = getItemId(item); + + if (itemIdToExpandedRowMapValues[itemId]) { + delete itemIdToExpandedRowMapValues[itemId]; + } else { + itemIdToExpandedRowMapValues[itemId] = renderItem(item); + } + + setItemIdToExpandedRowMap(itemIdToExpandedRowMapValues); + }, + [itemIdToExpandedRowMap, getItemId, renderItem] + ); + + const isRowExpanded = useCallback( + (item: T): boolean => { + const itemId = getItemId(item); + return itemIdToExpandedRowMap[itemId] != null; + }, + [itemIdToExpandedRowMap, getItemId] + ); + + return useMemo(() => { + return { + itemIdToExpandedRowMap, + getItemId, + toggleRowExpanded, + isRowExpanded, + }; + }, [itemIdToExpandedRowMap, getItemId, toggleRowExpanded, isRowExpanded]); +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/tables/use_pagination.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/tables/use_pagination.ts new file mode 100644 index 0000000000000..d29edc1550128 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/tables/use_pagination.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback, useMemo, useState } from 'react'; +import type { CriteriaWithPagination } from '@elastic/eui'; + +const DEFAULT_PAGE_SIZE_OPTIONS = [10, 20, 50]; +const DEFAULT_PAGE_NUMBER = 1; + +interface UsePaginationArgs { + pageSizeOptions?: number[]; + pageSizeDefault?: number; + pageNumberDefault?: number; +} + +type TableItem = Record; + +export const usePagination = (args: UsePaginationArgs) => { + const pageSizeOptions = args.pageSizeOptions ?? DEFAULT_PAGE_SIZE_OPTIONS; + const pageSizeDefault = args.pageSizeDefault ?? pageSizeOptions[0]; + const pageNumberDefault = args.pageNumberDefault ?? DEFAULT_PAGE_NUMBER; + + const [pageSize, setPageSize] = useState(pageSizeDefault); + const [pageNumber, setPageNumber] = useState(pageNumberDefault); + const [totalItemCount, setTotalItemCount] = useState(0); + + const state = useMemo(() => { + return { + pageSizeOptions, + pageSize, + pageNumber, + pageIndex: pageNumber - 1, + totalItemCount, + }; + }, [pageSizeOptions, pageSize, pageNumber, totalItemCount]); + + const update = useCallback( + (criteria: CriteriaWithPagination): void => { + setPageNumber(criteria.page.index + 1); + setPageSize(criteria.page.size); + }, + [setPageNumber, setPageSize] + ); + + const updateTotalItemCount = useCallback( + (count: number | null | undefined): void => { + setTotalItemCount(count ?? 0); + }, + [setTotalItemCount] + ); + + return useMemo( + () => ({ state, update, updateTotalItemCount }), + [state, update, updateTotalItemCount] + ); +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/tables/use_sorting.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/tables/use_sorting.ts new file mode 100644 index 0000000000000..45a6723fe5a57 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/tables/use_sorting.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback, useMemo, useState } from 'react'; +import type { CriteriaWithPagination } from '@elastic/eui'; +import type { SortOrder } from '../../../../../../common/detection_engine/schemas/common'; + +type TableItem = Record; + +export const useSorting = (defaultField: keyof T, defaultOrder: SortOrder) => { + const [sortField, setSortField] = useState(defaultField); + const [sortOrder, setSortOrder] = useState(defaultOrder); + + const state = useMemo(() => { + return { + sort: { + field: sortField, + direction: sortOrder, + }, + }; + }, [sortField, sortOrder]); + + const update = useCallback( + (criteria: CriteriaWithPagination): void => { + if (criteria.sort) { + setSortField(criteria.sort.field); + setSortOrder(criteria.sort.direction); + } + }, + [setSortField, setSortOrder] + ); + + return useMemo(() => ({ state, update }), [state, update]); +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/text/text_block.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/text/text_block.tsx new file mode 100644 index 0000000000000..d57619c3589e5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/text/text_block.tsx @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiCodeBlock } from '@elastic/eui'; + +const DEFAULT_OVERFLOW_HEIGHT = 320; + +interface TextBlockProps { + text: string | null | undefined; +} + +const TextBlockComponent: React.FC = ({ text }) => { + return ( + + {text ?? ''} + + ); +}; + +export const TextBlock = React.memo(TextBlockComponent); +TextBlock.displayName = 'TextBlock'; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/text/truncated_text.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/text/truncated_text.tsx new file mode 100644 index 0000000000000..9b9256150a442 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/basic/text/truncated_text.tsx @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +interface TruncatedTextProps { + text: string | null | undefined; +} + +const TruncatedTextComponent: React.FC = ({ text }) => { + return text != null ? {text} : null; +}; + +export const TruncatedText = React.memo(TruncatedTextComponent); +TruncatedText.displayName = 'TruncatedText'; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_events_table/execution_events_table.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_events_table/execution_events_table.tsx new file mode 100644 index 0000000000000..852ca23219812 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_events_table/execution_events_table.tsx @@ -0,0 +1,117 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useEffect, useMemo } from 'react'; +import type { CriteriaWithPagination } from '@elastic/eui'; +import { EuiBasicTable, EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; + +import type { RuleExecutionEvent } from '../../../../../common/detection_engine/rule_monitoring'; + +import { HeaderSection } from '../../../../common/components/header_section'; +import { EventTypeFilter } from '../basic/filters/event_type_filter'; +import { LogLevelFilter } from '../basic/filters/log_level_filter'; +import { ExecutionEventsTableRowDetails } from './execution_events_table_row_details'; + +import { useFilters } from './use_filters'; +import { useSorting } from '../basic/tables/use_sorting'; +import { usePagination } from '../basic/tables/use_pagination'; +import { useColumns } from './use_columns'; +import { useExpandableRows } from '../basic/tables/use_expandable_rows'; +import { useExecutionEvents } from './use_execution_events'; + +import * as i18n from './translations'; + +const PAGE_SIZE_OPTIONS = [10, 20, 50, 100, 200]; + +interface ExecutionEventsTableProps { + ruleId: string; +} + +const ExecutionEventsTableComponent: React.FC = ({ ruleId }) => { + const getItemId = useCallback((item: RuleExecutionEvent): string => { + return `${item.timestamp} ${item.sequence}`; + }, []); + + const renderExpandedItem = useCallback((item: RuleExecutionEvent) => { + return ; + }, []); + + const rows = useExpandableRows({ + getItemId, + renderItem: renderExpandedItem, + }); + + const columns = useColumns({ + toggleRowExpanded: rows.toggleRowExpanded, + isRowExpanded: rows.isRowExpanded, + }); + + const filters = useFilters(); + const sorting = useSorting('timestamp', 'desc'); + const pagination = usePagination({ pageSizeOptions: PAGE_SIZE_OPTIONS }); + + const executionEvents = useExecutionEvents({ + ruleId, + eventTypes: filters.state.eventTypes, + logLevels: filters.state.logLevels, + sortOrder: sorting.state.sort.direction, + page: pagination.state.pageNumber, + perPage: pagination.state.pageSize, + }); + + // Each time execution events are fetched + useEffect(() => { + // We need to update total item count for the pagination to work properly + pagination.updateTotalItemCount(executionEvents.data?.pagination.total); + }, [executionEvents, pagination]); + + const items = useMemo(() => executionEvents.data?.events ?? [], [executionEvents.data]); + + const handleTableChange = useCallback( + (criteria: CriteriaWithPagination): void => { + sorting.update(criteria); + pagination.update(criteria); + }, + [sorting, pagination] + ); + + return ( + + {/* Filter bar */} + + + + + + + + + + + + + {/* Table with items */} + + + ); +}; + +export const ExecutionEventsTable = React.memo(ExecutionEventsTableComponent); +ExecutionEventsTable.displayName = 'RuleExecutionEventsTable'; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_events_table/execution_events_table_row_details.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_events_table/execution_events_table_row_details.tsx new file mode 100644 index 0000000000000..27c6e0623c2f1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_events_table/execution_events_table_row_details.tsx @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiDescriptionList } from '@elastic/eui'; +import type { RuleExecutionEvent } from '../../../../../common/detection_engine/rule_monitoring'; +import { TextBlock } from '../basic/text/text_block'; + +import * as i18n from './translations'; + +interface ExecutionEventsTableRowDetailsProps { + item: RuleExecutionEvent; +} + +const ExecutionEventsTableRowDetailsComponent: React.FC = ({ + item, +}) => { + return ( + , + }, + { + title: i18n.ROW_DETAILS_JSON, + description: , + }, + ]} + /> + ); +}; + +export const ExecutionEventsTableRowDetails = React.memo(ExecutionEventsTableRowDetailsComponent); +ExecutionEventsTableRowDetails.displayName = 'ExecutionEventsTableRowDetails'; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_events_table/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_events_table/translations.ts new file mode 100644 index 0000000000000..0cc671d7b732c --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_events_table/translations.ts @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const TABLE_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleMonitoring.executionEventsTable.tableTitle', + { + defaultMessage: 'Execution log', + } +); + +export const TABLE_SUBTITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleMonitoring.executionEventsTable.tableSubtitle', + { + defaultMessage: 'A detailed log of rule execution events', + } +); + +export const COLUMN_TIMESTAMP = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleMonitoring.executionEventsTable.timestampColumn', + { + defaultMessage: 'Timestamp', + } +); + +export const COLUMN_LOG_LEVEL = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleMonitoring.executionEventsTable.logLevelColumn', + { + defaultMessage: 'Level', + } +); + +export const COLUMN_EVENT_TYPE = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleMonitoring.executionEventsTable.eventTypeColumn', + { + defaultMessage: 'Type', + } +); + +export const COLUMN_MESSAGE = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleMonitoring.executionEventsTable.messageColumn', + { + defaultMessage: 'Message', + } +); + +export const FETCH_ERROR = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleMonitoring.executionEventsTable.fetchErrorDescription', + { + defaultMessage: 'Failed to fetch rule execution events', + } +); + +export const ROW_DETAILS_MESSAGE = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleMonitoring.executionEventsTable.rowDetails.messageTitle', + { + defaultMessage: 'Message', + } +); + +export const ROW_DETAILS_JSON = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleMonitoring.executionEventsTable.rowDetails.jsonTitle', + { + defaultMessage: 'Full JSON', + } +); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_events_table/use_columns.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_events_table/use_columns.tsx new file mode 100644 index 0000000000000..a3d5d95431e0f --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_events_table/use_columns.tsx @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo } from 'react'; +import type { EuiBasicTableColumn } from '@elastic/eui'; +import { EuiButtonIcon, EuiScreenReaderOnly, RIGHT_ALIGNMENT } from '@elastic/eui'; + +import type { + LogLevel, + RuleExecutionEvent, + RuleExecutionEventType, +} from '../../../../../common/detection_engine/rule_monitoring'; + +import { FormattedDate } from '../../../../common/components/formatted_date'; +import { EventTypeIndicator } from '../basic/indicators/event_type_indicator'; +import { LogLevelIndicator } from '../basic/indicators/log_level_indicator'; +import { TruncatedText } from '../basic/text/truncated_text'; + +import * as i18n from './translations'; + +type TableColumn = EuiBasicTableColumn; + +interface UseColumnsArgs { + toggleRowExpanded: (item: RuleExecutionEvent) => void; + isRowExpanded: (item: RuleExecutionEvent) => boolean; +} + +export const useColumns = (args: UseColumnsArgs): TableColumn[] => { + const { toggleRowExpanded, isRowExpanded } = args; + + return useMemo(() => { + return [ + timestampColumn, + logLevelColumn, + eventTypeColumn, + messageColumn, + expanderColumn({ toggleRowExpanded, isRowExpanded }), + ]; + }, [toggleRowExpanded, isRowExpanded]); +}; + +const timestampColumn: TableColumn = { + field: 'timestamp', + name: i18n.COLUMN_TIMESTAMP, + render: (value: string) => , + sortable: true, + truncateText: false, + width: '20%', +}; + +const logLevelColumn: TableColumn = { + field: 'level', + name: i18n.COLUMN_LOG_LEVEL, + render: (value: LogLevel) => , + sortable: false, + truncateText: false, + width: '8%', +}; + +const eventTypeColumn: TableColumn = { + field: 'type', + name: i18n.COLUMN_EVENT_TYPE, + render: (value: RuleExecutionEventType) => , + sortable: false, + truncateText: false, + width: '8%', +}; + +const messageColumn: TableColumn = { + field: 'message', + name: i18n.COLUMN_MESSAGE, + render: (value: string) => , + sortable: false, + truncateText: true, + width: '64%', +}; + +const expanderColumn = ({ toggleRowExpanded, isRowExpanded }: UseColumnsArgs): TableColumn => { + return { + align: RIGHT_ALIGNMENT, + width: '40px', + isExpander: true, + name: ( + + {'Expand rows'} + + ), + render: (item: RuleExecutionEvent) => ( + toggleRowExpanded(item)} + aria-label={isRowExpanded(item) ? 'Collapse' : 'Expand'} + iconType={isRowExpanded(item) ? 'arrowUp' : 'arrowDown'} + /> + ), + }; +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_events_table/use_execution_events.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_events_table/use_execution_events.test.tsx new file mode 100644 index 0000000000000..9a62fd58a0fbc --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_events_table/use_execution_events.test.tsx @@ -0,0 +1,127 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { QueryClient, QueryClientProvider } from 'react-query'; +import { renderHook, cleanup } from '@testing-library/react-hooks'; + +import { + LogLevel, + RuleExecutionEventType, +} from '../../../../../common/detection_engine/rule_monitoring'; + +import { useExecutionEvents } from './use_execution_events'; +import { useToasts } from '../../../../common/lib/kibana'; +import { api } from '../../api'; + +jest.mock('../../../../common/lib/kibana'); +jest.mock('../../api'); + +const SOME_RULE_ID = 'some-rule-id'; + +describe('useExecutionEvents', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(async () => { + cleanup(); + }); + + const createReactQueryWrapper = () => { + const queryClient = new QueryClient({ + defaultOptions: { + queries: { + // Turn retries off, otherwise we won't be able to test errors + retry: false, + }, + }, + }); + const wrapper: React.FC = ({ children }) => ( + {children} + ); + return wrapper; + }; + + const render = () => + renderHook(() => useExecutionEvents({ ruleId: SOME_RULE_ID }), { + wrapper: createReactQueryWrapper(), + }); + + it('calls the API via fetchRuleExecutionEvents', async () => { + const fetchRuleExecutionEvents = jest.spyOn(api, 'fetchRuleExecutionEvents'); + + const { waitForNextUpdate } = render(); + + await waitForNextUpdate(); + + expect(fetchRuleExecutionEvents).toHaveBeenCalledTimes(1); + expect(fetchRuleExecutionEvents).toHaveBeenLastCalledWith( + expect.objectContaining({ ruleId: SOME_RULE_ID }) + ); + }); + + it('fetches data from the API', async () => { + const { result, waitForNextUpdate } = render(); + + // It starts from a loading state + expect(result.current.isLoading).toEqual(true); + expect(result.current.isSuccess).toEqual(false); + expect(result.current.isError).toEqual(false); + + // When fetchRuleExecutionEvents returns + await waitForNextUpdate(); + + // It switches to a success state + expect(result.current.isLoading).toEqual(false); + expect(result.current.isSuccess).toEqual(true); + expect(result.current.isError).toEqual(false); + expect(result.current.data).toEqual({ + events: [ + { + timestamp: '2021-12-29T10:42:59.996Z', + sequence: 0, + level: LogLevel.info, + type: RuleExecutionEventType['status-change'], + message: 'Rule changed status to "succeeded". Rule execution completed without errors', + }, + ], + pagination: { + page: 1, + per_page: 20, + total: 1, + }, + }); + }); + + it('handles exceptions from the API', async () => { + const exception = new Error('Boom!'); + jest.spyOn(api, 'fetchRuleExecutionEvents').mockRejectedValue(exception); + + const { result, waitForNextUpdate } = render(); + + // It starts from a loading state + expect(result.current.isLoading).toEqual(true); + expect(result.current.isSuccess).toEqual(false); + expect(result.current.isError).toEqual(false); + + // When fetchRuleExecutionEvents throws + await waitForNextUpdate(); + + // It switches to an error state + expect(result.current.isLoading).toEqual(false); + expect(result.current.isSuccess).toEqual(false); + expect(result.current.isError).toEqual(true); + expect(result.current.error).toEqual(exception); + + // And shows a toast with the caught exception + expect(useToasts().addError).toHaveBeenCalledTimes(1); + expect(useToasts().addError).toHaveBeenCalledWith(exception, { + title: 'Failed to fetch rule execution events', + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_events_table/use_execution_events.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_events_table/use_execution_events.ts new file mode 100644 index 0000000000000..4a37f55dc1d6a --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_events_table/use_execution_events.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useQuery } from 'react-query'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; + +import type { GetRuleExecutionEventsResponse } from '../../../../../common/detection_engine/rule_monitoring'; +import type { FetchRuleExecutionEventsArgs } from '../../api'; +import { api } from '../../api'; + +import * as i18n from './translations'; + +export type UseExecutionEventsArgs = Omit; + +export const useExecutionEvents = (args: UseExecutionEventsArgs) => { + const { addError } = useAppToasts(); + + return useQuery( + ['detectionEngine', 'ruleMonitoring', 'executionEvents', args], + ({ signal }) => { + return api.fetchRuleExecutionEvents({ ...args, signal }); + }, + { + keepPreviousData: true, + refetchInterval: 20000, + onError: (e) => { + addError(e, { title: i18n.FETCH_ERROR }); + }, + } + ); +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_events_table/use_filters.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_events_table/use_filters.ts new file mode 100644 index 0000000000000..96c78328b683b --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_events_table/use_filters.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useMemo, useState } from 'react'; + +import type { + LogLevel, + RuleExecutionEventType, +} from '../../../../../common/detection_engine/rule_monitoring'; + +export const useFilters = () => { + const [logLevels, setLogLevels] = useState([]); + const [eventTypes, setEventTypes] = useState([]); + + const state = useMemo(() => ({ logLevels, eventTypes }), [logLevels, eventTypes]); + + return useMemo( + () => ({ state, setLogLevels, setEventTypes }), + [state, setLogLevels, setEventTypes] + ); +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_results_table/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_results_table/translations.ts new file mode 100644 index 0000000000000..7925059c2b6e6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_results_table/translations.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const FETCH_ERROR = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleMonitoring.executionResultsTable.fetchErrorDescription', + { + defaultMessage: 'Failed to fetch rule execution results', + } +); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_execution_events.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_results_table/use_execution_results.test.tsx similarity index 86% rename from x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_execution_events.test.tsx rename to x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_results_table/use_execution_results.test.tsx index 5fba7dd7a2ed2..63565f7cfa1b5 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_execution_events.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_results_table/use_execution_results.test.tsx @@ -5,21 +5,20 @@ * 2.0. */ -jest.mock('./api'); -jest.mock('../../../../common/lib/kibana'); - import React from 'react'; import { QueryClient, QueryClientProvider } from 'react-query'; import { renderHook, cleanup } from '@testing-library/react-hooks'; -import { useRuleExecutionEvents } from './use_rule_execution_events'; - -import * as api from './api'; +import { useExecutionResults } from './use_execution_results'; import { useToasts } from '../../../../common/lib/kibana'; +import { api } from '../../api'; + +jest.mock('../../../../common/lib/kibana'); +jest.mock('../../api'); const SOME_RULE_ID = 'some-rule-id'; -describe('useRuleExecutionEvents', () => { +describe('useExecutionResults', () => { beforeEach(() => { jest.clearAllMocks(); }); @@ -46,7 +45,7 @@ describe('useRuleExecutionEvents', () => { const render = () => renderHook( () => - useRuleExecutionEvents({ + useExecutionResults({ ruleId: SOME_RULE_ID, start: 'now-30', end: 'now', @@ -58,15 +57,15 @@ describe('useRuleExecutionEvents', () => { } ); - it('calls the API via fetchRuleExecutionEvents', async () => { - const fetchRuleExecutionEvents = jest.spyOn(api, 'fetchRuleExecutionEvents'); + it('calls the API via fetchRuleExecutionResults', async () => { + const fetchRuleExecutionResults = jest.spyOn(api, 'fetchRuleExecutionResults'); const { waitForNextUpdate } = render(); await waitForNextUpdate(); - expect(fetchRuleExecutionEvents).toHaveBeenCalledTimes(1); - expect(fetchRuleExecutionEvents).toHaveBeenLastCalledWith( + expect(fetchRuleExecutionResults).toHaveBeenCalledTimes(1); + expect(fetchRuleExecutionResults).toHaveBeenLastCalledWith( expect.objectContaining({ ruleId: SOME_RULE_ID }) ); }); @@ -118,7 +117,7 @@ describe('useRuleExecutionEvents', () => { it('handles exceptions from the API', async () => { const exception = new Error('Boom!'); - jest.spyOn(api, 'fetchRuleExecutionEvents').mockRejectedValue(exception); + jest.spyOn(api, 'fetchRuleExecutionResults').mockRejectedValue(exception); const { result, waitForNextUpdate } = render(); @@ -139,7 +138,7 @@ describe('useRuleExecutionEvents', () => { // And shows a toast with the caught exception expect(useToasts().addError).toHaveBeenCalledTimes(1); expect(useToasts().addError).toHaveBeenCalledWith(exception, { - title: 'Failed to fetch rule execution events', + title: 'Failed to fetch rule execution results', }); }); }); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_results_table/use_execution_results.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_results_table/use_execution_results.tsx new file mode 100644 index 0000000000000..a07289969af12 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/components/execution_results_table/use_execution_results.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useQuery } from 'react-query'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; + +import type { GetRuleExecutionResultsResponse } from '../../../../../common/detection_engine/rule_monitoring'; +import type { FetchRuleExecutionResultsArgs } from '../../api'; +import { api } from '../../api'; + +import * as i18n from './translations'; + +export type UseExecutionResultsArgs = Omit; + +export const useExecutionResults = (args: UseExecutionResultsArgs) => { + const { addError } = useAppToasts(); + + return useQuery( + ['detectionEngine', 'ruleMonitoring', 'executionResults', args], + ({ signal }) => { + return api.fetchRuleExecutionResults({ ...args, signal }); + }, + { + keepPreviousData: true, + onError: (e) => { + addError(e, { title: i18n.FETCH_ERROR }); + }, + } + ); +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/index.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/index.ts new file mode 100644 index 0000000000000..b778a4b1034d6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './api'; + +export * from './components/basic/filters/execution_status_filter'; +export * from './components/basic/indicators/execution_status_indicator'; +export * from './components/execution_events_table/execution_events_table'; +export * from './components/execution_results_table/use_execution_results'; + +export * from './logic/execution_settings/use_execution_settings'; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/logic/execution_settings/use_execution_settings.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/logic/execution_settings/use_execution_settings.ts new file mode 100644 index 0000000000000..cc7e7abb5cc89 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/logic/execution_settings/use_execution_settings.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useMemo } from 'react'; + +import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; +import { useUiSetting$ } from '../../../../common/lib/kibana'; + +import { + EXTENDED_RULE_EXECUTION_LOGGING_ENABLED_SETTING, + EXTENDED_RULE_EXECUTION_LOGGING_MIN_LEVEL_SETTING, +} from '../../../../../common/constants'; +import type { RuleExecutionSettings } from '../../../../../common/detection_engine/rule_monitoring'; +import { LogLevelSetting } from '../../../../../common/detection_engine/rule_monitoring'; + +export const useRuleExecutionSettings = (): RuleExecutionSettings => { + const featureFlagEnabled = useIsExperimentalFeatureEnabled('extendedRuleExecutionLoggingEnabled'); + + const advancedSettingEnabled = useAdvancedSettingSafely( + EXTENDED_RULE_EXECUTION_LOGGING_ENABLED_SETTING, + false + ); + const advancedSettingMinLevel = useAdvancedSettingSafely( + EXTENDED_RULE_EXECUTION_LOGGING_MIN_LEVEL_SETTING, + LogLevelSetting.off + ); + + return useMemo(() => { + return { + extendedLogging: { + isEnabled: featureFlagEnabled && advancedSettingEnabled, + minLevel: advancedSettingMinLevel, + }, + }; + }, [featureFlagEnabled, advancedSettingEnabled, advancedSettingMinLevel]); +}; + +const useAdvancedSettingSafely = (key: string, defaultValue: T): T => { + try { + const [value] = useUiSetting$(key); + return value; + } catch (e) { + // It throws when the setting is not registered (when featureFlagEnabled === false). + return defaultValue; + } +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/mocks.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/mocks.ts new file mode 100644 index 0000000000000..e944c5663c33a --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_monitoring/mocks.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './api/__mocks__'; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/data_view_selector/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/data_view_selector/index.tsx index ec19332ee3d37..d319d3f24f547 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/data_view_selector/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/data_view_selector/index.tsx @@ -11,26 +11,17 @@ import type { EuiComboBoxOptionOption } from '@elastic/eui'; import { EuiCallOut, EuiComboBox, EuiFormRow, EuiSpacer } from '@elastic/eui'; import type { DataViewListItem } from '@kbn/data-views-plugin/common'; -import type { DataViewBase } from '@kbn/es-query'; import type { FieldHook } from '../../../../shared_imports'; import { getFieldValidityAndErrorMessage } from '../../../../shared_imports'; import * as i18n from './translations'; -import { useKibana } from '../../../../common/lib/kibana'; import type { DefineStepRule } from '../../../pages/detection_engine/rules/types'; interface DataViewSelectorProps { - kibanaDataViews: { [x: string]: DataViewListItem }; + kibanaDataViews: Record; field: FieldHook; - setIndexPattern: (indexPattern: DataViewBase) => void; } -export const DataViewSelector = ({ - kibanaDataViews, - field, - setIndexPattern, -}: DataViewSelectorProps) => { - const { data } = useKibana().services; - +export const DataViewSelector = ({ kibanaDataViews, field }: DataViewSelectorProps) => { let isInvalid; let errorMessage; let dataViewId: string | null | undefined; @@ -62,7 +53,15 @@ export const DataViewSelector = ({ : [] ); - const [selectedDataView, setSelectedDataView] = useState(); + useEffect(() => { + if (!selectedDataViewNotFound && dataViewId) { + setSelectedOption([ + { id: kibanaDataViews[dataViewId].id, label: kibanaDataViews[dataViewId].title }, + ]); + } else { + setSelectedOption([]); + } + }, [dataViewId, field, kibanaDataViews, selectedDataViewNotFound]); // TODO: optimize this, pass down array of data view ids // at the same time we grab the data views in the top level form component @@ -75,17 +74,6 @@ export const DataViewSelector = ({ : []; }, [kibanaDataViewsDefined, kibanaDataViews]); - useEffect(() => { - const fetchSingleDataView = async () => { - if (selectedDataView != null) { - const dv = await data.dataViews.get(selectedDataView.id); - setIndexPattern(dv); - } - }; - - fetchSingleDataView(); - }, [data.dataViews, selectedDataView, setIndexPattern]); - const onChangeDataViews = (options: Array>) => { const selectedDataViewOption = options; @@ -96,10 +84,9 @@ export const DataViewSelector = ({ selectedDataViewOption.length > 0 && selectedDataViewOption[0].id != null ) { - setSelectedDataView(kibanaDataViews[selectedDataViewOption[0].id]); - field?.setValue(selectedDataViewOption[0].id); + const selectedDataViewId = selectedDataViewOption[0].id; + field?.setValue(selectedDataViewId); } else { - setSelectedDataView(undefined); field?.setValue(undefined); } }; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/eql_query_bar/validators.ts b/x-pack/plugins/security_solution/public/detections/components/rules/eql_query_bar/validators.ts index 8ca8355bc6502..ae6e84f187109 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/eql_query_bar/validators.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/eql_query_bar/validators.ts @@ -11,6 +11,7 @@ import type { FieldHook, ValidationError, ValidationFunc } from '../../../../sha import { isEqlRule } from '../../../../../common/detection_engine/utils'; import { KibanaServices } from '../../../../common/lib/kibana'; import type { DefineStepRule } from '../../../pages/detection_engine/rules/types'; +import { DataSourceType } from '../../../pages/detection_engine/rules/types'; import { validateEql } from '../../../../common/hooks/eql/api'; import type { FieldValueQueryBar } from '../query_bar'; import * as i18n from './translations'; @@ -69,7 +70,11 @@ export const eqlValidator = async ( const { data } = KibanaServices.get(); let dataViewTitle = index?.join(); let runtimeMappings = {}; - if (dataViewId != null) { + if ( + dataViewId != null && + dataViewId !== '' && + formData.dataSourceType === DataSourceType.DataView + ) { const dataView = await data.dataViews.get(dataViewId); dataViewTitle = dataView.title; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status.tsx index 3c61a6a944137..f4284381f7a4d 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { EuiFlexItem, EuiHealth, EuiText } from '@elastic/eui'; -import type { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common'; +import type { RuleExecutionStatus } from '../../../../../common/detection_engine/rule_monitoring'; import { FormattedDate } from '../../../../common/components/formatted_date'; import { getEmptyTagValue } from '../../../../common/components/empty_value'; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status_badge.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status_badge.test.tsx index 52a6c723fcbd6..4dd6edcade20f 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status_badge.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status_badge.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; -import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common'; +import { RuleExecutionStatus } from '../../../../../common/detection_engine/rule_monitoring'; import { RuleStatusBadge } from './rule_status_badge'; describe('RuleStatusBadge', () => { diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status_badge.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status_badge.tsx index 6a37d275e3870..bb3f7360892e6 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status_badge.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status_badge.tsx @@ -11,7 +11,7 @@ import { getEmptyTagValue } from '../../../../common/components/empty_value'; import { HealthTruncateText } from '../../../../common/components/health_truncate_text'; import { getCapitalizedStatusText, getStatusColor } from './utils'; -import type { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common'; +import type { RuleExecutionStatus } from '../../../../../common/detection_engine/rule_monitoring'; interface RuleStatusBadgeProps { status: RuleExecutionStatus | null | undefined; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status_failed_callout.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status_failed_callout.test.tsx index 3e4f1f3c43e57..ec6a029ad27bc 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status_failed_callout.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status_failed_callout.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { render } from '@testing-library/react'; -import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common'; +import { RuleExecutionStatus } from '../../../../../common/detection_engine/rule_monitoring'; import { RuleStatusFailedCallOut } from './rule_status_failed_callout'; jest.mock('../../../../common/lib/kibana'); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status_failed_callout.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status_failed_callout.tsx index 5f10150383369..073d88d19181b 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status_failed_callout.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/rule_status_failed_callout.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { EuiCallOut, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { FormattedDate } from '../../../../common/components/formatted_date'; -import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common'; +import { RuleExecutionStatus } from '../../../../../common/detection_engine/rule_monitoring'; import * as i18n from './translations'; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/utils.ts b/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/utils.ts index dc074b157e3d2..c6d5e1ce5af25 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/utils.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_execution_status/utils.ts @@ -8,7 +8,7 @@ import type { IconColor } from '@elastic/eui'; import { capitalize } from 'lodash'; import { assertUnreachable } from '../../../../../common/utility_types'; -import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common'; +import { RuleExecutionStatus } from '../../../../../common/detection_engine/rule_monitoring'; export const getStatusText = (value: RuleExecutionStatus | null | undefined): string | null => { if (value == null) { diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx index 1b92ac0667a7c..6391d785ef39d 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx @@ -22,6 +22,7 @@ import type { RuleStep, DefineStepRule, } from '../../../pages/detection_engine/rules/types'; +import { DataSourceType } from '../../../pages/detection_engine/rules/types'; import { fillEmptySeverityMappings } from '../../../pages/detection_engine/rules/helpers'; import { TestProviders } from '../../../../common/mock'; @@ -54,6 +55,7 @@ export const stepDefineStepMLRule: DefineStepRule = { threatMapping: [], timeline: { id: null, title: null }, eqlOptions: {}, + dataSourceType: DataSourceType.IndexPatterns, newTermsFields: ['host.ip'], historyWindowSize: '7d', }; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx index c63442939905b..de2ad79d44cb5 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx @@ -36,11 +36,14 @@ import { isMlRule } from '../../../../../common/machine_learning/helpers'; import { hasMlAdminPermissions } from '../../../../../common/machine_learning/has_ml_admin_permissions'; import { hasMlLicense } from '../../../../../common/machine_learning/has_ml_license'; import { useMlCapabilities } from '../../../../common/components/ml/hooks/use_ml_capabilities'; -import { useUiSetting$ } from '../../../../common/lib/kibana'; +import { useUiSetting$, useKibana } from '../../../../common/lib/kibana'; import type { EqlOptionsSelected, FieldsEqlOptions } from '../../../../../common/search_strategy'; -import { filterRuleFieldsForType } from '../../../pages/detection_engine/rules/create/helpers'; +import { + filterRuleFieldsForType, + getStepDataDataSource, +} from '../../../pages/detection_engine/rules/create/helpers'; import type { DefineStepRule, RuleStepProps } from '../../../pages/detection_engine/rules/types'; -import { RuleStep } from '../../../pages/detection_engine/rules/types'; +import { RuleStep, DataSourceType } from '../../../pages/detection_engine/rules/types'; import { StepRuleDescription } from '../description_step'; import { QueryBarDefineRule } from '../query_bar'; import { SelectRuleType } from '../select_rule_type'; @@ -78,11 +81,11 @@ import { NewTermsFields } from '../new_terms_fields'; import { ScheduleItem } from '../schedule_item_form'; import { DocLink } from '../../../../common/components/links_to_docs/doc_link'; -const DATA_VIEW_SELECT_ID = 'dataView'; -const INDEX_PATTERN_SELECT_ID = 'indexPatterns'; - const CommonUseField = getUseField({ component: Field }); +const StyledVisibleContainer = styled.div<{ isVisible: boolean }>` + display: ${(props) => (props.isVisible ? 'block' : 'none')}; +`; interface StepDefineRuleProps extends RuleStepProps { defaultValues?: DefineStepRule; } @@ -119,6 +122,7 @@ export const stepDefineDefaultValue: DefineStepRule = { title: DEFAULT_TIMELINE_TITLE, }, eqlOptions: {}, + dataSourceType: DataSourceType.IndexPatterns, newTermsFields: [], historyWindowSize: '7d', }; @@ -174,6 +178,7 @@ const StepDefineRuleComponent: FC = ({ const [openTimelineSearch, setOpenTimelineSearch] = useState(false); const [indexModified, setIndexModified] = useState(false); const [threatIndexModified, setThreatIndexModified] = useState(false); + const [dataViewTitle, setDataViewTitle] = useState(); const [indicesConfig] = useUiSetting$(DEFAULT_INDEX_KEY); const [threatIndicesConfig] = useUiSetting$(DEFAULT_THREAT_INDEX_KEY); @@ -202,6 +207,7 @@ const StepDefineRuleComponent: FC = ({ threatMapping: formThreatMapping, machineLearningJobId: formMachineLearningJobId, anomalyThreshold: formAnomalyThreshold, + dataSourceType: formDataSourceType, newTermsFields: formNewTermsFields, historyWindowSize: formHistoryWindowSize, }, @@ -221,6 +227,7 @@ const StepDefineRuleComponent: FC = ({ 'threatMapping', 'machineLearningJobId', 'anomalyThreshold', + 'dataSourceType', 'newTermsFields', 'historyWindowSize', ], @@ -236,6 +243,7 @@ const StepDefineRuleComponent: FC = ({ const newTermsFields = formNewTermsFields ?? initialState.newTermsFields; const historyWindowSize = formHistoryWindowSize ?? initialState.historyWindowSize; const ruleType = formRuleType || initialState.ruleType; + const dataSourceType = formDataSourceType || initialState.dataSourceType; // if 'index' is selected, use these browser fields // otherwise use the dataview browserfields @@ -243,24 +251,51 @@ const StepDefineRuleComponent: FC = ({ const [optionsSelected, setOptionsSelected] = useState( defaultValues?.eqlOptions || {} ); - const [initIsIndexPatternLoading, { browserFields, indexPatterns: initIndexPattern }] = - useFetchIndex(index, false); - const [indexPattern, setIndexPattern] = useState(initIndexPattern); - const [isIndexPatternLoading, setIsIndexPatternLoading] = useState(initIsIndexPatternLoading); - const [dataSourceRadioIdSelected, setDataSourceRadioIdSelected] = useState( - dataView == null || dataView === '' ? INDEX_PATTERN_SELECT_ID : DATA_VIEW_SELECT_ID + const [isIndexPatternLoading, { browserFields, indexPatterns: initIndexPattern }] = useFetchIndex( + index, + false ); + const [indexPattern, setIndexPattern] = useState(initIndexPattern); + const { data } = useKibana().services; + + // Why do we need this? to ensure the query bar auto-suggest gets the latest updates + // when the index pattern changes + // when we select new dataView + // when we choose some other dataSourceType useEffect(() => { - if (dataSourceRadioIdSelected === INDEX_PATTERN_SELECT_ID) { - setIndexPattern(initIndexPattern); + if (dataSourceType === DataSourceType.IndexPatterns) { + if (!isIndexPatternLoading) { + setIndexPattern(initIndexPattern); + } } - }, [initIndexPattern, dataSourceRadioIdSelected]); + + if (dataSourceType === DataSourceType.DataView) { + const fetchDataView = async () => { + if (dataView != null) { + const dv = await data.dataViews.get(dataView); + setDataViewTitle(dv.title); + setIndexPattern(dv); + } + }; + + fetchDataView(); + } + }, [dataSourceType, isIndexPatternLoading, data, dataView, initIndexPattern]); // Callback for when user toggles between Data Views and Index Patterns - const onChangeDataSource = (optionId: string) => { - setDataSourceRadioIdSelected(optionId); - }; + const onChangeDataSource = useCallback( + (optionId: string) => { + form.setFieldValue('dataSourceType', optionId); + form.getFields().index.reset({ + resetValue: false, + }); + form.getFields().dataViewId.reset({ + resetValue: false, + }); + }, + [form] + ); const [aggFields, setAggregatableFields] = useState([]); @@ -433,28 +468,26 @@ const StepDefineRuleComponent: FC = ({ const dataViewIndexPatternToggleButtonOptions: EuiButtonGroupOptionProps[] = useMemo( () => [ { - id: INDEX_PATTERN_SELECT_ID, + id: DataSourceType.IndexPatterns, label: i18nCore.translate( 'xpack.securitySolution.ruleDefine.indexTypeSelect.indexPattern', { defaultMessage: 'Index Patterns', } ), - iconType: - dataSourceRadioIdSelected === INDEX_PATTERN_SELECT_ID ? 'checkInCircleFilled' : 'empty', - 'data-test-subj': `rule-index-toggle-${INDEX_PATTERN_SELECT_ID}`, + iconType: dataSourceType === DataSourceType.IndexPatterns ? 'checkInCircleFilled' : 'empty', + 'data-test-subj': `rule-index-toggle-${DataSourceType.IndexPatterns}`, }, { - id: DATA_VIEW_SELECT_ID, + id: DataSourceType.DataView, label: i18nCore.translate('xpack.securitySolution.ruleDefine.indexTypeSelect.dataView', { defaultMessage: 'Data View', }), - iconType: - dataSourceRadioIdSelected === DATA_VIEW_SELECT_ID ? 'checkInCircleFilled' : 'empty', - 'data-test-subj': `rule-index-toggle-${DATA_VIEW_SELECT_ID}`, + iconType: dataSourceType === DataSourceType.DataView ? 'checkInCircleFilled' : 'empty', + 'data-test-subj': `rule-index-toggle-${DataSourceType.DataView}`, }, ], - [dataSourceRadioIdSelected] + [dataSourceType] ); const DataViewSelectorMemo = useMemo(() => { @@ -465,8 +498,6 @@ const StepDefineRuleComponent: FC = ({ component={DataViewSelector} componentProps={{ kibanaDataViews, - setIndexPattern, - setIsIndexPatternLoading, }} /> ); @@ -503,7 +534,7 @@ const StepDefineRuleComponent: FC = ({ isFullWidth={true} legend="Rule index pattern or data view selector" data-test-subj="dataViewIndexPatternButtonGroup" - idSelected={dataSourceRadioIdSelected} + idSelected={dataSourceType} onChange={onChangeDataSource} options={dataViewIndexPatternToggleButtonOptions} color="primary" @@ -512,9 +543,10 @@ const StepDefineRuleComponent: FC = ({ - {dataSourceRadioIdSelected === DATA_VIEW_SELECT_ID ? ( - DataViewSelectorMemo - ) : ( + + {DataViewSelectorMemo} + + = ({ }, }} /> - )} + ); }, [ - dataSourceRadioIdSelected, + dataSourceType, dataViewIndexPatternToggleButtonOptions, DataViewSelectorMemo, indexModified, handleResetIndices, + onChangeDataSource, ]); const QueryBarMemo = useMemo( @@ -619,19 +652,36 @@ const StepDefineRuleComponent: FC = ({ [indexPattern] ); + const dataForDescription: Partial = getStepDataDataSource(initialState); + + if (dataSourceType === DataSourceType.DataView) { + dataForDescription.dataViewTitle = dataViewTitle; + } + return isReadOnlyView ? ( ) : ( <>
+ + + = { ...args: Parameters ): ReturnType> | undefined => { const [{ formData }] = args; - const skipValidation = isMlRule(formData.ruleType) || formData.dataViewId != null; + const skipValidation = + isMlRule(formData.ruleType) || formData.dataSourceType !== DataSourceType.IndexPatterns; if (skipValidation) { return; @@ -94,10 +96,11 @@ export const schema: FormSchema = { // the dropdown defaults the dataViewId to an empty string somehow on render.. // need to figure this out. const notEmptyDataViewId = formData.dataViewId != null && formData.dataViewId !== ''; + const skipValidation = isMlRule(formData.ruleType) || - ((formData.index != null || notEmptyDataViewId) && - !(formData.index != null && notEmptyDataViewId)); + notEmptyDataViewId || + formData.dataSourceType !== DataSourceType.DataView; if (skipValidation) { return; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/__mocks__/api.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/__mocks__/api.ts index 346acb249dbb0..69aa2a4502bc0 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/__mocks__/api.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/__mocks__/api.ts @@ -6,7 +6,6 @@ */ import type { - GetAggregateRuleExecutionEventsResponse, GetInstalledIntegrationsResponse, RulesSchema, } from '../../../../../../common/detection_engine/schemas/response'; @@ -60,49 +59,6 @@ export const fetchRuleById = jest.fn( export const fetchRules = async (_: FetchRulesProps): Promise => Promise.resolve(rulesMock); -export const fetchRuleExecutionEvents = async ({ - ruleId, - start, - end, - filters, - signal, -}: { - ruleId: string; - start: string; - end: string; - filters?: string; - signal?: AbortSignal; -}): Promise => { - return Promise.resolve({ - events: [ - { - duration_ms: 3866, - es_search_duration_ms: 1236, - execution_uuid: '88d15095-7937-462c-8f21-9763e1387cad', - gap_duration_s: 0, - indexing_duration_ms: 95, - message: - "rule executed: siem.queryRule:fb1fc150-a292-11ec-a2cf-c1b28b0392b0: 'Lots of Execution Events'", - num_active_alerts: 0, - num_errored_actions: 0, - num_new_alerts: 0, - num_recovered_alerts: 0, - num_succeeded_actions: 1, - num_triggered_actions: 1, - schedule_delay_ms: -127535, - search_duration_ms: 1255, - security_message: 'succeeded', - security_status: 'succeeded', - status: 'success', - timed_out: false, - timestamp: '2022-03-13T06:04:05.838Z', - total_search_duration_ms: 0, - }, - ], - total: 1, - }); -}; - export const fetchTags = async ({ signal }: { signal: AbortSignal }): Promise => Promise.resolve(['elastic', 'love', 'quality', 'code']); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts index 0acaaf7a1fe0f..28d708743419a 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts @@ -5,7 +5,9 @@ * 2.0. */ +import { buildEsQuery } from '@kbn/es-query'; import { KibanaServices } from '../../../../common/lib/kibana'; + import { createRule, updateRule, @@ -15,7 +17,6 @@ import { createPrepackagedRules, importRules, exportRules, - fetchRuleExecutionEvents, fetchTags, getPrePackagedRulesStatus, previewRule, @@ -27,7 +28,7 @@ import { } from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock'; import { getPatchRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/patch_rules_schema.mock'; import { rulesMock } from './mock'; -import { buildEsQuery } from '@kbn/es-query'; + const abortCtrl = new AbortController(); const mockKibanaServices = KibanaServices.get as jest.Mock; jest.mock('../../../../common/lib/kibana'); @@ -617,56 +618,6 @@ describe('Detections Rules API', () => { }); }); - describe('fetchRuleExecutionEvents', () => { - const responseMock = { events: [] }; - - beforeEach(() => { - fetchMock.mockClear(); - fetchMock.mockResolvedValue(responseMock); - }); - - test('calls API with correct parameters', async () => { - await fetchRuleExecutionEvents({ - ruleId: '42', - start: '2001-01-01T17:00:00.000Z', - end: '2001-01-02T17:00:00.000Z', - queryText: '', - statusFilters: [], - signal: abortCtrl.signal, - }); - - expect(fetchMock).toHaveBeenCalledWith( - '/internal/detection_engine/rules/42/execution/events', - { - method: 'GET', - query: { - end: '2001-01-02T17:00:00.000Z', - page: undefined, - per_page: undefined, - query_text: '', - sort_field: undefined, - sort_order: undefined, - start: '2001-01-01T17:00:00.000Z', - status_filters: '', - }, - signal: abortCtrl.signal, - } - ); - }); - - test('returns API response as is', async () => { - const response = await fetchRuleExecutionEvents({ - ruleId: '42', - start: 'now-30', - end: 'now', - queryText: '', - statusFilters: [], - signal: abortCtrl.signal, - }); - expect(response).toEqual(responseMock); - }); - }); - describe('fetchTags', () => { beforeEach(() => { fetchMock.mockClear(); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts index 217810e343970..dfee0f418d2d5 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts @@ -5,9 +5,7 @@ * 2.0. */ -import type { SortOrder } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { camelCase } from 'lodash'; -import dateMath from '@kbn/datemath'; import type { HttpStart } from '@kbn/core/public'; import { @@ -17,23 +15,17 @@ import { DETECTION_ENGINE_TAGS_URL, DETECTION_ENGINE_RULES_BULK_ACTION, DETECTION_ENGINE_RULES_PREVIEW, - detectionEngineRuleExecutionEventsUrl, DETECTION_ENGINE_INSTALLED_INTEGRATIONS_URL, } from '../../../../../common/constants'; -import type { - AggregateRuleExecutionEvent, - BulkAction, - RuleExecutionStatus, -} from '../../../../../common/detection_engine/schemas/common'; +import type { BulkAction } from '../../../../../common/detection_engine/schemas/common'; import type { FullResponseSchema, PreviewResponse, } from '../../../../../common/detection_engine/schemas/request'; import type { RulesSchema, - GetAggregateRuleExecutionEventsResponse, + GetInstalledIntegrationsResponse, } from '../../../../../common/detection_engine/schemas/response'; -import type { GetInstalledIntegrationsResponse } from '../../../../../common/detection_engine/schemas/response/get_installed_integrations_response_schema'; import type { UpdateRulesProps, @@ -321,64 +313,6 @@ export const exportRules = async ({ }); }; -/** - * Fetch rule execution events (e.g. status changes) from Event Log. - * - * @param ruleId Saved Object ID of the rule (`rule.id`, not static `rule.rule_id`) - * @param start Start daterange either in UTC ISO8601 or as datemath string (e.g. `2021-12-29T02:44:41.653Z` or `now-30`) - * @param end End daterange either in UTC ISO8601 or as datemath string (e.g. `2021-12-29T02:44:41.653Z` or `now/w`) - * @param queryText search string in querystring format (e.g. `event.duration > 1000 OR kibana.alert.rule.execution.metrics.execution_gap_duration_s > 100`) - * @param statusFilters RuleExecutionStatus[] array of `statusFilters` (e.g. `succeeded,failed,partial failure`) - * @param page current page to fetch - * @param perPage number of results to fetch per page - * @param sortField keyof AggregateRuleExecutionEvent field to sort by - * @param sortOrder SortOrder what order to sort by (e.g. `asc` or `desc`) - * @param signal AbortSignal Optional signal for cancelling the request - * - * @throws An error if response is not OK - */ -export const fetchRuleExecutionEvents = async ({ - ruleId, - start, - end, - queryText, - statusFilters, - page, - perPage, - sortField, - sortOrder, - signal, -}: { - ruleId: string; - start: string; - end: string; - queryText?: string; - statusFilters?: RuleExecutionStatus[]; - page?: number; - perPage?: number; - sortField?: keyof AggregateRuleExecutionEvent; - sortOrder?: SortOrder; - signal?: AbortSignal; -}): Promise => { - const url = detectionEngineRuleExecutionEventsUrl(ruleId); - const startDate = dateMath.parse(start); - const endDate = dateMath.parse(end, { roundUp: true }); - return KibanaServices.get().http.fetch(url, { - method: 'GET', - query: { - start: startDate?.utc().toISOString(), - end: endDate?.utc().toISOString(), - query_text: queryText, - status_filters: statusFilters?.sort()?.join(','), - page, - per_page: perPage, - sort_field: sortField, - sort_order: sortOrder, - }, - signal, - }); -}; - /** * Fetch all unique Tags used by Rules * diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/index.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/index.ts index 8f6dbccd1ee57..42c1eff7435bc 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/index.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/index.ts @@ -11,4 +11,3 @@ export * from './use_create_rule'; export * from './types'; export * from './use_rule'; export * from './use_pre_packaged_rules'; -export * from './use_rule_execution_events'; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/mock.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/mock.ts index 99c38b6a62a1d..bdcc8e0ec5e8a 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/mock.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/mock.ts @@ -5,7 +5,6 @@ * 2.0. */ -import type { GetAggregateRuleExecutionEventsResponse } from '../../../../../common/detection_engine/schemas/response'; import type { FetchRulesResponse, Rule } from './types'; export const savedRuleMock: Rule = { @@ -136,170 +135,3 @@ export const rulesMock: FetchRulesResponse = { }, ], }; - -export const ruleExecutionEventsMock: GetAggregateRuleExecutionEventsResponse = { - events: [ - { - execution_uuid: 'dc45a63c-4872-4964-a2d0-bddd8b2e634d', - timestamp: '2022-04-28T21:19:08.047Z', - duration_ms: 3, - status: 'failure', - message: 'siem.queryRule:a6e61cf0-c737-11ec-9e32-e14913ffdd2d: execution failed', - num_active_alerts: 0, - num_new_alerts: 0, - num_recovered_alerts: 0, - num_triggered_actions: 0, - num_succeeded_actions: 0, - num_errored_actions: 0, - total_search_duration_ms: 0, - es_search_duration_ms: 0, - schedule_delay_ms: 2169, - timed_out: false, - indexing_duration_ms: 0, - search_duration_ms: 0, - gap_duration_s: 0, - security_status: 'failed', - security_message: 'Rule failed to execute because rule ran after it was disabled.', - }, - { - execution_uuid: '0fde9271-05d0-4bfb-8ff8-815756d28350', - timestamp: '2022-04-28T21:19:04.973Z', - duration_ms: 1446, - status: 'success', - message: - "rule executed: siem.queryRule:a6e61cf0-c737-11ec-9e32-e14913ffdd2d: 'Click here for hot fresh alerts!'", - num_active_alerts: 0, - num_new_alerts: 0, - num_recovered_alerts: 0, - num_triggered_actions: 0, - num_succeeded_actions: 0, - num_errored_actions: 0, - total_search_duration_ms: 0, - es_search_duration_ms: 0, - schedule_delay_ms: 2089, - timed_out: false, - indexing_duration_ms: 0, - search_duration_ms: 2, - gap_duration_s: 0, - security_status: 'succeeded', - security_message: 'succeeded', - }, - { - execution_uuid: '5daaa259-ded8-4a52-853e-1e7652d325d5', - timestamp: '2022-04-28T21:19:01.976Z', - duration_ms: 1395, - status: 'success', - message: - "rule executed: siem.queryRule:a6e61cf0-c737-11ec-9e32-e14913ffdd2d: 'Click here for hot fresh alerts!'", - num_active_alerts: 0, - num_new_alerts: 0, - num_recovered_alerts: 0, - num_triggered_actions: 0, - num_succeeded_actions: 0, - num_errored_actions: 0, - total_search_duration_ms: 0, - es_search_duration_ms: 1, - schedule_delay_ms: 2637, - timed_out: false, - indexing_duration_ms: 0, - search_duration_ms: 3, - gap_duration_s: 0, - security_status: 'succeeded', - security_message: 'succeeded', - }, - { - execution_uuid: 'c7223e1c-4264-4a27-8697-0d720243fafc', - timestamp: '2022-04-28T21:18:58.431Z', - duration_ms: 1815, - status: 'success', - message: - "rule executed: siem.queryRule:a6e61cf0-c737-11ec-9e32-e14913ffdd2d: 'Click here for hot fresh alerts!'", - num_active_alerts: 0, - num_new_alerts: 0, - num_recovered_alerts: 0, - num_triggered_actions: 0, - num_succeeded_actions: 0, - num_errored_actions: 0, - total_search_duration_ms: 0, - es_search_duration_ms: 1, - schedule_delay_ms: -255429, - timed_out: false, - indexing_duration_ms: 0, - search_duration_ms: 3, - gap_duration_s: 0, - security_status: 'succeeded', - security_message: 'succeeded', - }, - { - execution_uuid: '1f6ba0c1-cc36-4f45-b919-7790b8a8d670', - timestamp: '2022-04-28T21:18:13.954Z', - duration_ms: 2055, - status: 'success', - message: - "rule executed: siem.queryRule:a6e61cf0-c737-11ec-9e32-e14913ffdd2d: 'Click here for hot fresh alerts!'", - num_active_alerts: 0, - num_new_alerts: 0, - num_recovered_alerts: 0, - num_triggered_actions: 0, - num_succeeded_actions: 0, - num_errored_actions: 0, - total_search_duration_ms: 0, - es_search_duration_ms: 0, - schedule_delay_ms: 2027, - timed_out: false, - indexing_duration_ms: 0, - search_duration_ms: 0, - gap_duration_s: 0, - security_status: 'partial failure', - security_message: - 'Check privileges failed to execute ResponseError: index_not_found_exception: [index_not_found_exception] Reason: no such index [yup] name: "Click here for hot fresh alerts!" id: "a6e61cf0-c737-11ec-9e32-e14913ffdd2d" rule id: "34946b12-88d1-49ef-82b7-9cad45972030" execution id: "1f6ba0c1-cc36-4f45-b919-7790b8a8d670" space ID: "default"', - }, - { - execution_uuid: 'b0f65d64-b229-432b-9d39-f4385a7f9368', - timestamp: '2022-04-28T21:15:43.086Z', - duration_ms: 1205, - status: 'success', - message: - "rule executed: siem.queryRule:a6e61cf0-c737-11ec-9e32-e14913ffdd2d: 'Click here for hot fresh alerts!'", - num_active_alerts: 0, - num_new_alerts: 0, - num_recovered_alerts: 0, - num_triggered_actions: 0, - num_succeeded_actions: 0, - num_errored_actions: 0, - total_search_duration_ms: 0, - es_search_duration_ms: 672, - schedule_delay_ms: 3086, - timed_out: false, - indexing_duration_ms: 140, - search_duration_ms: 684, - gap_duration_s: 0, - security_status: 'succeeded', - security_message: 'succeeded', - }, - { - execution_uuid: '7bfd25b9-c0d8-44b1-982c-485169466a8e', - timestamp: '2022-04-28T21:10:40.135Z', - duration_ms: 6321, - status: 'success', - message: - "rule executed: siem.queryRule:a6e61cf0-c737-11ec-9e32-e14913ffdd2d: 'Click here for hot fresh alerts!'", - num_active_alerts: 0, - num_new_alerts: 0, - num_recovered_alerts: 0, - num_triggered_actions: 0, - num_succeeded_actions: 0, - num_errored_actions: 0, - total_search_duration_ms: 0, - es_search_duration_ms: 930, - schedule_delay_ms: 1222, - timed_out: false, - indexing_duration_ms: 2103, - search_duration_ms: 946, - gap_duration_s: 0, - security_status: 'succeeded', - security_message: 'succeeded', - }, - ], - total: 7, -}; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/translations.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/translations.ts index 86107a4019b0a..c1161db6db26f 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/translations.ts @@ -109,10 +109,3 @@ export const RELOAD_MISSING_PREPACKAGED_RULES_AND_TIMELINES = ( 'Install {missingRules} Elastic prebuilt {missingRules, plural, =1 {rule} other {rules}} and {missingTimelines} Elastic prebuilt {missingTimelines, plural, =1 {timeline} other {timelines}} ', } ); - -export const RULE_EXECUTION_EVENTS_FETCH_FAILURE = i18n.translate( - 'xpack.securitySolution.containers.detectionEngine.ruleExecutionEventsFetchFailDescription', - { - defaultMessage: 'Failed to fetch rule execution events', - } -); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts index 31c0b8da55cbf..ed5ebc233f9ad 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts @@ -22,6 +22,9 @@ import { severity_mapping, severity, } from '@kbn/securitysolution-io-ts-alerting-types'; + +import { RuleExecutionSummary } from '../../../../../common/detection_engine/rule_monitoring'; + import type { SortOrder, BulkAction, @@ -41,7 +44,6 @@ import { event_category_override, tiebreaker_field, threshold, - ruleExecutionSummary, RelatedIntegrationArray, RequiredFieldArray, SetupGuide, @@ -169,7 +171,7 @@ export const RuleSchema = t.intersection([ exceptions_list: listArray, uuid: t.string, version: t.number, - execution_summary: ruleExecutionSummary, + execution_summary: RuleExecutionSummary, }), ]); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_execution_events.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_execution_events.tsx deleted file mode 100644 index 2aa378379fc14..0000000000000 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_execution_events.tsx +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { SortOrder } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { useQuery } from 'react-query'; -import type { - AggregateRuleExecutionEvent, - RuleExecutionStatus, -} from '../../../../../common/detection_engine/schemas/common'; -import type { GetAggregateRuleExecutionEventsResponse } from '../../../../../common/detection_engine/schemas/response'; -import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; -import { fetchRuleExecutionEvents } from './api'; -import * as i18n from './translations'; - -export interface UseRuleExecutionEventsArgs { - ruleId: string; - start: string; - end: string; - queryText?: string; - statusFilters?: RuleExecutionStatus[]; - page?: number; - perPage?: number; - sortField?: keyof AggregateRuleExecutionEvent; - sortOrder?: SortOrder; -} - -export const useRuleExecutionEvents = ({ - ruleId, - start, - end, - queryText, - statusFilters, - page, - perPage, - sortField, - sortOrder, -}: UseRuleExecutionEventsArgs) => { - const { addError } = useAppToasts(); - - return useQuery( - [ - 'ruleExecutionEvents', - { - ruleId, - start, - end, - queryText, - statusFilters, - page, - perPage, - sortField, - sortOrder, - }, - ], - async ({ signal }) => { - return fetchRuleExecutionEvents({ - ruleId, - start, - end, - queryText, - statusFilters, - page, - perPage, - sortField, - sortOrder, - signal, - }); - }, - { - keepPreviousData: true, - onError: (e) => { - addError(e, { title: i18n.RULE_EXECUTION_EVENTS_FETCH_FAILURE }); - }, - } - ); -}; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/__mocks__/mock.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/__mocks__/mock.ts index 243b5195788b3..fdf1370587638 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/__mocks__/mock.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/__mocks__/mock.ts @@ -8,6 +8,7 @@ import { FilterStateStore } from '@kbn/es-query'; import type { Rule } from '../../../../../containers/detection_engine/rules'; import type { AboutStepRule, ActionsStepRule, DefineStepRule, ScheduleStepRule } from '../../types'; +import { DataSourceType } from '../../types'; import type { FieldValueQueryBar } from '../../../../../components/rules/query_bar'; import { fillEmptySeverityMappings } from '../../helpers'; import { getThreatMock } from '../../../../../../../common/detection_engine/schemas/types/threat.mock'; @@ -216,6 +217,7 @@ export const mockDefineStepRule = (): DefineStepRule => ({ }, }, eqlOptions: {}, + dataSourceType: DataSourceType.IndexPatterns, newTermsFields: ['host.ip'], historyWindowSize: '7d', }); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/use_columns.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/use_columns.tsx index e93c6a4ce0af0..30706752eeffd 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/use_columns.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/use_columns.tsx @@ -39,7 +39,7 @@ import { RuleStatusBadge } from '../../../../components/rules/rule_execution_sta import type { DurationMetric, RuleExecutionSummary, -} from '../../../../../../common/detection_engine/schemas/common'; +} from '../../../../../../common/detection_engine/rule_monitoring'; import { useAppToasts } from '../../../../../common/hooks/use_app_toasts'; import { useStartTransaction } from '../../../../../common/lib/apm/use_start_transaction'; import { useInvalidateRules } from '../../../../containers/detection_engine/rules/use_find_rules_query'; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts index c50d602c086bd..bec0746bde5d1 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/helpers.ts @@ -9,6 +9,7 @@ import { has, isEmpty } from 'lodash/fp'; import type { Unit } from '@kbn/datemath'; import moment from 'moment'; import deepmerge from 'deepmerge'; +import omit from 'lodash/omit'; import type { ExceptionListType, @@ -40,6 +41,7 @@ import type { RuleStep, AdvancedPreviewOptions, } from '../types'; +import { DataSourceType } from '../types'; import type { FieldValueQueryBar } from '../../../../components/rules/query_bar'; import type { CreateRulesSchema } from '../../../../../../common/detection_engine/schemas/request'; import { stepDefineDefaultValue } from '../../../../components/rules/step_define_rule'; @@ -337,9 +339,34 @@ export const filterEmptyThreats = (threats: Threats): Threats => { }); }; +/** + * remove unused data source. + * Ex: rule is using a data view so we should not + * write an index property on the rule form. + * @param defineStepData + * @returns DefineStepRule + */ +export const getStepDataDataSource = ( + defineStepData: DefineStepRule +): Omit & { + index?: string[]; + dataViewId?: string; +} => { + const copiedStepData = { ...defineStepData }; + if (defineStepData.dataSourceType === DataSourceType.DataView) { + return omit(copiedStepData, ['index', 'dataSourceType']); + } else if (defineStepData.dataSourceType === DataSourceType.IndexPatterns) { + return omit(copiedStepData, ['dataViewId', 'dataSourceType']); + } + return copiedStepData; +}; + export const formatDefineStepData = (defineStepData: DefineStepRule): DefineStepRuleJson => { - const ruleFields = filterRuleFieldsForType(defineStepData, defineStepData.ruleType); + const stepData = getStepDataDataSource(defineStepData); + + const ruleFields = filterRuleFieldsForType(stepData, stepData.ruleType); const { ruleType, timeline } = ruleFields; + const baseFields = { type: ruleType, ...(timeline.id != null && diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/__mocks__/rule_details_context.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/__mocks__/rule_details_context.tsx index 220a0635f2011..e8cdcebbc938a 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/__mocks__/rule_details_context.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/__mocks__/rule_details_context.tsx @@ -10,7 +10,7 @@ import type { RuleDetailsContextType } from '../rule_details_context'; export const useRuleDetailsContextMock = { create: (): jest.Mocked => ({ - executionLogs: { + executionResults: { state: { superDatePicker: { recentlyUsedRanges: [], diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/__snapshots__/execution_log_search_bar.test.tsx.snap b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/__snapshots__/execution_log_search_bar.test.tsx.snap index a4e8e2cf6e9bd..241f363d4885f 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/__snapshots__/execution_log_search_bar.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/__snapshots__/execution_log_search_bar.test.tsx.snap @@ -10,69 +10,17 @@ exports[`ExecutionLogSearchBar snapshots renders correctly against snapshot 1`] - - - Status - - } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - isOpen={false} - ownFocus={true} - panelPaddingSize="none" - repositionOnScroll={true} - > - - - Succeeded - - - - - Failed - - - - - Partial failure - - - - + `; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/execution_log_columns.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/execution_log_columns.tsx index 8bf9c4895e07a..9d5fd1a974dac 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/execution_log_columns.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/execution_log_columns.tsx @@ -5,26 +5,27 @@ * 2.0. */ +import React from 'react'; import type { EuiBasicTableColumn } from '@elastic/eui'; -import { EuiHealth, EuiLink, EuiText } from '@elastic/eui'; +import { EuiLink, EuiText } from '@elastic/eui'; import type { DocLinksStart } from '@kbn/core/public'; import { FormattedMessage } from '@kbn/i18n-react'; -import { capitalize } from 'lodash'; -import React from 'react'; + import type { - AggregateRuleExecutionEvent, + RuleExecutionResult, RuleExecutionStatus, -} from '../../../../../../../common/detection_engine/schemas/common'; -import { getEmptyTagValue, getEmptyValue } from '../../../../../../common/components/empty_value'; +} from '../../../../../../../common/detection_engine/rule_monitoring'; + +import { getEmptyValue } from '../../../../../../common/components/empty_value'; import { FormattedDate } from '../../../../../../common/components/formatted_date'; -import { getStatusColor } from '../../../../../components/rules/rule_execution_status/utils'; +import { ExecutionStatusIndicator } from '../../../../../../detection_engine/rule_monitoring'; import { PopoverTooltip } from '../../all/popover_tooltip'; import { TableHeaderTooltipCell } from '../../all/table_header_tooltip_cell'; import { RuleDurationFormat } from './rule_duration_format'; import * as i18n from './translations'; -export const EXECUTION_LOG_COLUMNS: Array> = [ +export const EXECUTION_LOG_COLUMNS: Array> = [ { name: ( ), field: 'security_status', - render: (value: RuleExecutionStatus) => - value ? ( - {capitalize(value)} - ) : ( - getEmptyTagValue() - ), + render: (value: RuleExecutionStatus) => ( + + ), sortable: false, truncateText: false, width: '10%', @@ -51,7 +49,7 @@ export const EXECUTION_LOG_COLUMNS: Array ), - render: (value: string) => , + render: (value: string) => , sortable: true, truncateText: false, width: '15%', @@ -88,7 +86,7 @@ export const EXECUTION_LOG_COLUMNS: Array> => [ +): Array> => [ { field: 'gap_duration_s', name: ( diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/execution_log_search_bar.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/execution_log_search_bar.test.tsx index 60b5f7813bbf0..abda12afde763 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/execution_log_search_bar.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/execution_log_search_bar.test.tsx @@ -23,7 +23,12 @@ describe('ExecutionLogSearchBar', () => { describe('snapshots', () => { test('renders correctly against snapshot', () => { const wrapper = shallow( - + ); expect(wrapper).toMatchSnapshot(); }); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/execution_log_search_bar.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/execution_log_search_bar.tsx index 74804b7c6b557..39a4c3ee159de 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/execution_log_search_bar.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/execution_log_search_bar.tsx @@ -5,20 +5,13 @@ * 2.0. */ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { capitalize, replace } from 'lodash'; -import { - EuiHealth, - EuiFieldSearch, - EuiFlexGroup, - EuiFlexItem, - EuiPopover, - EuiFilterGroup, - EuiFilterButton, - EuiFilterSelectItem, -} from '@elastic/eui'; -import { RuleExecutionStatus } from '../../../../../../../common/detection_engine/schemas/common'; -import { getStatusColor } from '../../../../../components/rules/rule_execution_status/utils'; +import React, { useCallback } from 'react'; +import { replace } from 'lodash'; +import { EuiFieldSearch, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +import { RuleExecutionStatus } from '../../../../../../../common/detection_engine/rule_monitoring'; +import { ExecutionStatusFilter } from '../../../../../../detection_engine/rule_monitoring'; + import * as i18n from './translations'; export const EXECUTION_LOG_SCHEMA_MAPPING = { @@ -42,22 +35,18 @@ export const replaceQueryTextAliases = (queryText: string): string => { ); }; -const statuses = [ +// This only includes statuses which are or can be final +const STATUS_FILTERS = [ RuleExecutionStatus.succeeded, RuleExecutionStatus.failed, RuleExecutionStatus['partial failure'], ]; -const statusFilters = statuses.map((status) => ({ - label: {capitalize(status)}, - selected: false, -})); - interface ExecutionLogTableSearchProps { onlyShowFilters: true; + selectedStatuses: RuleExecutionStatus[]; + onStatusFilterChange: (selectedStatuses: RuleExecutionStatus[]) => void; onSearch: (queryText: string) => void; - onStatusFilterChange: (statusFilters: RuleExecutionStatus[]) => void; - defaultSelectedStatusFilters?: RuleExecutionStatus[]; } /** @@ -68,47 +57,14 @@ interface ExecutionLogTableSearchProps { * Please see this comment for history/details: https://github.com/elastic/kibana/pull/127339/files#r825240516 */ export const ExecutionLogSearchBar = React.memo( - ({ onlyShowFilters, onSearch, onStatusFilterChange, defaultSelectedStatusFilters = [] }) => { - const [isPopoverOpen, setIsPopoverOpen] = useState(false); - const [selectedFilters, setSelectedFilters] = useState( - defaultSelectedStatusFilters - ); - - const onSearchCallback = useCallback( + ({ onlyShowFilters, selectedStatuses, onStatusFilterChange, onSearch }) => { + const handleSearch = useCallback( (queryText: string) => { onSearch(replaceQueryTextAliases(queryText)); }, [onSearch] ); - const onStatusFilterChangeCallback = useCallback( - (filter: RuleExecutionStatus) => { - setSelectedFilters( - selectedFilters.includes(filter) - ? selectedFilters.filter((f) => f !== filter) - : [...selectedFilters, filter] - ); - }, - [selectedFilters] - ); - - const filtersComponent = useMemo(() => { - return statuses.map((filter, index) => ( - onStatusFilterChangeCallback(filter)} - title={filter} - > - {capitalize(filter)} - - )); - }, [onStatusFilterChangeCallback, selectedFilters]); - - useEffect(() => { - onStatusFilterChange(selectedFilters); - }, [onStatusFilterChange, selectedFilters]); - return ( @@ -117,37 +73,18 @@ export const ExecutionLogSearchBar = React.memo( data-test-subj="executionLogSearch" aria-label={i18n.RULE_EXECUTION_LOG_SEARCH_PLACEHOLDER} placeholder={i18n.RULE_EXECUTION_LOG_SEARCH_PLACEHOLDER} - onSearch={onSearchCallback} + onSearch={handleSearch} isClearable={true} fullWidth={true} /> )} - - setIsPopoverOpen(!isPopoverOpen)} - numFilters={statusFilters.length} - isSelected={isPopoverOpen} - hasActiveFilters={selectedFilters.length > 0} - numActiveFilters={selectedFilters.length} - > - {i18n.COLUMN_STATUS} - - } - isOpen={isPopoverOpen} - closePopover={() => setIsPopoverOpen(false)} - panelPaddingSize="none" - repositionOnScroll - > - {filtersComponent} - - + ); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/execution_log_table.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/execution_log_table.test.tsx index 608853f004eb9..52f85b228ab36 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/execution_log_table.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/execution_log_table.test.tsx @@ -5,20 +5,23 @@ * 2.0. */ -import { ruleExecutionEventsMock } from '../../../../../containers/detection_engine/rules/mock'; +import React from 'react'; +import { noop } from 'lodash/fp'; import { render, screen } from '@testing-library/react'; + import { TestProviders } from '../../../../../../common/mock'; import { useRuleDetailsContextMock } from '../__mocks__/rule_details_context'; -import React from 'react'; -import { noop } from 'lodash/fp'; +import { getRuleExecutionResultsResponseMock } from '../../../../../../../common/detection_engine/rule_monitoring/mocks'; -import { useRuleExecutionEvents } from '../../../../../containers/detection_engine/rules'; +import { useExecutionResults } from '../../../../../../detection_engine/rule_monitoring'; import { useSourcererDataView } from '../../../../../../common/containers/sourcerer'; import { useRuleDetailsContext } from '../rule_details_context'; import { ExecutionLogTable } from './execution_log_table'; jest.mock('../../../../../../common/containers/sourcerer'); -jest.mock('../../../../../containers/detection_engine/rules'); +jest.mock( + '../../../../../../detection_engine/rule_monitoring/components/execution_results_table/use_execution_results' +); jest.mock('../rule_details_context'); const mockUseSourcererDataView = useSourcererDataView as jest.Mock; @@ -30,9 +33,9 @@ mockUseSourcererDataView.mockReturnValue({ loading: false, }); -const mockUseRuleExecutionEvents = useRuleExecutionEvents as jest.Mock; +const mockUseRuleExecutionEvents = useExecutionResults as jest.Mock; mockUseRuleExecutionEvents.mockReturnValue({ - data: ruleExecutionEventsMock, + data: getRuleExecutionResultsResponseMock.getSomeResponse(), isLoading: false, isFetching: false, }); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/execution_log_table.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/execution_log_table.tsx index fe0ff1b7b555a..e0b798f76b591 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/execution_log_table.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/execution_log_table.tsx @@ -5,32 +5,36 @@ * 2.0. */ +import React, { useCallback, useMemo, useRef } from 'react'; import { useDispatch } from 'react-redux'; import styled from 'styled-components'; import moment from 'moment'; -import React, { useCallback, useMemo, useRef } from 'react'; import type { OnTimeChangeProps, OnRefreshProps, OnRefreshChangeProps } from '@elastic/eui'; import { EuiTextColor, EuiFlexGroup, EuiFlexItem, + EuiPanel, EuiSuperDatePicker, EuiSpacer, EuiSwitch, EuiBasicTable, EuiButton, } from '@elastic/eui'; + import type { Filter, Query } from '@kbn/es-query'; import { buildFilter, FILTERS } from '@kbn/es-query'; import { MAX_EXECUTION_EVENTS_DISPLAYED } from '@kbn/securitysolution-rules'; import { mountReactNode } from '@kbn/core/public/utils'; + import { RuleDetailTabs } from '..'; import { RULE_DETAILS_EXECUTION_LOG_TABLE_SHOW_METRIC_COLUMNS_STORAGE_KEY } from '../../../../../../../common/constants'; import type { - AggregateRuleExecutionEvent, + RuleExecutionResult, RuleExecutionStatus, -} from '../../../../../../../common/detection_engine/schemas/common'; +} from '../../../../../../../common/detection_engine/rule_monitoring'; +import { HeaderSection } from '../../../../../../common/components/header_section'; import { UtilityBar, UtilityBarGroup, @@ -56,7 +60,7 @@ import { isRelativeTimeRange, } from '../../../../../../common/store/inputs/model'; import { SourcererScopeName } from '../../../../../../common/store/sourcerer/model'; -import { useRuleExecutionEvents } from '../../../../../containers/detection_engine/rules'; +import { useExecutionResults } from '../../../../../../detection_engine/rule_monitoring'; import { useRuleDetailsContext } from '../rule_details_context'; import * as i18n from './translations'; import { EXECUTION_LOG_COLUMNS, GET_EXECUTION_LOG_METRICS_COLUMNS } from './execution_log_columns'; @@ -97,7 +101,7 @@ const ExecutionLogTableComponent: React.FC = ({ } = useKibana().services; const { - [RuleDetailTabs.executionLogs]: { + [RuleDetailTabs.executionResults]: { state: { superDatePicker: { recentlyUsedRanges, refreshInterval, isPaused, start, end }, queryText, @@ -181,7 +185,7 @@ const ExecutionLogTableComponent: React.FC = ({ isFetching, isLoading, refetch, - } = useRuleExecutionEvents({ + } = useExecutionResults({ ruleId, start, end, @@ -368,7 +372,7 @@ const ExecutionLogTableComponent: React.FC = ({ description: i18n.COLUMN_ACTIONS_TOOLTIP, icon: 'filter', type: 'icon', - onClick: (executionEvent: AggregateRuleExecutionEvent) => { + onClick: (executionEvent: RuleExecutionResult) => { if (executionEvent?.execution_uuid) { onFilterByExecutionIdCallback( executionEvent.execution_uuid, @@ -393,14 +397,18 @@ const ExecutionLogTableComponent: React.FC = ({ ); return ( - <> + + {/* Filter bar */} + + + @@ -418,7 +426,10 @@ const ExecutionLogTableComponent: React.FC = ({ /> + + + {/* Utility bar */} @@ -460,15 +471,17 @@ const ExecutionLogTableComponent: React.FC = ({ + + {/* Table with items */} - + ); }; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/translations.ts index b161ae3662e0e..114faa77f871e 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/execution_log_table/translations.ts @@ -7,6 +7,20 @@ import { i18n } from '@kbn/i18n'; +export const TABLE_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDetails.ruleExecutionLog.tableTitle', + { + defaultMessage: 'Execution log', + } +); + +export const TABLE_SUBTITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDetails.ruleExecutionLog.tableSubtitle', + { + defaultMessage: 'A log of rule execution results', + } +); + export const SHOWING_EXECUTIONS = (totalItems: number) => i18n.translate( 'xpack.securitySolution.detectionEngine.ruleDetails.ruleExecutionLog.totalExecutionsLabel', diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index 2c9fc4c2f74d0..051e3db28dd7e 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -104,10 +104,14 @@ import { RuleStatusFailedCallOut, ruleStatusI18n, } from '../../../../components/rules/rule_execution_status'; +import { + ExecutionEventsTable, + useRuleExecutionSettings, +} from '../../../../../detection_engine/rule_monitoring'; +import { ExecutionLogTable } from './execution_log_table/execution_log_table'; import * as detectionI18n from '../../translations'; import * as ruleI18n from '../translations'; -import { ExecutionLogTable } from './execution_log_table/execution_log_table'; import { RuleDetailsContextProvider } from './rule_details_context'; import * as i18n from './translations'; import { NeedAdminForUpdateRulesCallOut } from '../../../../components/callouts/need_admin_for_update_callout'; @@ -141,8 +145,9 @@ const StyledMinHeightTabContainer = styled.div` export enum RuleDetailTabs { alerts = 'alerts', - executionLogs = 'executionLogs', exceptions = 'exceptions', + executionResults = 'executionResults', + executionEvents = 'executionEvents', } const ruleDetailTabs = [ @@ -159,10 +164,16 @@ const ruleDetailTabs = [ dataTestSubj: 'exceptionsTab', }, { - id: RuleDetailTabs.executionLogs, - name: i18n.RULE_EXECUTION_LOGS, + id: RuleDetailTabs.executionResults, + name: i18n.EXECUTION_RESULTS_TAB, + disabled: false, + dataTestSubj: 'executionResultsTab', + }, + { + id: RuleDetailTabs.executionEvents, + name: i18n.EXECUTION_EVENTS_TAB, disabled: false, - dataTestSubj: 'executionLogsTab', + dataTestSubj: 'executionEventsTab', }, ]; @@ -345,15 +356,23 @@ const RuleDetailsPageComponent: React.FC = ({ return null; }, [rule, spacesApi]); + const ruleExecutionSettings = useRuleExecutionSettings(); + useEffect(() => { + let visibleTabs = ruleDetailTabs; + let currentTab = RuleDetailTabs.alerts; + if (!hasIndexRead) { - setTabs(ruleDetailTabs.filter(({ id }) => id !== RuleDetailTabs.alerts)); - setRuleDetailTab(RuleDetailTabs.exceptions); - } else { - setTabs(ruleDetailTabs); - setRuleDetailTab(RuleDetailTabs.alerts); + visibleTabs = visibleTabs.filter(({ id }) => id !== RuleDetailTabs.alerts); + currentTab = RuleDetailTabs.exceptions; } - }, [hasIndexRead]); + if (!ruleExecutionSettings.extendedLogging.isEnabled) { + visibleTabs = visibleTabs.filter(({ id }) => id !== RuleDetailTabs.executionEvents); + } + + setTabs(visibleTabs); + setRuleDetailTab(currentTab); + }, [hasIndexRead, ruleExecutionSettings]); const showUpdating = useMemo( () => isLoadingIndexPattern || isAlertsLoading || loading, @@ -467,7 +486,12 @@ const RuleDetailsPageComponent: React.FC = ({ setRuleDetailTab(tab.id)} isSelected={tab.id === ruleDetailTab} - disabled={tab.disabled || (tab.id === RuleDetailTabs.executionLogs && !isExistingRule)} + disabled={ + tab.disabled || + ((tab.id === RuleDetailTabs.executionResults || + tab.id === RuleDetailTabs.executionEvents) && + !isExistingRule) + } key={tab.id} data-test-subj={tab.dataTestSubj} > @@ -850,9 +874,12 @@ const RuleDetailsPageComponent: React.FC = ({ onRuleChange={refreshRule} /> )} - {ruleDetailTab === RuleDetailTabs.executionLogs && ( + {ruleDetailTab === RuleDetailTabs.executionResults && ( )} + {ruleDetailTab === RuleDetailTabs.executionEvents && ( + + )} diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/rule_details_context.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/rule_details_context.tsx index f7f6f735069d1..58b34eb4a2bee 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/rule_details_context.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/rule_details_context.tsx @@ -10,9 +10,9 @@ import type { DurationRange } from '@elastic/eui/src/components/date_picker/type import React, { createContext, useContext, useMemo, useState } from 'react'; import { RULE_DETAILS_EXECUTION_LOG_TABLE_SHOW_METRIC_COLUMNS_STORAGE_KEY } from '../../../../../../common/constants'; import type { - AggregateRuleExecutionEvent, + RuleExecutionResult, RuleExecutionStatus, -} from '../../../../../../common/detection_engine/schemas/common'; +} from '../../../../../../common/detection_engine/rule_monitoring'; import { invariant } from '../../../../../../common/utils/invariant'; import { useKibana } from '../../../../../common/lib/kibana'; import { RuleDetailTabs } from '.'; @@ -63,7 +63,7 @@ export interface ExecutionLogTableState { pageSize: number; }; sort: { - sortField: keyof AggregateRuleExecutionEvent; + sortField: keyof RuleExecutionResult; sortDirection: SortOrder; }; } @@ -101,14 +101,14 @@ export interface ExecutionLogTableActions { setShowMetricColumns: React.Dispatch>; setPageIndex: React.Dispatch>; setPageSize: React.Dispatch>; - setSortField: React.Dispatch>; + setSortField: React.Dispatch>; setSortDirection: React.Dispatch>; } export interface RuleDetailsContextType { // TODO: Add section for RuleDetailTabs.exceptions and store query/pagination/etc. // TODO: Let's discuss how to integration with ExceptionsViewerComponent state mgmt - [RuleDetailTabs.executionLogs]: { + [RuleDetailTabs.executionResults]: { state: ExecutionLogTableState; actions: ExecutionLogTableActions; }; @@ -139,13 +139,13 @@ export const RuleDetailsContextProvider = ({ children }: RuleDetailsContextProvi // Pagination state const [pageIndex, setPageIndex] = useState(1); const [pageSize, setPageSize] = useState(5); - const [sortField, setSortField] = useState('timestamp'); + const [sortField, setSortField] = useState('timestamp'); const [sortDirection, setSortDirection] = useState('desc'); // // End Execution Log Table tab const providerValue = useMemo( () => ({ - [RuleDetailTabs.executionLogs]: { + [RuleDetailTabs.executionResults]: { state: { superDatePicker: { recentlyUsedRanges, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/translations.ts index e0c6f319f6e91..299c355ffa480 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/translations.ts @@ -35,17 +35,24 @@ export const UNKNOWN = i18n.translate( } ); -export const RULE_EXECUTION_LOGS = i18n.translate( - 'xpack.securitySolution.detectionEngine.ruleDetails.ruleExecutionLogsTab', +export const EXCEPTIONS_TAB = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDetails.exceptionsTab', { - defaultMessage: 'Rule execution logs ', + defaultMessage: 'Exceptions', } ); -export const EXCEPTIONS_TAB = i18n.translate( - 'xpack.securitySolution.detectionEngine.ruleDetails.exceptionsTab', +export const EXECUTION_RESULTS_TAB = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDetails.ruleExecutionResultsTab', { - defaultMessage: 'Exceptions', + defaultMessage: 'Execution results', + } +); + +export const EXECUTION_EVENTS_TAB = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDetails.ruleExecutionEventsTab', + { + defaultMessage: 'Execution events', } ); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx index be2cce0b1486c..fd1e8059d047b 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.test.tsx @@ -50,6 +50,8 @@ describe('rule helpers', () => { const defineRuleStepData = { ruleType: 'saved_query', anomalyThreshold: 50, + dataSourceType: 'indexPatterns', + dataViewId: undefined, index: ['auditbeat-*'], machineLearningJobId: [], queryBar: { @@ -215,6 +217,8 @@ describe('rule helpers', () => { const expected = { ruleType: 'saved_query', anomalyThreshold: 50, + dataSourceType: 'indexPatterns', + dataViewId: undefined, machineLearningJobId: [], index: ['auditbeat-*'], queryBar: { @@ -266,6 +270,8 @@ describe('rule helpers', () => { const expected = { ruleType: 'saved_query', anomalyThreshold: 50, + dataSourceType: 'indexPatterns', + dataViewId: undefined, machineLearningJobId: [], index: ['auditbeat-*'], queryBar: { diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx index 4ae39c29909d9..86a54e099e7b2 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx @@ -33,6 +33,7 @@ import type { ScheduleStepRule, ActionsStepRule, } from './types'; +import { DataSourceType } from './types'; import { severityOptions } from '../../../components/rules/step_about_rule/data'; export interface GetStepsData { @@ -120,6 +121,7 @@ export const getDefineStepsData = (rule: Rule): DefineStepRule => ({ eventCategoryField: rule.event_category_override, tiebreakerField: rule.tiebreaker_field, }, + dataSourceType: rule.data_view_id ? DataSourceType.DataView : DataSourceType.IndexPatterns, newTermsFields: rule.new_terms_fields ?? [], historyWindowSize: rule.history_window_start ? convertHistoryStartToSize(rule.history_window_start) diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts index 2ff8b84f469f4..ce4060dcf6e87 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts @@ -133,6 +133,11 @@ export interface AboutStepRiskScore { isMappingChecked: boolean; } +export enum DataSourceType { + IndexPatterns = 'indexPatterns', + DataView = 'dataView', +} + /** * add / update data source types to show XOR relationship between 'index' and 'dataViewId' fields * Maybe something with io-ts? @@ -153,6 +158,7 @@ export interface DefineStepRule { threatQueryBar: FieldValueQueryBar; threatMapping: ThreatMapping; eqlOptions: EqlOptionsSelected; + dataSourceType: DataSourceType; newTermsFields: string[]; historyWindowSize: string; } diff --git a/x-pack/plugins/security_solution/public/hosts/index.ts b/x-pack/plugins/security_solution/public/hosts/index.ts index d5f1aa0e895ae..395a2fc4a1db2 100644 --- a/x-pack/plugins/security_solution/public/hosts/index.ts +++ b/x-pack/plugins/security_solution/public/hosts/index.ts @@ -16,7 +16,6 @@ import { initialHostsState, hostsReducer } from './store'; const HOST_TIMELINE_IDS: TimelineIdLiteral[] = [ TimelineId.hostsPageEvents, - TimelineId.hostsPageExternalAlerts, TimelineId.hostsPageSessions, ]; diff --git a/x-pack/plugins/security_solution/public/hosts/links.ts b/x-pack/plugins/security_solution/public/hosts/links.ts index 2bf430d0c4def..744b33312436d 100644 --- a/x-pack/plugins/security_solution/public/hosts/links.ts +++ b/x-pack/plugins/security_solution/public/hosts/links.ts @@ -46,13 +46,6 @@ export const links: LinkItem = { }), path: `${HOSTS_PATH}/events`, }, - { - id: SecurityPageName.hostsExternalAlerts, - title: i18n.translate('xpack.securitySolution.appLinks.hosts.externalAlerts', { - defaultMessage: 'External Alerts', - }), - path: `${HOSTS_PATH}/externalAlerts`, - }, { id: SecurityPageName.hostsRisk, title: i18n.translate('xpack.securitySolution.appLinks.hosts.risk', { diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.test.tsx b/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.test.tsx index 4c68761dbe1b6..d6eb1de4e23ae 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.test.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.test.tsx @@ -74,7 +74,6 @@ describe('body', () => { [HostsTableType.uncommonProcesses]: 'UncommonProcessQueryTabBody', [HostsTableType.anomalies]: 'AnomaliesQueryTabBody', [HostsTableType.events]: 'EventsQueryTabBody', - [HostsTableType.alerts]: 'HostAlertsQueryTabBody', }; const mockHostDetailsPageFilters = getHostDetailsPageFilters('host-1'); diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.tsx b/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.tsx index 543ecc174fe1b..4036f59ad52b6 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useCallback } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { Switch } from 'react-router-dom'; import { Route } from '@kbn/kibana-react-plugin/public'; @@ -16,7 +16,8 @@ import { HostsTableType } from '../../store/model'; import { AnomaliesQueryTabBody } from '../../../common/containers/anomalies/anomalies_query_tab_body'; import { useGlobalTime } from '../../../common/containers/use_global_time'; import { AnomaliesHostTable } from '../../../common/components/ml/tables/anomalies_host_table'; -import { EventsQueryTabBody } from '../../../common/components/events_tab/events_query_tab_body'; +import { EventsQueryTabBody } from '../../../common/components/events_tab'; +import { hostNameExistsFilter } from '../../../common/components/visualization_actions/utils'; import type { HostDetailsTabsProps } from './types'; import { type } from './utils'; @@ -25,7 +26,6 @@ import { HostsQueryTabBody, AuthenticationsQueryTabBody, UncommonProcessQueryTabBody, - HostAlertsQueryTabBody, HostRiskTabBody, SessionsTabBody, } from '../navigation'; @@ -37,7 +37,7 @@ export const HostDetailsTabs = React.memo( filterQuery, indexNames, indexPattern, - pageFilters, + pageFilters = [], setAbsoluteRangeDatePicker, hostDetailsPagePath, }) => { @@ -84,6 +84,11 @@ export const HostDetailsTabs = React.memo( updateDateRange, }; + const externalAlertPageFilters = useMemo( + () => [...hostNameExistsFilter, ...pageFilters], + [pageFilters] + ); + return ( @@ -104,11 +109,9 @@ export const HostDetailsTabs = React.memo( {...tabProps} pageFilters={pageFilters} timelineId={TimelineId.hostsPageEvents} + externalAlertPageFilters={externalAlertPageFilters} /> - - - diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/nav_tabs.tsx b/x-pack/plugins/security_solution/public/hosts/pages/details/nav_tabs.tsx index 1ad3bc5dad232..bb0856bd07ecb 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/details/nav_tabs.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/details/nav_tabs.tsx @@ -50,12 +50,6 @@ export const navTabsHostDetails = ({ href: getTabsOnHostDetailsUrl(hostName, HostsTableType.events), disabled: false, }, - [HostsTableType.alerts]: { - id: HostsTableType.alerts, - name: i18n.NAVIGATION_ALERTS_TITLE, - href: getTabsOnHostDetailsUrl(hostName, HostsTableType.alerts), - disabled: false, - }, [HostsTableType.risk]: { id: HostsTableType.risk, name: i18n.NAVIGATION_HOST_RISK_TITLE, diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/utils.ts b/x-pack/plugins/security_solution/public/hosts/pages/details/utils.ts index 5e9614bc05d74..631905cbadb44 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/details/utils.ts +++ b/x-pack/plugins/security_solution/public/hosts/pages/details/utils.ts @@ -25,7 +25,6 @@ const TabNameMappedToI18nKey: Record = { [HostsTableType.uncommonProcesses]: i18n.NAVIGATION_UNCOMMON_PROCESSES_TITLE, [HostsTableType.anomalies]: i18n.NAVIGATION_ANOMALIES_TITLE, [HostsTableType.events]: i18n.NAVIGATION_EVENTS_TITLE, - [HostsTableType.alerts]: i18n.NAVIGATION_ALERTS_TITLE, [HostsTableType.risk]: i18n.NAVIGATION_HOST_RISK_TITLE, [HostsTableType.sessions]: i18n.NAVIGATION_SESSIONS_TITLE, }; diff --git a/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx b/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx index a6d45234a18c0..1b04e0c63edac 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx @@ -74,12 +74,7 @@ const HostsComponent = () => { const containerElement = useRef(null); const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); const graphEventId = useShallowEqualSelector( - (state) => - ( - getTimeline(state, TimelineId.hostsPageEvents) ?? - getTimeline(state, TimelineId.hostsPageExternalAlerts) ?? - timelineDefaults - ).graphEventId + (state) => (getTimeline(state, TimelineId.hostsPageEvents) ?? timelineDefaults).graphEventId ); const getGlobalFiltersQuerySelector = useMemo( () => inputsSelectors.globalFiltersQuerySelector(), @@ -102,7 +97,7 @@ const HostsComponent = () => { const { uiSettings } = useKibana().services; const { tabName } = useParams<{ tabName: string }>(); const tabsFilters: Filter[] = React.useMemo(() => { - if (tabName === HostsTableType.alerts || tabName === HostsTableType.events) { + if (tabName === HostsTableType.events) { return filters.length > 0 ? [...filters, ...hostNameExistsFilter] : hostNameExistsFilter; } diff --git a/x-pack/plugins/security_solution/public/hosts/pages/hosts_tabs.tsx b/x-pack/plugins/security_solution/public/hosts/pages/hosts_tabs.tsx index 7dc106fa80586..a06aa4b0aadc3 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/hosts_tabs.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/hosts_tabs.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { memo, useCallback } from 'react'; +import React, { memo, useCallback, useMemo } from 'react'; import { Switch } from 'react-router-dom'; import { Route } from '@kbn/kibana-react-plugin/public'; @@ -16,7 +16,7 @@ import { HostsTableType } from '../store/model'; import { AnomaliesQueryTabBody } from '../../common/containers/anomalies/anomalies_query_tab_body'; import { AnomaliesHostTable } from '../../common/components/ml/tables/anomalies_host_table'; import type { UpdateDateRange } from '../../common/components/charts/common'; -import { EventsQueryTabBody } from '../../common/components/events_tab/events_query_tab_body'; +import { EventsQueryTabBody } from '../../common/components/events_tab'; import { HOSTS_PATH } from '../../../common/constants'; import { @@ -25,14 +25,14 @@ import { UncommonProcessQueryTabBody, SessionsTabBody, } from './navigation'; -import { HostAlertsQueryTabBody } from './navigation/alerts_query_tab_body'; import { TimelineId } from '../../../common/types'; +import { hostNameExistsFilter } from '../../common/components/visualization_actions/utils'; export const HostsTabs = memo( ({ deleteQuery, filterQuery, - pageFilters, + pageFilters = [], from, indexNames, isInitializing, @@ -81,6 +81,10 @@ export const HostsTabs = memo( updateDateRange, }; + const externalAlertPageFilters = useMemo( + () => [...hostNameExistsFilter, ...pageFilters], + [pageFilters] + ); return ( @@ -98,13 +102,11 @@ export const HostsTabs = memo( - - - diff --git a/x-pack/plugins/security_solution/public/hosts/pages/index.tsx b/x-pack/plugins/security_solution/public/hosts/pages/index.tsx index 8b45cf11aa604..105ab3e6e4157 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/index.tsx @@ -23,7 +23,6 @@ const getHostsTabPath = () => `${HostsTableType.anomalies}|` + `${HostsTableType.events}|` + `${HostsTableType.risk}|` + - `${HostsTableType.alerts}|` + `${HostsTableType.sessions})`; const getHostDetailsTabPath = () => @@ -33,7 +32,6 @@ const getHostDetailsTabPath = () => `${HostsTableType.anomalies}|` + `${HostsTableType.events}|` + `${HostsTableType.risk}|` + - `${HostsTableType.alerts}|` + `${HostsTableType.sessions})`; export const HostsContainer = React.memo(() => ( diff --git a/x-pack/plugins/security_solution/public/hosts/pages/nav_tabs.tsx b/x-pack/plugins/security_solution/public/hosts/pages/nav_tabs.tsx index 74b2a5bd0b442..e8ec705bd84d9 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/nav_tabs.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/nav_tabs.tsx @@ -46,12 +46,6 @@ export const navTabsHosts = ({ href: getTabsOnHostsUrl(HostsTableType.events), disabled: false, }, - [HostsTableType.alerts]: { - id: HostsTableType.alerts, - name: i18n.NAVIGATION_ALERTS_TITLE, - href: getTabsOnHostsUrl(HostsTableType.alerts), - disabled: false, - }, [HostsTableType.risk]: { id: HostsTableType.risk, name: i18n.NAVIGATION_HOST_RISK_TITLE, diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/alerts_query_tab_body.tsx b/x-pack/plugins/security_solution/public/hosts/pages/navigation/alerts_query_tab_body.tsx deleted file mode 100644 index 5a6e6e647ce0e..0000000000000 --- a/x-pack/plugins/security_solution/public/hosts/pages/navigation/alerts_query_tab_body.tsx +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useMemo } from 'react'; - -import { TimelineId } from '../../../../common/types/timeline'; -import { AlertsView } from '../../../common/components/alerts_viewer'; -import { hostNameExistsFilter } from '../../../common/components/visualization_actions/utils'; -import type { AlertsComponentQueryProps } from './types'; - -export const HostAlertsQueryTabBody = React.memo((alertsProps: AlertsComponentQueryProps) => { - const { pageFilters, ...rest } = alertsProps; - const hostPageFilters = useMemo( - () => (pageFilters != null ? [...hostNameExistsFilter, ...pageFilters] : hostNameExistsFilter), - [pageFilters] - ); - - return ( - - ); -}); - -HostAlertsQueryTabBody.displayName = 'HostAlertsQueryTabBody'; diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/index.ts b/x-pack/plugins/security_solution/public/hosts/pages/navigation/index.ts index 5ebbc61cabe41..211b19d190f62 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/navigation/index.ts +++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/index.ts @@ -8,7 +8,6 @@ export * from './authentications_query_tab_body'; export * from './hosts_query_tab_body'; export * from './uncommon_process_query_tab_body'; -export * from './alerts_query_tab_body'; export * from './host_risk_tab_body'; export * from './host_risk_score_tab_body'; export * from './sessions_tab_body'; diff --git a/x-pack/plugins/security_solution/public/hosts/pages/translations.ts b/x-pack/plugins/security_solution/public/hosts/pages/translations.ts index b2ac1611087c6..bc0eb450d4fea 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/translations.ts +++ b/x-pack/plugins/security_solution/public/hosts/pages/translations.ts @@ -46,13 +46,6 @@ export const NAVIGATION_EVENTS_TITLE = i18n.translate( } ); -export const NAVIGATION_ALERTS_TITLE = i18n.translate( - 'xpack.securitySolution.hosts.navigation.alertsTitle', - { - defaultMessage: 'External alerts', - } -); - export const NAVIGATION_HOST_RISK_TITLE = i18n.translate( 'xpack.securitySolution.hosts.navigation.hostRiskTitle', { @@ -74,12 +67,6 @@ export const ERROR_FETCHING_EVENTS_DATA = i18n.translate( } ); -export const EVENTS_UNIT = (totalCount: number) => - i18n.translate('xpack.securitySolution.hosts.navigaton.eventsUnit', { - values: { totalCount }, - defaultMessage: `{totalCount, plural, =1 {event} other {events}}`, - }); - export const VIEW_DASHBOARD_BUTTON = i18n.translate( 'xpack.securitySolution.hosts.navigaton.hostRisk.viewDashboardButtonLabel', { diff --git a/x-pack/plugins/security_solution/public/hosts/store/helpers.test.ts b/x-pack/plugins/security_solution/public/hosts/store/helpers.test.ts index bf2887267623d..186b48f64e40e 100644 --- a/x-pack/plugins/security_solution/public/hosts/store/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/hosts/store/helpers.test.ts @@ -33,10 +33,6 @@ export const mockHostsState: HostsModel = { limit: DEFAULT_TABLE_LIMIT, }, [HostsTableType.anomalies]: null, - [HostsTableType.alerts]: { - activePage: 4, - limit: DEFAULT_TABLE_LIMIT, - }, [HostsTableType.risk]: { activePage: DEFAULT_TABLE_ACTIVE_PAGE, limit: DEFAULT_TABLE_LIMIT, @@ -73,10 +69,6 @@ export const mockHostsState: HostsModel = { limit: DEFAULT_TABLE_LIMIT, }, [HostsTableType.anomalies]: null, - [HostsTableType.alerts]: { - activePage: 4, - limit: DEFAULT_TABLE_LIMIT, - }, [HostsTableType.risk]: { activePage: DEFAULT_TABLE_ACTIVE_PAGE, limit: DEFAULT_TABLE_LIMIT, @@ -117,10 +109,6 @@ describe('Hosts redux store', () => { activePage: 0, limit: 10, }, - [HostsTableType.alerts]: { - activePage: 0, - limit: 10, - }, [HostsTableType.risk]: { activePage: 0, limit: 10, @@ -158,10 +146,6 @@ describe('Hosts redux store', () => { activePage: 0, limit: 10, }, - [HostsTableType.alerts]: { - activePage: 0, - limit: 10, - }, [HostsTableType.risk]: { activePage: 0, limit: 10, diff --git a/x-pack/plugins/security_solution/public/hosts/store/helpers.ts b/x-pack/plugins/security_solution/public/hosts/store/helpers.ts index 7133414ff8f31..6093a2c72a3a9 100644 --- a/x-pack/plugins/security_solution/public/hosts/store/helpers.ts +++ b/x-pack/plugins/security_solution/public/hosts/store/helpers.ts @@ -29,10 +29,6 @@ export const setHostPageQueriesActivePageToZero = (state: HostsModel): Queries = ...state.page.queries[HostsTableType.uncommonProcesses], activePage: DEFAULT_TABLE_ACTIVE_PAGE, }, - [HostsTableType.alerts]: { - ...state.page.queries[HostsTableType.alerts], - activePage: DEFAULT_TABLE_ACTIVE_PAGE, - }, }); export const setHostDetailsQueriesActivePageToZero = (state: HostsModel): Queries => ({ @@ -53,10 +49,6 @@ export const setHostDetailsQueriesActivePageToZero = (state: HostsModel): Querie ...state.details.queries[HostsTableType.uncommonProcesses], activePage: DEFAULT_TABLE_ACTIVE_PAGE, }, - [HostsTableType.alerts]: { - ...state.page.queries[HostsTableType.alerts], - activePage: DEFAULT_TABLE_ACTIVE_PAGE, - }, }); export const setHostsQueriesActivePageToZero = (state: HostsModel, type: HostsType): Queries => { diff --git a/x-pack/plugins/security_solution/public/hosts/store/model.ts b/x-pack/plugins/security_solution/public/hosts/store/model.ts index 381c25c79ae1b..bd62e7cb62c2f 100644 --- a/x-pack/plugins/security_solution/public/hosts/store/model.ts +++ b/x-pack/plugins/security_solution/public/hosts/store/model.ts @@ -23,7 +23,6 @@ export enum HostsTableType { events = 'events', uncommonProcesses = 'uncommonProcesses', anomalies = 'anomalies', - alerts = 'externalAlerts', risk = 'hostRisk', sessions = 'sessions', } @@ -49,7 +48,6 @@ export interface Queries { [HostsTableType.events]: BasicQueryPaginated; [HostsTableType.uncommonProcesses]: BasicQueryPaginated; [HostsTableType.anomalies]: null | undefined; - [HostsTableType.alerts]: BasicQueryPaginated; [HostsTableType.risk]: HostRiskScoreQuery; [HostsTableType.sessions]: BasicQueryPaginated; } diff --git a/x-pack/plugins/security_solution/public/hosts/store/reducer.ts b/x-pack/plugins/security_solution/public/hosts/store/reducer.ts index 7d7e8b445b3e6..cf323795fa431 100644 --- a/x-pack/plugins/security_solution/public/hosts/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/hosts/store/reducer.ts @@ -50,10 +50,6 @@ export const initialHostsState: HostsState = { limit: DEFAULT_TABLE_LIMIT, }, [HostsTableType.anomalies]: null, - [HostsTableType.alerts]: { - activePage: DEFAULT_TABLE_ACTIVE_PAGE, - limit: DEFAULT_TABLE_LIMIT, - }, [HostsTableType.risk]: { activePage: DEFAULT_TABLE_ACTIVE_PAGE, limit: DEFAULT_TABLE_LIMIT, @@ -90,10 +86,6 @@ export const initialHostsState: HostsState = { limit: DEFAULT_TABLE_LIMIT, }, [HostsTableType.anomalies]: null, - [HostsTableType.alerts]: { - activePage: DEFAULT_TABLE_ACTIVE_PAGE, - limit: DEFAULT_TABLE_LIMIT, - }, [HostsTableType.risk]: { activePage: DEFAULT_TABLE_ACTIVE_PAGE, limit: DEFAULT_TABLE_LIMIT, diff --git a/x-pack/plugins/security_solution/public/landing_pages/links.ts b/x-pack/plugins/security_solution/public/landing_pages/links.ts index 5a9735480bc14..773e18b0271d8 100644 --- a/x-pack/plugins/security_solution/public/landing_pages/links.ts +++ b/x-pack/plugins/security_solution/public/landing_pages/links.ts @@ -20,6 +20,7 @@ import { links as networkLinks } from '../network/links'; import { links as usersLinks } from '../users/links'; import { links as kubernetesLinks } from '../kubernetes/links'; import { dashboardLinks as cloudSecurityPostureLinks } from '../cloud_security_posture/links'; +import { links as threatIntelligenceLinks } from '../threat_intelligence/links'; export const dashboardsLandingLinks: LinkItem = { id: SecurityPageName.dashboardsLanding, @@ -50,7 +51,7 @@ export const threatHuntingLandingLinks: LinkItem = { defaultMessage: 'Explore', }), ], - links: [hostsLinks, networkLinks, usersLinks], + links: [hostsLinks, networkLinks, usersLinks, threatIntelligenceLinks], skipUrlState: true, hideTimeline: true, }; diff --git a/x-pack/plugins/security_solution/public/lazy_sub_plugins.tsx b/x-pack/plugins/security_solution/public/lazy_sub_plugins.tsx index 88b063dd037be..c6b8d37a5342d 100644 --- a/x-pack/plugins/security_solution/public/lazy_sub_plugins.tsx +++ b/x-pack/plugins/security_solution/public/lazy_sub_plugins.tsx @@ -13,19 +13,17 @@ import { Cases } from './cases'; import { Detections } from './detections'; import { Exceptions } from './exceptions'; - import { Hosts } from './hosts'; import { Users } from './users'; import { Network } from './network'; import { Kubernetes } from './kubernetes'; import { Overview } from './overview'; import { Rules } from './rules'; - import { Timelines } from './timelines'; import { Management } from './management'; import { LandingPages } from './landing_pages'; - import { CloudSecurityPosture } from './cloud_security_posture'; +import { ThreatIntelligence } from './threat_intelligence'; /** * The classes used to instantiate the sub plugins. These are grouped into a single object for the sake of bundling them in a single dynamic import. @@ -38,12 +36,12 @@ const subPluginClasses = { Users, Network, Kubernetes, - Overview, Rules, Timelines, Management, LandingPages, CloudSecurityPosture, + ThreatIntelligence, }; export { subPluginClasses }; diff --git a/x-pack/plugins/security_solution/public/network/index.ts b/x-pack/plugins/security_solution/public/network/index.ts index e3ec6d650529f..51a2ca91cde8d 100644 --- a/x-pack/plugins/security_solution/public/network/index.ts +++ b/x-pack/plugins/security_solution/public/network/index.ts @@ -20,7 +20,7 @@ export class Network { return { routes, storageTimelines: { - timelineById: getTimelinesInStorageByIds(storage, [TimelineId.networkPageExternalAlerts]), + timelineById: getTimelinesInStorageByIds(storage, [TimelineId.networkPageEvents]), }, store: { initialState: { network: initialNetworkState }, diff --git a/x-pack/plugins/security_solution/public/network/links.ts b/x-pack/plugins/security_solution/public/network/links.ts index 221d46a2f78ee..9490d88f77694 100644 --- a/x-pack/plugins/security_solution/public/network/links.ts +++ b/x-pack/plugins/security_solution/public/network/links.ts @@ -47,13 +47,6 @@ export const links: LinkItem = { }), path: `${NETWORK_PATH}/tls`, }, - { - id: SecurityPageName.networkExternalAlerts, - title: i18n.translate('xpack.securitySolution.appLinks.network.externalAlerts', { - defaultMessage: 'External Alerts', - }), - path: `${NETWORK_PATH}/external-alerts`, - }, { id: SecurityPageName.networkAnomalies, title: i18n.translate('xpack.securitySolution.appLinks.hosts.anomalies', { diff --git a/x-pack/plugins/security_solution/public/network/pages/details/utils.ts b/x-pack/plugins/security_solution/public/network/pages/details/utils.ts index 8f8daa88ff9ab..ff8d44a07ef17 100644 --- a/x-pack/plugins/security_solution/public/network/pages/details/utils.ts +++ b/x-pack/plugins/security_solution/public/network/pages/details/utils.ts @@ -19,7 +19,7 @@ import type { GetSecuritySolutionUrl } from '../../../common/components/link_to' export const type = networkModel.NetworkType.details; const TabNameMappedToI18nKey: Record = { - [NetworkRouteType.alerts]: i18n.NAVIGATION_ALERTS_TITLE, + [NetworkRouteType.alerts]: i18n.NAVIGATION_EVENTS_TITLE, [NetworkRouteType.anomalies]: i18n.NAVIGATION_ANOMALIES_TITLE, [NetworkRouteType.flows]: i18n.NAVIGATION_FLOWS_TITLE, [NetworkRouteType.dns]: i18n.NAVIGATION_DNS_TITLE, diff --git a/x-pack/plugins/security_solution/public/network/pages/navigation/alerts_query_tab_body.tsx b/x-pack/plugins/security_solution/public/network/pages/navigation/alerts_query_tab_body.tsx deleted file mode 100644 index b469d8624aa7a..0000000000000 --- a/x-pack/plugins/security_solution/public/network/pages/navigation/alerts_query_tab_body.tsx +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; - -import { TimelineId } from '../../../../common/types/timeline'; -import { AlertsView } from '../../../common/components/alerts_viewer'; -import type { NetworkComponentQueryProps } from './types'; -import { filterNetworkExternalAlertData } from '../../../common/components/visualization_actions/utils'; - -export const NetworkAlertsQueryTabBody = React.memo((alertsProps: NetworkComponentQueryProps) => ( - -)); - -NetworkAlertsQueryTabBody.displayName = 'NetworkAlertsQueryTabBody'; diff --git a/x-pack/plugins/security_solution/public/network/pages/navigation/index.ts b/x-pack/plugins/security_solution/public/network/pages/navigation/index.ts index 4c91731183ed7..bdc3d732b8e92 100644 --- a/x-pack/plugins/security_solution/public/network/pages/navigation/index.ts +++ b/x-pack/plugins/security_solution/public/network/pages/navigation/index.ts @@ -9,7 +9,6 @@ export * from './network_routes'; export * from './network_routes_loading'; export * from './nav_tabs'; export * from './utils'; -export * from './alerts_query_tab_body'; export * from './countries_query_tab_body'; export * from './dns_query_tab_body'; export * from './http_query_tab_body'; diff --git a/x-pack/plugins/security_solution/public/network/pages/navigation/nav_tabs.tsx b/x-pack/plugins/security_solution/public/network/pages/navigation/nav_tabs.tsx index 62a0541fed55b..d1992c756cc67 100644 --- a/x-pack/plugins/security_solution/public/network/pages/navigation/nav_tabs.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/navigation/nav_tabs.tsx @@ -47,7 +47,7 @@ export const navTabsNetwork = (hasMlUserPermissions: boolean): NetworkNavTab => }, [NetworkRouteType.alerts]: { id: NetworkRouteType.alerts, - name: i18n.NAVIGATION_ALERTS_TITLE, + name: i18n.NAVIGATION_EVENTS_TITLE, href: getTabsOnNetworkUrl(NetworkRouteType.alerts), disabled: false, }, diff --git a/x-pack/plugins/security_solution/public/network/pages/navigation/network_routes.tsx b/x-pack/plugins/security_solution/public/network/pages/navigation/network_routes.tsx index a91b47aac32d9..c98ddacb6122b 100644 --- a/x-pack/plugins/security_solution/public/network/pages/navigation/network_routes.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/navigation/network_routes.tsx @@ -18,11 +18,13 @@ import { DnsQueryTabBody, HttpQueryTabBody, IPsQueryTabBody, - NetworkAlertsQueryTabBody, TlsQueryTabBody, } from '.'; -import { AnomaliesQueryTabBody } from '../../../common/containers/anomalies/anomalies_query_tab_body'; +import { EventsQueryTabBody } from '../../../common/components/events_tab'; import { AnomaliesNetworkTable } from '../../../common/components/ml/tables/anomalies_network_table'; +import { filterNetworkExternalAlertData } from '../../../common/components/visualization_actions/utils'; +import { AnomaliesQueryTabBody } from '../../../common/containers/anomalies/anomalies_query_tab_body'; +import { TimelineId } from '../../../../common/types'; import { ConditionalFlexGroup } from './conditional_flex_group'; import type { NetworkRoutesProps } from './types'; import { NetworkRouteType } from './types'; @@ -154,7 +156,11 @@ export const NetworkRoutes = React.memo( /> - + ); diff --git a/x-pack/plugins/security_solution/public/network/pages/navigation/types.ts b/x-pack/plugins/security_solution/public/network/pages/navigation/types.ts index 0299d33ea603a..4e010e91478dc 100644 --- a/x-pack/plugins/security_solution/public/network/pages/navigation/types.ts +++ b/x-pack/plugins/security_solution/public/network/pages/navigation/types.ts @@ -17,7 +17,7 @@ import type { GlobalTimeArgs } from '../../../common/containers/use_global_time' import type { SetAbsoluteRangeDatePicker } from '../types'; import type { DocValueFields } from '../../../common/containers/source'; -interface QueryTabBodyProps extends Pick { +export interface QueryTabBodyProps extends Pick { endDate: string; filterQuery?: string | ESTermQuery; indexNames: string[]; @@ -62,7 +62,7 @@ export enum NetworkRouteType { anomalies = 'anomalies', tls = 'tls', http = 'http', - alerts = 'external-alerts', + alerts = 'events', // changed officially to events in #136427 } export type KeyNetworkNavTabWithoutMlPermission = NetworkRouteType.dns & diff --git a/x-pack/plugins/security_solution/public/network/pages/network.tsx b/x-pack/plugins/security_solution/public/network/pages/network.tsx index 3773ddb9aaaa2..b117624782af8 100644 --- a/x-pack/plugins/security_solution/public/network/pages/network.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/network.tsx @@ -69,8 +69,7 @@ const NetworkComponent = React.memo( const containerElement = useRef(null); const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); const graphEventId = useShallowEqualSelector( - (state) => - (getTimeline(state, TimelineId.networkPageExternalAlerts) ?? timelineDefaults).graphEventId + (state) => (getTimeline(state, TimelineId.networkPageEvents) ?? timelineDefaults).graphEventId ); const getGlobalFiltersQuerySelector = useMemo( () => inputsSelectors.globalFiltersQuerySelector(), diff --git a/x-pack/plugins/security_solution/public/network/pages/translations.ts b/x-pack/plugins/security_solution/public/network/pages/translations.ts index d461e45ab4534..550afe94f811f 100644 --- a/x-pack/plugins/security_solution/public/network/pages/translations.ts +++ b/x-pack/plugins/security_solution/public/network/pages/translations.ts @@ -46,9 +46,9 @@ export const NAVIGATION_ANOMALIES_TITLE = i18n.translate( } ); -export const NAVIGATION_ALERTS_TITLE = i18n.translate( - 'xpack.securitySolution.network.navigation.alertsTitle', +export const NAVIGATION_EVENTS_TITLE = i18n.translate( + 'xpack.securitySolution.network.navigation.eventsTitle', { - defaultMessage: 'External alerts', + defaultMessage: 'Events', } ); diff --git a/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.test.tsx deleted file mode 100644 index 8d6aa6d6dc86a..0000000000000 --- a/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.test.tsx +++ /dev/null @@ -1,353 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { ReactWrapper } from 'enzyme'; -import { mount } from 'enzyme'; -import React from 'react'; - -import '../../../common/mock/match_media'; -import '../../../common/mock/react_beautiful_dnd'; -import { useMatrixHistogramCombined } from '../../../common/containers/matrix_histogram'; -import { waitFor } from '@testing-library/react'; -import { mockIndexPattern, TestProviders } from '../../../common/mock'; - -import { AlertsByCategory } from '.'; -import { mockCasesContext } from '@kbn/cases-plugin/public/mocks/mock_cases_context'; -import { useRouteSpy } from '../../../common/utils/route/use_route_spy'; - -jest.mock('../../../common/components/link_to'); -jest.mock('../../../common/components/visualization_actions', () => ({ - VisualizationActions: jest.fn(() =>
), -})); - -jest.mock('../../../common/containers/matrix_histogram', () => ({ - useMatrixHistogramCombined: jest.fn(), -})); - -jest.mock('../../../common/lib/kibana', () => { - const original = jest.requireActual('../../../common/lib/kibana'); - - return { - ...original, - useKibana: () => ({ - services: { - ...original.useKibana().services, - cases: { - ui: { - getCasesContext: jest.fn().mockReturnValue(mockCasesContext), - }, - }, - }, - }), - }; -}); - -jest.mock('../../../common/utils/route/use_route_spy', () => ({ - useRouteSpy: jest.fn().mockReturnValue([ - { - detailName: 'mockHost', - pageName: 'hosts', - tabName: 'externalAlerts', - }, - ]), -})); - -const from = '2020-03-31T06:00:00.000Z'; -const to = '2019-03-31T06:00:00.000Z'; - -describe('Alerts by category', () => { - let wrapper: ReactWrapper; - const testProps = { - deleteQuery: jest.fn(), - filters: [], - from, - indexNames: [], - indexPattern: mockIndexPattern, - setQuery: jest.fn(), - to, - query: { - query: '', - language: 'kuery', - }, - }; - describe('before loading data', () => { - beforeAll(async () => { - (useMatrixHistogramCombined as jest.Mock).mockReturnValue([ - false, - { - data: null, - inspect: false, - totalCount: null, - }, - ]); - - wrapper = mount( - - - - ); - - await waitFor(() => { - wrapper.update(); - }); - }); - - test('it renders the expected title', async () => { - await waitFor(() => { - expect(wrapper.find('[data-test-subj="header-section-title"]').first().text()).toEqual( - 'External alert trend' - ); - }); - }); - - test('it renders the subtitle (to prevent layout thrashing)', async () => { - await waitFor(() => { - expect(wrapper.find('[data-test-subj="header-panel-subtitle"]').exists()).toBe(true); - }); - }); - - test('it renders the expected filter fields', async () => { - await waitFor(() => { - const expectedOptions = ['event.category', 'event.module']; - - expectedOptions.forEach((option) => { - expect(wrapper.find(`option[value="${option}"]`).text()).toEqual(option); - }); - }); - }); - - test('it renders the `View alerts` button', async () => { - await waitFor(() => { - expect(wrapper.find('[data-test-subj="view-alerts"]').exists()).toBe(true); - }); - }); - - test('it does NOT render the bar chart when data is not available', async () => { - await waitFor(() => { - expect(wrapper.find(`.echChart`).exists()).toBe(false); - }); - }); - }); - - describe('after loading data', () => { - beforeAll(async () => { - (useMatrixHistogramCombined as jest.Mock).mockReturnValue([ - false, - { - data: [ - { x: 1, y: 2, g: 'g1' }, - { x: 2, y: 4, g: 'g1' }, - { x: 3, y: 6, g: 'g1' }, - { x: 1, y: 1, g: 'g2' }, - { x: 2, y: 3, g: 'g2' }, - { x: 3, y: 5, g: 'g2' }, - ], - inspect: false, - totalCount: 6, - }, - ]); - - wrapper = mount( - - - - ); - - wrapper.update(); - }); - - test('it renders the expected subtitle', async () => { - await waitFor(() => { - expect(wrapper.find('[data-test-subj="header-panel-subtitle"]').text()).toEqual( - 'Showing: 6 external alerts' - ); - }); - }); - - test('it renders the bar chart when data is available', async () => { - await waitFor(() => { - expect(wrapper.find(`.echChart`).exists()).toBe(true); - }); - }); - - test('it shows visualization actions on host page', async () => { - await waitFor(() => { - expect(wrapper.find('[data-test-subj="mock-viz-actions"]').exists()).toBe(true); - }); - }); - - test('it shows visualization actions on network page', async () => { - (useRouteSpy as jest.Mock).mockReturnValue([ - { - detailName: undefined, - pageName: 'network', - tabName: 'external-alerts', - }, - ]); - - const testWrapper = mount( - - - - ); - - await waitFor(() => { - testWrapper.update(); - }); - await waitFor(() => { - expect(testWrapper.find('[data-test-subj="mock-viz-actions"]').exists()).toBe(true); - }); - }); - - test('it does not shows visualization actions on other pages', async () => { - (useRouteSpy as jest.Mock).mockReturnValue([ - { - detailName: undefined, - pageName: 'overview', - tabName: undefined, - }, - ]); - const testWrapper = mount( - - - - ); - - await waitFor(() => { - testWrapper.update(); - }); - - await waitFor(() => { - expect(testWrapper.find('[data-test-subj="mock-viz-actions"]').exists()).toBe(false); - }); - }); - }); - - describe('Host page', () => { - beforeAll(async () => { - (useRouteSpy as jest.Mock).mockReturnValue([ - { - detailName: 'mockHost', - pageName: 'hosts', - tabName: 'externalAlerts', - }, - ]); - - (useMatrixHistogramCombined as jest.Mock).mockReturnValue([ - false, - { - data: [ - { x: 1, y: 2, g: 'g1' }, - { x: 2, y: 4, g: 'g1' }, - { x: 3, y: 6, g: 'g1' }, - { x: 1, y: 1, g: 'g2' }, - { x: 2, y: 3, g: 'g2' }, - { x: 3, y: 5, g: 'g2' }, - ], - inspect: false, - totalCount: 6, - }, - ]); - - wrapper = mount( - - - - ); - - wrapper.update(); - }); - - test('it shows visualization actions', async () => { - await waitFor(() => { - expect(wrapper.find('[data-test-subj="mock-viz-actions"]').exists()).toBe(true); - }); - }); - }); - - describe('Network page', () => { - beforeAll(async () => { - (useRouteSpy as jest.Mock).mockReturnValue([ - { - detailName: undefined, - pageName: 'network', - tabName: 'external-alerts', - }, - ]); - (useMatrixHistogramCombined as jest.Mock).mockReturnValue([ - false, - { - data: [ - { x: 1, y: 2, g: 'g1' }, - { x: 2, y: 4, g: 'g1' }, - { x: 3, y: 6, g: 'g1' }, - { x: 1, y: 1, g: 'g2' }, - { x: 2, y: 3, g: 'g2' }, - { x: 3, y: 5, g: 'g2' }, - ], - inspect: false, - totalCount: 6, - }, - ]); - - wrapper = mount( - - - - ); - - wrapper.update(); - }); - - test('it shows visualization actions', async () => { - await waitFor(() => { - expect(wrapper.find('[data-test-subj="mock-viz-actions"]').exists()).toBe(true); - }); - }); - }); - - describe('Othen than Host or Network page', () => { - beforeAll(async () => { - (useRouteSpy as jest.Mock).mockReturnValue([ - { - detailName: undefined, - pageName: 'overview', - tabName: undefined, - }, - ]); - (useMatrixHistogramCombined as jest.Mock).mockReturnValue([ - false, - { - data: [ - { x: 1, y: 2, g: 'g1' }, - { x: 2, y: 4, g: 'g1' }, - { x: 3, y: 6, g: 'g1' }, - { x: 1, y: 1, g: 'g2' }, - { x: 2, y: 3, g: 'g2' }, - { x: 3, y: 5, g: 'g2' }, - ], - inspect: false, - totalCount: 6, - }, - ]); - - wrapper = mount( - - - - ); - - wrapper.update(); - }); - - test('it does not shows visualization actions', async () => { - await waitFor(() => { - expect(wrapper.find('[data-test-subj="mock-viz-actions"]').exists()).toBe(false); - }); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.tsx b/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.tsx deleted file mode 100644 index 93a70f75d0f7f..0000000000000 --- a/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.tsx +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import numeral from '@elastic/numeral'; -import React, { useEffect, useMemo, useCallback } from 'react'; -import { Position } from '@elastic/charts'; -import styled from 'styled-components'; - -import type { DataViewBase, Filter, Query } from '@kbn/es-query'; -import { EuiButton } from '@elastic/eui'; -import { getEsQueryConfig } from '@kbn/data-plugin/common'; -import { DEFAULT_NUMBER_FORMAT, APP_UI_ID } from '../../../../common/constants'; -import { SHOWING, UNIT } from '../../../common/components/alerts_viewer/translations'; -import { MatrixHistogram } from '../../../common/components/matrix_histogram'; -import { useKibana, useUiSetting$ } from '../../../common/lib/kibana'; -import { convertToBuildEsQuery } from '../../../common/lib/keury'; -import { HostsTableType } from '../../../hosts/store/model'; - -import * as i18n from '../../pages/translations'; -import { - alertsStackByOptions, - histogramConfigs, -} from '../../../common/components/alerts_viewer/histogram_configs'; -import type { MatrixHistogramConfigs } from '../../../common/components/matrix_histogram/types'; -import { getTabsOnHostsUrl } from '../../../common/components/link_to/redirect_to_hosts'; -import type { GlobalTimeArgs } from '../../../common/containers/use_global_time'; -import { SecurityPageName } from '../../../app/types'; -import { useFormatUrl } from '../../../common/components/link_to'; -import { useInvalidFilterQuery } from '../../../common/hooks/use_invalid_filter_query'; - -const ID = 'alertsByCategoryOverview'; - -const DEFAULT_STACK_BY = 'event.module'; - -const StyledLinkButton = styled(EuiButton)` - margin-left: 0; - @media only screen and (min-width: ${(props) => props.theme.eui.euiBreakpoints.m}) { - margin-left: ${({ theme }) => theme.eui.euiSizeL}; - } -`; -interface Props extends Pick { - filters: Filter[]; - hideHeaderChildren?: boolean; - indexPattern: DataViewBase; - indexNames: string[]; - query: Query; -} - -const AlertsByCategoryComponent: React.FC = ({ - deleteQuery, - filters, - from, - hideHeaderChildren = false, - indexPattern, - indexNames, - query, - setQuery, - to, -}) => { - const { - uiSettings, - application: { navigateToApp }, - } = useKibana().services; - const { formatUrl, search: urlSearch } = useFormatUrl(SecurityPageName.hosts); - const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); - - const goToHostAlerts = useCallback( - (ev) => { - ev.preventDefault(); - navigateToApp(APP_UI_ID, { - deepLinkId: SecurityPageName.hosts, - path: getTabsOnHostsUrl(HostsTableType.alerts, urlSearch), - }); - }, - [navigateToApp, urlSearch] - ); - - const alertsCountViewAlertsButton = useMemo( - () => ( - - {i18n.VIEW_ALERTS} - - ), - [goToHostAlerts, formatUrl] - ); - - const alertsByCategoryHistogramConfigs: MatrixHistogramConfigs = useMemo( - () => ({ - ...histogramConfigs, - defaultStackByOption: - alertsStackByOptions.find((o) => o.text === DEFAULT_STACK_BY) ?? alertsStackByOptions[0], - subtitle: (totalCount: number) => - `${SHOWING}: ${numeral(totalCount).format(defaultNumberFormat)} ${UNIT(totalCount)}`, - legendPosition: Position.Right, - }), - // eslint-disable-next-line react-hooks/exhaustive-deps - [] - ); - - const [filterQuery, kqlError] = useMemo( - () => - convertToBuildEsQuery({ - config: getEsQueryConfig(uiSettings), - indexPattern, - queries: [query], - filters, - }), - [filters, indexPattern, uiSettings, query] - ); - - useInvalidFilterQuery({ id: ID, filterQuery, kqlError, query, startDate: from, endDate: to }); - - useEffect(() => { - return () => { - if (deleteQuery) { - deleteQuery({ id: ID }); - } - }; - }, [deleteQuery]); - - return ( - - ); -}; - -AlertsByCategoryComponent.displayName = 'AlertsByCategoryComponent'; - -export const AlertsByCategory = React.memo(AlertsByCategoryComponent); diff --git a/x-pack/plugins/security_solution/public/overview/components/events_by_dataset/index.tsx b/x-pack/plugins/security_solution/public/overview/components/events_by_dataset/index.tsx index 4dc0452dd8831..15768c6359814 100644 --- a/x-pack/plugins/security_solution/public/overview/components/events_by_dataset/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/events_by_dataset/index.tsx @@ -26,8 +26,8 @@ import { convertToBuildEsQuery } from '../../../common/lib/keury'; import { useKibana, useUiSetting$ } from '../../../common/lib/kibana'; import { eventsStackByOptions, - histogramConfigs, -} from '../../../common/components/events_tab/events_query_tab_body'; + eventsHistogramConfig, +} from '../../../common/components/events_tab/histogram_configurations'; import { HostsTableType } from '../../../hosts/store/model'; import type { InputsModelId } from '../../../common/store/inputs/constants'; import type { GlobalTimeArgs } from '../../../common/containers/use_global_time'; @@ -154,9 +154,9 @@ const EventsByDatasetComponent: React.FC = ({ const eventsByDatasetHistogramConfigs: MatrixHistogramConfigs = useMemo( () => ({ - ...histogramConfigs, + ...eventsHistogramConfig, stackByOptions: - onlyField != null ? [getHistogramOption(onlyField)] : histogramConfigs.stackByOptions, + onlyField != null ? [getHistogramOption(onlyField)] : eventsHistogramConfig.stackByOptions, defaultStackByOption: onlyField != null ? getHistogramOption(onlyField) diff --git a/x-pack/plugins/security_solution/public/overview/components/signals_by_category/index.ts b/x-pack/plugins/security_solution/public/overview/components/signals_by_category/index.ts new file mode 100644 index 0000000000000..631f1fad37a8c --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/components/signals_by_category/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { SignalsByCategory } from './signals_by_category'; diff --git a/x-pack/plugins/security_solution/public/overview/components/signals_by_category/signals_by_category.test.tsx b/x-pack/plugins/security_solution/public/overview/components/signals_by_category/signals_by_category.test.tsx new file mode 100644 index 0000000000000..a305ffc140d41 --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/components/signals_by_category/signals_by_category.test.tsx @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { act, render } from '@testing-library/react'; + +import { TestProviders } from '../../../common/mock'; +import { SignalsByCategory } from './signals_by_category'; + +jest.mock('react-router-dom', () => { + const originalModule = jest.requireActual('react-router-dom'); + return { + ...originalModule, + useLocation: jest.fn().mockReturnValue({ pathname: '' }), + }; +}); + +const mockUseFiltersForSignals = jest.fn(() => []); +jest.mock('./use_filters_for_signals_by_category', () => ({ + useFiltersForSignalsByCategory: () => mockUseFiltersForSignals(), +})); + +const props = { + query: { + query: '', + language: 'kuery', + }, + filters: [], +}; + +const renderComponent = () => + render( + + + + ); + +describe('SignalsByCategory', () => { + it('Renders to the page', () => { + act(() => { + const { getByText } = renderComponent(); + expect(getByText('Alert trend')).toBeInTheDocument(); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/overview/components/signals_by_category/index.tsx b/x-pack/plugins/security_solution/public/overview/components/signals_by_category/signals_by_category.tsx similarity index 98% rename from x-pack/plugins/security_solution/public/overview/components/signals_by_category/index.tsx rename to x-pack/plugins/security_solution/public/overview/components/signals_by_category/signals_by_category.tsx index b05989c0c4a91..7f34bad9e76d9 100644 --- a/x-pack/plugins/security_solution/public/overview/components/signals_by_category/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/signals_by_category/signals_by_category.tsx @@ -85,8 +85,7 @@ const SignalsByCategoryComponent: React.FC = ({ showTotalAlertsCount={true} signalIndexName={signalIndexName} runtimeMappings={runtimeMappings} - timelineId={timelineId} - title={i18n.ALERT_COUNT} + title={i18n.ALERT_TREND} titleSize={onlyField == null ? 'm' : 's'} updateDateRange={updateDateRangeCallback} /> diff --git a/x-pack/plugins/security_solution/public/overview/pages/overview.tsx b/x-pack/plugins/security_solution/public/overview/pages/overview.tsx index 69c44741d5c4c..f0664881cff98 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/overview.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/overview.tsx @@ -8,7 +8,6 @@ import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiShowFor } from '@elastic/eui'; import React, { useCallback, useState, useMemo } from 'react'; -import { AlertsByCategory } from '../components/alerts_by_category'; import { FiltersGlobal } from '../../common/components/filters_global'; import { SiemSearchBar } from '../../common/components/search_bar'; import { SecuritySolutionPageWrapper } from '../../common/components/page_wrapper'; @@ -95,25 +94,10 @@ const OverviewComponent = () => { {hasIndexRead && hasKibanaREAD && ( - <> - - - - - - - - - + + + + )} diff --git a/x-pack/plugins/security_solution/public/overview/pages/translations.ts b/x-pack/plugins/security_solution/public/overview/pages/translations.ts index 4dc99c8850b64..ab67f6ce23008 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/translations.ts +++ b/x-pack/plugins/security_solution/public/overview/pages/translations.ts @@ -25,8 +25,8 @@ export const RECENT_TIMELINES = i18n.translate( } ); -export const ALERT_COUNT = i18n.translate('xpack.securitySolution.overview.signalCountTitle', { - defaultMessage: 'Detection alert trend', +export const ALERT_TREND = i18n.translate('xpack.securitySolution.overview.signalCountTitle', { + defaultMessage: 'Alert trend', }); export const TOP = (fieldName: string) => diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index f03a38817bf85..e7b92c5f5f756 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -331,6 +331,7 @@ export class Plugin implements IPlugin { + const services = useKibana().services; + const { threatIntelligence } = services; + const ThreatIntelligencePlugin = threatIntelligence.getComponent(); + + return ( + + + + + ); +}; + +export const ThreatIntelligencePage = React.memo(ThreatIntelligence); diff --git a/x-pack/plugins/security_solution/public/threat_intelligence/routes.tsx b/x-pack/plugins/security_solution/public/threat_intelligence/routes.tsx new file mode 100644 index 0000000000000..65b1440c49d2c --- /dev/null +++ b/x-pack/plugins/security_solution/public/threat_intelligence/routes.tsx @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { TrackApplicationView } from '@kbn/usage-collection-plugin/public'; +import { ThreatIntelligencePage } from './pages/threat_intelligence'; +import { SecurityPageName, THREAT_INTELLIGENCE_PATH } from '../../common/constants'; +import type { SecuritySubPluginRoutes } from '../app/types'; + +const ThreatIntelligenceRoutes = () => ( + + + +); + +export const routes: SecuritySubPluginRoutes = [ + { + path: THREAT_INTELLIGENCE_PATH, + render: ThreatIntelligenceRoutes, + }, +]; diff --git a/x-pack/plugins/security_solution/public/timelines/components/netflow/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/netflow/__snapshots__/index.test.tsx.snap index bdd720346c57b..bcfd01077cca0 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/netflow/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/netflow/__snapshots__/index.test.tsx.snap @@ -94,16 +94,42 @@ tr:hover .c3:focus::before { background-image: linear-gradient( 135deg, #1d1e24 25%, transparent 25% ), linear-gradient( -135deg, #1d1e24 25%, transparent 25% ), linear-gradient( 135deg, transparent 75%, #1d1e24 75% ), linear-gradient( -135deg, transparent 75%, #1d1e24 75% ); } +.c2 { + display: inline-block; + max-width: 100%; +} + +.c2 [data-rbd-placeholder-context-id] { + display: none !important; +} + +.c4 > span.euiToolTipAnchor { + display: block; +} + +.c4 > span.euiToolTipAnchor.eui-textTruncate { + display: inline-block; +} + +.c5 { + vertical-align: top; +} + +.c22 { + margin-right: 5px; +} + .c21 { margin-right: 5px; } -.c7 { - margin-right: 3px; +.c15 { + position: relative; + top: 1px; } -.c8 { - margin: 0 5px; +.c14 { + margin-right: 5px; } .c17 { @@ -134,13 +160,12 @@ tr:hover .c3:focus::before { margin: 0 5px; } -.c15 { - position: relative; - top: 1px; +.c7 { + margin-right: 3px; } -.c14 { - margin-right: 5px; +.c8 { + margin: 0 5px; } .c12 { @@ -166,31 +191,6 @@ tr:hover .c3:focus::before { margin-right: 10px; } -.c2 { - display: inline-block; - max-width: 100%; -} - -.c2 [data-rbd-placeholder-context-id] { - display: none !important; -} - -.c4 > span.euiToolTipAnchor { - display: block; -} - -.c4 > span.euiToolTipAnchor.eui-textTruncate { - display: inline-block; -} - -.c5 { - vertical-align: top; -} - -.c22 { - margin-right: 5px; -} -
span.euiToolTipAnchor { + display: block; +} + +.c6 > span.euiToolTipAnchor.eui-textTruncate { + display: inline-block; +} + +.c7 { + vertical-align: top; +} + +.c24 { + margin-right: 5px; +} + .c23 { margin-right: 5px; } @@ -179,31 +204,6 @@ tr:hover .c5:focus::before { margin-right: 10px; } -.c4 { - display: inline-block; - max-width: 100%; -} - -.c4 [data-rbd-placeholder-context-id] { - display: none !important; -} - -.c6 > span.euiToolTipAnchor { - display: block; -} - -.c6 > span.euiToolTipAnchor.eui-textTruncate { - display: inline-block; -} - -.c7 { - vertical-align: top; -} - -.c24 { - margin-right: 5px; -} - .c1 { margin: 5px 0; } diff --git a/x-pack/plugins/security_solution/public/timelines/containers/helpers.test.ts b/x-pack/plugins/security_solution/public/timelines/containers/helpers.test.ts index b079c70078557..20f719034e8d9 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/containers/helpers.test.ts @@ -17,13 +17,7 @@ describe('skipQueryForDetectionsPage', () => { skipQueryForDetectionsPage(TimelineId.hostsPageEvents, ['auditbeat-*', 'filebeat-*']) ).toBe(false); expect( - skipQueryForDetectionsPage(TimelineId.hostsPageExternalAlerts, ['auditbeat-*', 'filebeat-*']) - ).toBe(false); - expect( - skipQueryForDetectionsPage(TimelineId.networkPageExternalAlerts, [ - 'auditbeat-*', - 'filebeat-*', - ]) + skipQueryForDetectionsPage(TimelineId.networkPageEvents, ['auditbeat-*', 'filebeat-*']) ).toBe(false); }); diff --git a/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.test.ts b/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.test.ts index 10fabbb86fd0a..d580114eac1fd 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/containers/local_storage/index.test.ts @@ -59,10 +59,10 @@ describe('SiemLocalStorage', () => { it('adds a timeline when storage contains another timelines', () => { const timelineStorage = useTimelinesStorage(); timelineStorage.addTimeline(TimelineId.hostsPageEvents, mockTimelineModel); - timelineStorage.addTimeline(TimelineId.hostsPageExternalAlerts, mockTimelineModel); + timelineStorage.addTimeline(TimelineId.usersPageEvents, mockTimelineModel); expect(JSON.parse(localStorage.getItem(LOCAL_STORAGE_TIMELINE_KEY))).toEqual({ [TimelineId.hostsPageEvents]: timelineToStore, - [TimelineId.hostsPageExternalAlerts]: timelineToStore, + [TimelineId.usersPageEvents]: timelineToStore, }); }); }); @@ -71,11 +71,11 @@ describe('SiemLocalStorage', () => { it('gets all timelines correctly', () => { const timelineStorage = useTimelinesStorage(); timelineStorage.addTimeline(TimelineId.hostsPageEvents, mockTimelineModel); - timelineStorage.addTimeline(TimelineId.hostsPageExternalAlerts, mockTimelineModel); + timelineStorage.addTimeline(TimelineId.usersPageEvents, mockTimelineModel); const timelines = timelineStorage.getAllTimelines(); expect(timelines).toEqual({ [TimelineId.hostsPageEvents]: timelineToStore, - [TimelineId.hostsPageExternalAlerts]: timelineToStore, + [TimelineId.usersPageEvents]: timelineToStore, }); }); @@ -99,14 +99,14 @@ describe('SiemLocalStorage', () => { it('gets timelines correctly', () => { const timelineStorage = useTimelinesStorage(); timelineStorage.addTimeline(TimelineId.hostsPageEvents, mockTimelineModel); - timelineStorage.addTimeline(TimelineId.hostsPageExternalAlerts, mockTimelineModel); + timelineStorage.addTimeline(TimelineId.usersPageEvents, mockTimelineModel); const timelines = getTimelinesInStorageByIds(storage, [ TimelineId.hostsPageEvents, - TimelineId.hostsPageExternalAlerts, + TimelineId.usersPageEvents, ]); expect(timelines).toEqual({ [TimelineId.hostsPageEvents]: timelineToStore, - [TimelineId.hostsPageExternalAlerts]: timelineToStore, + [TimelineId.usersPageEvents]: timelineToStore, }); }); @@ -125,7 +125,7 @@ describe('SiemLocalStorage', () => { it('returns empty timelime when a specific timeline does not exists', () => { const timelineStorage = useTimelinesStorage(); timelineStorage.addTimeline(TimelineId.hostsPageEvents, mockTimelineModel); - const timelines = getTimelinesInStorageByIds(storage, [TimelineId.hostsPageExternalAlerts]); + const timelines = getTimelinesInStorageByIds(storage, [TimelineId.usersPageEvents]); expect(timelines).toEqual({}); }); @@ -134,7 +134,7 @@ describe('SiemLocalStorage', () => { timelineStorage.addTimeline(TimelineId.hostsPageEvents, mockTimelineModel); const timelines = getTimelinesInStorageByIds(storage, [ TimelineId.hostsPageEvents, - TimelineId.hostsPageExternalAlerts, + TimelineId.usersPageEvents, ]); expect(timelines).toEqual({ [TimelineId.hostsPageEvents]: timelineToStore, @@ -154,10 +154,10 @@ describe('SiemLocalStorage', () => { })), }; timelineStorage.addTimeline(TimelineId.hostsPageEvents, unmigratedMockTimelineModel); - timelineStorage.addTimeline(TimelineId.hostsPageExternalAlerts, mockTimelineModel); + timelineStorage.addTimeline(TimelineId.usersPageEvents, mockTimelineModel); const timelines = getTimelinesInStorageByIds(storage, [ TimelineId.hostsPageEvents, - TimelineId.hostsPageExternalAlerts, + TimelineId.usersPageEvents, ]); // all legacy `width` values are migrated to `initialWidth`: @@ -171,7 +171,7 @@ describe('SiemLocalStorage', () => { width: 98765, })), }, - [TimelineId.hostsPageExternalAlerts]: { + [TimelineId.usersPageEvents]: { ...timelineToStore, columns: getExpectedColumns(mockTimelineModel), }, @@ -190,10 +190,10 @@ describe('SiemLocalStorage', () => { })), }; timelineStorage.addTimeline(TimelineId.hostsPageEvents, unmigratedMockTimelineModel); - timelineStorage.addTimeline(TimelineId.hostsPageExternalAlerts, mockTimelineModel); + timelineStorage.addTimeline(TimelineId.usersPageEvents, mockTimelineModel); const timelines = getTimelinesInStorageByIds(storage, [ TimelineId.hostsPageEvents, - TimelineId.hostsPageExternalAlerts, + TimelineId.usersPageEvents, ]); expect(timelines).toStrictEqual({ @@ -206,7 +206,7 @@ describe('SiemLocalStorage', () => { width: 98765, })), }, - [TimelineId.hostsPageExternalAlerts]: { + [TimelineId.usersPageEvents]: { ...timelineToStore, columns: getExpectedColumns(mockTimelineModel), }, @@ -225,10 +225,10 @@ describe('SiemLocalStorage', () => { })), }; timelineStorage.addTimeline(TimelineId.hostsPageEvents, unmigratedMockTimelineModel); - timelineStorage.addTimeline(TimelineId.hostsPageExternalAlerts, mockTimelineModel); + timelineStorage.addTimeline(TimelineId.usersPageEvents, mockTimelineModel); const timelines = getTimelinesInStorageByIds(storage, [ TimelineId.hostsPageEvents, - TimelineId.hostsPageExternalAlerts, + TimelineId.usersPageEvents, ]); // all legacy `label` values are migrated to `displayAsText`: @@ -241,7 +241,7 @@ describe('SiemLocalStorage', () => { label: `A legacy label ${i}`, })), }, - [TimelineId.hostsPageExternalAlerts]: { + [TimelineId.usersPageEvents]: { ...timelineToStore, columns: getExpectedColumns(mockTimelineModel), }, @@ -262,10 +262,10 @@ describe('SiemLocalStorage', () => { })), }; timelineStorage.addTimeline(TimelineId.hostsPageEvents, unmigratedMockTimelineModel); - timelineStorage.addTimeline(TimelineId.hostsPageExternalAlerts, mockTimelineModel); + timelineStorage.addTimeline(TimelineId.usersPageEvents, mockTimelineModel); const timelines = getTimelinesInStorageByIds(storage, [ TimelineId.hostsPageEvents, - TimelineId.hostsPageExternalAlerts, + TimelineId.usersPageEvents, ]); expect(timelines).toStrictEqual({ @@ -278,7 +278,7 @@ describe('SiemLocalStorage', () => { label: `A legacy label ${i}`, })), }, - [TimelineId.hostsPageExternalAlerts]: { + [TimelineId.usersPageEvents]: { ...timelineToStore, columns: getExpectedColumns(mockTimelineModel), }, @@ -296,10 +296,10 @@ describe('SiemLocalStorage', () => { TimelineId.hostsPageEvents, invalidColumnsMockTimelineModel as unknown as TimelineModel ); - timelineStorage.addTimeline(TimelineId.hostsPageExternalAlerts, mockTimelineModel); + timelineStorage.addTimeline(TimelineId.usersPageEvents, mockTimelineModel); const timelines = getTimelinesInStorageByIds(storage, [ TimelineId.hostsPageEvents, - TimelineId.hostsPageExternalAlerts, + TimelineId.usersPageEvents, ]); expect(timelines).toStrictEqual({ @@ -307,7 +307,7 @@ describe('SiemLocalStorage', () => { ...timelineToStore, columns: 'this is NOT an array', }, - [TimelineId.hostsPageExternalAlerts]: { + [TimelineId.usersPageEvents]: { ...timelineToStore, columns: getExpectedColumns(mockTimelineModel), }, @@ -319,11 +319,11 @@ describe('SiemLocalStorage', () => { it('gets timelines correctly', () => { const timelineStorage = useTimelinesStorage(); timelineStorage.addTimeline(TimelineId.hostsPageEvents, mockTimelineModel); - timelineStorage.addTimeline(TimelineId.hostsPageExternalAlerts, mockTimelineModel); + timelineStorage.addTimeline(TimelineId.usersPageEvents, mockTimelineModel); const timelines = getAllTimelinesInStorage(storage); expect(timelines).toEqual({ [TimelineId.hostsPageEvents]: timelineToStore, - [TimelineId.hostsPageExternalAlerts]: timelineToStore, + [TimelineId.usersPageEvents]: timelineToStore, }); }); @@ -343,10 +343,10 @@ describe('SiemLocalStorage', () => { it('adds a timeline when storage contains another timelines', () => { addTimelineInStorage(storage, TimelineId.hostsPageEvents, mockTimelineModel); - addTimelineInStorage(storage, TimelineId.hostsPageExternalAlerts, mockTimelineModel); + addTimelineInStorage(storage, TimelineId.usersPageEvents, mockTimelineModel); expect(JSON.parse(localStorage.getItem(LOCAL_STORAGE_TIMELINE_KEY))).toEqual({ [TimelineId.hostsPageEvents]: timelineToStore, - [TimelineId.hostsPageExternalAlerts]: timelineToStore, + [TimelineId.usersPageEvents]: timelineToStore, }); }); }); diff --git a/x-pack/plugins/security_solution/public/types.ts b/x-pack/plugins/security_solution/public/types.ts index bc6d71c04798b..1104ff630b61a 100644 --- a/x-pack/plugins/security_solution/public/types.ts +++ b/x-pack/plugins/security_solution/public/types.ts @@ -39,6 +39,7 @@ import type { SavedObjectsTaggingApi, SavedObjectTaggingOssPluginStart, } from '@kbn/saved-objects-tagging-oss-plugin/public'; +import type { ThreatIntelligencePluginStart } from '@kbn/threat-intelligence-plugin/public'; import type { ResolverPluginSetup } from './resolver/types'; import type { Inspect } from '../common/search_strategy'; import type { Detections } from './detections'; @@ -54,6 +55,7 @@ import type { Timelines } from './timelines'; import type { Management } from './management'; import type { LandingPages } from './landing_pages'; import type { CloudSecurityPosture } from './cloud_security_posture'; +import type { ThreatIntelligence } from './threat_intelligence'; export interface SetupPlugins { home?: HomePublicPluginSetup; @@ -87,6 +89,7 @@ export interface StartPlugins { osquery?: OsqueryPluginStart; security: SecurityPluginSetup; cloudSecurityPosture: CspClientPluginStart; + threatIntelligence: ThreatIntelligencePluginStart; } export interface StartPluginsDependencies extends StartPlugins { @@ -127,6 +130,7 @@ export interface SubPlugins { management: Management; landingPages: LandingPages; cloudSecurityPosture: CloudSecurityPosture; + threatIntelligence: ThreatIntelligence; } // TODO: find a better way to defined these types @@ -144,4 +148,5 @@ export interface StartedSubPlugins { management: ReturnType; landingPages: ReturnType; cloudSecurityPosture: ReturnType; + threatIntelligence: ReturnType; } diff --git a/x-pack/plugins/security_solution/public/users/links.ts b/x-pack/plugins/security_solution/public/users/links.ts index 0990cbed24b33..0ddef435c1f58 100644 --- a/x-pack/plugins/security_solution/public/users/links.ts +++ b/x-pack/plugins/security_solution/public/users/links.ts @@ -56,12 +56,5 @@ export const links: LinkItem = { }), path: `${USERS_PATH}/events`, }, - { - id: SecurityPageName.usersExternalAlerts, - title: i18n.translate('xpack.securitySolution.appLinks.users.externalAlerts', { - defaultMessage: 'External Alerts', - }), - path: `${USERS_PATH}/externalAlerts`, - }, ], }; diff --git a/x-pack/plugins/security_solution/public/users/pages/constants.ts b/x-pack/plugins/security_solution/public/users/pages/constants.ts index e8b9e4a4118a1..b8788fb5ac29e 100644 --- a/x-pack/plugins/security_solution/public/users/pages/constants.ts +++ b/x-pack/plugins/security_solution/public/users/pages/constants.ts @@ -10,6 +10,6 @@ import { UsersTableType } from '../store/model'; export const usersDetailsPagePath = `${USERS_PATH}/:detailName`; -export const usersTabPath = `${USERS_PATH}/:tabName(${UsersTableType.allUsers}|${UsersTableType.authentications}|${UsersTableType.anomalies}|${UsersTableType.risk}|${UsersTableType.events}|${UsersTableType.alerts})`; +export const usersTabPath = `${USERS_PATH}/:tabName(${UsersTableType.allUsers}|${UsersTableType.authentications}|${UsersTableType.anomalies}|${UsersTableType.risk}|${UsersTableType.events}|)`; -export const usersDetailsTabPath = `${usersDetailsPagePath}/:tabName(${UsersTableType.authentications}|${UsersTableType.anomalies}|${UsersTableType.events}|${UsersTableType.alerts}|${UsersTableType.risk})`; +export const usersDetailsTabPath = `${usersDetailsPagePath}/:tabName(${UsersTableType.authentications}|${UsersTableType.anomalies}|${UsersTableType.events}|${UsersTableType.risk})`; diff --git a/x-pack/plugins/security_solution/public/users/pages/details/details_tabs.tsx b/x-pack/plugins/security_solution/public/users/pages/details/details_tabs.tsx index 5bf985d59e14e..da3aa5d585517 100644 --- a/x-pack/plugins/security_solution/public/users/pages/details/details_tabs.tsx +++ b/x-pack/plugins/security_solution/public/users/pages/details/details_tabs.tsx @@ -18,8 +18,7 @@ import type { UpdateDateRange } from '../../../common/components/charts/common'; import type { Anomaly } from '../../../common/components/ml/types'; import { usersDetailsPagePath } from '../constants'; import { TimelineId } from '../../../../common/types'; -import { EventsQueryTabBody } from '../../../common/components/events_tab/events_query_tab_body'; -import { AlertsView } from '../../../common/components/alerts_viewer'; +import { EventsQueryTabBody } from '../../../common/components/events_tab'; import { userNameExistsFilter } from './helpers'; import { AuthenticationsQueryTabBody } from '../navigation'; import { UserRiskTabBody } from '../navigation/user_risk_tab_body'; @@ -36,7 +35,7 @@ export const UsersDetailsTabs = React.memo( type, setAbsoluteRangeDatePicker, detailName, - pageFilters, + pageFilters = [], }) => { const narrowDateRange = useCallback( (score: Anomaly, interval: string) => { @@ -65,12 +64,6 @@ export const UsersDetailsTabs = React.memo( [setAbsoluteRangeDatePicker] ); - const alertsPageFilters = useMemo( - () => - pageFilters != null ? [...userNameExistsFilter, ...pageFilters] : userNameExistsFilter, - [pageFilters] - ); - const tabProps = { deleteQuery, endDate: to, @@ -85,6 +78,11 @@ export const UsersDetailsTabs = React.memo( userName: detailName, }; + const externalAlertPageFilters = useMemo( + () => [...userNameExistsFilter, ...pageFilters], + [pageFilters] + ); + return ( @@ -98,15 +96,7 @@ export const UsersDetailsTabs = React.memo( {...tabProps} pageFilters={pageFilters} timelineId={TimelineId.usersPageEvents} - /> - - - - diff --git a/x-pack/plugins/security_solution/public/users/pages/details/nav_tabs.tsx b/x-pack/plugins/security_solution/public/users/pages/details/nav_tabs.tsx index 9c7827bd9709f..b57d1014e0167 100644 --- a/x-pack/plugins/security_solution/public/users/pages/details/nav_tabs.tsx +++ b/x-pack/plugins/security_solution/public/users/pages/details/nav_tabs.tsx @@ -40,12 +40,6 @@ export const navTabsUsersDetails = ( href: getTabsOnUsersDetailsUrl(userName, UsersTableType.events), disabled: false, }, - [UsersTableType.alerts]: { - id: UsersTableType.alerts, - name: i18n.NAVIGATION_ALERTS_TITLE, - href: getTabsOnUsersDetailsUrl(userName, UsersTableType.alerts), - disabled: false, - }, [UsersTableType.risk]: { id: UsersTableType.risk, name: i18n.NAVIGATION_RISK_TITLE, diff --git a/x-pack/plugins/security_solution/public/users/pages/details/types.ts b/x-pack/plugins/security_solution/public/users/pages/details/types.ts index 8f702577713f2..0046f3da7d63b 100644 --- a/x-pack/plugins/security_solution/public/users/pages/details/types.ts +++ b/x-pack/plugins/security_solution/public/users/pages/details/types.ts @@ -6,14 +6,21 @@ */ import type { ActionCreator } from 'typescript-fsa'; -import type { DataViewBase, Filter } from '@kbn/es-query'; + +import type { DataViewBase, Filter, Query } from '@kbn/es-query'; + import type { InputsModelId } from '../../../common/store/inputs/constants'; import type { UsersQueryProps } from '../types'; import type { NavTab } from '../../../common/components/navigation/types'; -import type { UsersTableType } from '../../store/model'; +import type { UsersDetailsTableType } from '../../store/model'; import type { usersModel } from '../../store'; +interface UsersDetailsComponentReduxProps { + query: Query; + filters: Filter[]; +} + interface UserBodyComponentDispatchProps { setAbsoluteRangeDatePicker: ActionCreator<{ id: InputsModelId; @@ -24,22 +31,22 @@ interface UserBodyComponentDispatchProps { usersDetailsPagePath: string; } +interface UsersDetailsComponentDispatchProps extends UserBodyComponentDispatchProps { + setUsersDetailsTablesActivePageToZero: ActionCreator; +} + export interface UsersDetailsProps { detailName: string; usersDetailsPagePath: string; } -export type KeyUsersDetailsNavTabWithoutMlPermission = UsersTableType.events & - UsersTableType.alerts; +export type UsersDetailsComponentProps = UsersDetailsComponentReduxProps & + UsersDetailsComponentDispatchProps & + UsersQueryProps; -type KeyUsersDetailsNavTabWithMlPermission = KeyUsersDetailsNavTabWithoutMlPermission & - UsersTableType.anomalies; +type KeyUsersDetailsNavTab = `${UsersDetailsTableType}`; -type KeyUsersDetailsNavTab = - | KeyUsersDetailsNavTabWithoutMlPermission - | KeyUsersDetailsNavTabWithMlPermission; - -export type UsersDetailsNavTab = Record; +export type UsersDetailsNavTab = Partial>; export type UsersDetailsTabsProps = UserBodyComponentDispatchProps & UsersQueryProps & { @@ -49,3 +56,9 @@ export type UsersDetailsTabsProps = UserBodyComponentDispatchProps & indexPattern: DataViewBase; type: usersModel.UsersType; }; + +export type SetAbsoluteRangeDatePicker = ActionCreator<{ + id: InputsModelId; + from: string; + to: string; +}>; diff --git a/x-pack/plugins/security_solution/public/users/pages/details/utils.ts b/x-pack/plugins/security_solution/public/users/pages/details/utils.ts index 79329e7d128a7..f07978cc38c46 100644 --- a/x-pack/plugins/security_solution/public/users/pages/details/utils.ts +++ b/x-pack/plugins/security_solution/public/users/pages/details/utils.ts @@ -25,7 +25,6 @@ const TabNameMappedToI18nKey: Record = { [UsersTableType.anomalies]: i18n.NAVIGATION_ANOMALIES_TITLE, [UsersTableType.risk]: i18n.NAVIGATION_RISK_TITLE, [UsersTableType.events]: i18n.NAVIGATION_EVENTS_TITLE, - [UsersTableType.alerts]: i18n.NAVIGATION_ALERTS_TITLE, [UsersTableType.risk]: i18n.NAVIGATION_RISK_TITLE, }; diff --git a/x-pack/plugins/security_solution/public/users/pages/nav_tabs.tsx b/x-pack/plugins/security_solution/public/users/pages/nav_tabs.tsx index 832d070dfc492..0e5218090f162 100644 --- a/x-pack/plugins/security_solution/public/users/pages/nav_tabs.tsx +++ b/x-pack/plugins/security_solution/public/users/pages/nav_tabs.tsx @@ -44,12 +44,6 @@ export const navTabsUsers = ( href: getTabsOnUsersUrl(UsersTableType.events), disabled: false, }, - [UsersTableType.alerts]: { - id: UsersTableType.alerts, - name: i18n.NAVIGATION_ALERTS_TITLE, - href: getTabsOnUsersUrl(UsersTableType.alerts), - disabled: false, - }, [UsersTableType.risk]: { id: UsersTableType.risk, name: i18n.NAVIGATION_RISK_TITLE, diff --git a/x-pack/plugins/security_solution/public/users/pages/navigation/types.ts b/x-pack/plugins/security_solution/public/users/pages/navigation/types.ts index fb95d2ed794a3..aeac9326a1f93 100644 --- a/x-pack/plugins/security_solution/public/users/pages/navigation/types.ts +++ b/x-pack/plugins/security_solution/public/users/pages/navigation/types.ts @@ -10,16 +10,9 @@ import type { GlobalTimeArgs } from '../../../common/containers/use_global_time' import type { ESTermQuery } from '../../../../common/typed_json'; import type { NavTab } from '../../../common/components/navigation/types'; -type KeyUsersNavTabWithoutMlPermission = UsersTableType.allUsers & - UsersTableType.risk & - UsersTableType.events & - UsersTableType.alerts; +type KeyUsersNavTab = `${UsersTableType}`; -type KeyUsersNavTabWithMlPermission = KeyUsersNavTabWithoutMlPermission & UsersTableType.anomalies; - -type KeyUsersNavTab = KeyUsersNavTabWithoutMlPermission | KeyUsersNavTabWithMlPermission; - -export type UsersNavTab = Record; +export type UsersNavTab = Partial>; export interface QueryTabBodyProps { type: UsersType; startDate: GlobalTimeArgs['from']; diff --git a/x-pack/plugins/security_solution/public/users/pages/translations.ts b/x-pack/plugins/security_solution/public/users/pages/translations.ts index c36abbaab86ec..6668564c6cef1 100644 --- a/x-pack/plugins/security_solution/public/users/pages/translations.ts +++ b/x-pack/plugins/security_solution/public/users/pages/translations.ts @@ -46,13 +46,6 @@ export const NAVIGATION_EVENTS_TITLE = i18n.translate( } ); -export const NAVIGATION_ALERTS_TITLE = i18n.translate( - 'xpack.securitySolution.users.navigation.alertsTitle', - { - defaultMessage: 'External alerts', - } -); - export const USER_RISK_SCORE_OVER_TIME = i18n.translate( 'xpack.securitySolution.users.navigation.userScoreOverTimeTitle', { diff --git a/x-pack/plugins/security_solution/public/users/pages/users.tsx b/x-pack/plugins/security_solution/public/users/pages/users.tsx index 878324f3b55d1..9ac8efc0e83c4 100644 --- a/x-pack/plugins/security_solution/public/users/pages/users.tsx +++ b/x-pack/plugins/security_solution/public/users/pages/users.tsx @@ -90,7 +90,7 @@ const UsersComponent = () => { const { tabName } = useParams<{ tabName: string }>(); const tabsFilters: Filter[] = React.useMemo(() => { - if (tabName === UsersTableType.alerts || tabName === UsersTableType.events) { + if (tabName === UsersTableType.events) { return filters.length > 0 ? [...filters, ...userNameExistsFilter] : userNameExistsFilter; } diff --git a/x-pack/plugins/security_solution/public/users/pages/users_tabs.tsx b/x-pack/plugins/security_solution/public/users/pages/users_tabs.tsx index f1aca82f21493..039831cdb9bbe 100644 --- a/x-pack/plugins/security_solution/public/users/pages/users_tabs.tsx +++ b/x-pack/plugins/security_solution/public/users/pages/users_tabs.tsx @@ -20,9 +20,8 @@ import { scoreIntervalToDateTime } from '../../common/components/ml/score/score_ import type { UpdateDateRange } from '../../common/components/charts/common'; import { UserRiskScoreQueryTabBody } from './navigation/user_risk_score_tab_body'; -import { EventsQueryTabBody } from '../../common/components/events_tab/events_query_tab_body'; +import { EventsQueryTabBody } from '../../common/components/events_tab'; import { TimelineId } from '../../../common/types'; -import { AlertsView } from '../../common/components/alerts_viewer'; export const UsersTabs = memo( ({ @@ -94,16 +93,8 @@ export const UsersTabs = memo( - - - diff --git a/x-pack/plugins/security_solution/public/users/store/model.ts b/x-pack/plugins/security_solution/public/users/store/model.ts index a93ef5461609d..8588790513874 100644 --- a/x-pack/plugins/security_solution/public/users/store/model.ts +++ b/x-pack/plugins/security_solution/public/users/store/model.ts @@ -19,7 +19,13 @@ export enum UsersTableType { anomalies = 'anomalies', risk = 'userRisk', events = 'events', - alerts = 'externalAlerts', +} + +export enum UsersDetailsTableType { + authentications = 'authentications', + anomalies = 'anomalies', + risk = 'userRisk', + events = 'events', } export interface BasicQueryPaginated { @@ -42,13 +48,11 @@ export interface UsersQueries { [UsersTableType.anomalies]: null | undefined; [UsersTableType.risk]: UsersRiskScoreQuery; [UsersTableType.events]: BasicQueryPaginated; - [UsersTableType.alerts]: BasicQueryPaginated; } export interface UserDetailsQueries { [UsersTableType.anomalies]: null | undefined; [UsersTableType.events]: BasicQueryPaginated; - [UsersTableType.alerts]: BasicQueryPaginated; } export interface UsersPageModel { diff --git a/x-pack/plugins/security_solution/public/users/store/reducer.ts b/x-pack/plugins/security_solution/public/users/store/reducer.ts index 76e3d245c3b5d..c2eae888188b1 100644 --- a/x-pack/plugins/security_solution/public/users/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/users/store/reducer.ts @@ -51,10 +51,6 @@ export const initialUsersState: UsersModel = { activePage: DEFAULT_TABLE_ACTIVE_PAGE, limit: DEFAULT_TABLE_LIMIT, }, - [UsersTableType.alerts]: { - activePage: DEFAULT_TABLE_ACTIVE_PAGE, - limit: DEFAULT_TABLE_LIMIT, - }, }, }, details: { @@ -64,10 +60,6 @@ export const initialUsersState: UsersModel = { activePage: DEFAULT_TABLE_ACTIVE_PAGE, limit: DEFAULT_TABLE_LIMIT, }, - [UsersTableType.alerts]: { - activePage: DEFAULT_TABLE_ACTIVE_PAGE, - limit: DEFAULT_TABLE_LIMIT, - }, }, }, }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts index 0f20d04fc5f7c..9a43f02ce5fd0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts @@ -24,7 +24,7 @@ import { ruleRegistryMocks } from '@kbn/rule-registry-plugin/server/mocks'; import { siemMock } from '../../../../mocks'; import { createMockConfig } from '../../../../config.mock'; -import { ruleExecutionLogMock } from '../../rule_execution_log/__mocks__'; +import { ruleExecutionLogMock } from '../../rule_monitoring/mocks'; import { requestMock } from './request'; import { internalFrameworkRequest } from '../../../framework'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts index c8cfeee596500..bbc40dcf4b8d6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -20,12 +20,10 @@ import { DETECTION_ENGINE_SIGNALS_FINALIZE_MIGRATION_URL, DETECTION_ENGINE_SIGNALS_MIGRATION_STATUS_URL, DETECTION_ENGINE_RULES_BULK_ACTION, - DETECTION_ENGINE_RULE_EXECUTION_EVENTS_URL, DETECTION_ENGINE_RULES_BULK_UPDATE, DETECTION_ENGINE_RULES_BULK_DELETE, DETECTION_ENGINE_RULES_BULK_CREATE, } from '../../../../../common/constants'; -import type { GetAggregateRuleExecutionEventsResponse } from '../../../../../common/detection_engine/schemas/response'; import type { RuleAlertType, HapiReadableStream } from '../../rules/types'; import { requestMock } from './request'; import type { QuerySignalsSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/query_signals_index_schema'; @@ -40,13 +38,10 @@ import { getPerformBulkActionSchemaMock, getPerformBulkActionEditSchemaMock, } from '../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema.mock'; -import type { RuleExecutionSummary } from '../../../../../common/detection_engine/schemas/common'; -import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common'; // eslint-disable-next-line no-restricted-imports import type { LegacyRuleNotificationAlertType } from '../../notifications/legacy_types'; // eslint-disable-next-line no-restricted-imports import type { LegacyIRuleActionsAttributes } from '../../rule_actions/legacy_types'; -import type { RuleExecutionSummariesByRuleId } from '../../rule_execution_log'; export const typicalSetStatusSignalByIdsPayload = (): SetSignalsStatusSchemaDecoded => ({ signal_ids: ['somefakeid1', 'somefakeid2'], @@ -234,19 +229,6 @@ export const getFindResultWithMultiHits = ({ }; }; -export const getRuleExecutionEventsRequest = () => - requestMock.create({ - method: 'get', - path: DETECTION_ENGINE_RULE_EXECUTION_EVENTS_URL, - params: { - ruleId: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - }, - query: { - start: '2022-03-31T22:02:01.622Z', - end: '2022-03-31T22:02:31.622Z', - }, - }); - export const getImportRulesRequest = (hapiStream?: HapiReadableStream) => requestMock.create({ method: 'post', @@ -445,96 +427,6 @@ export const getEmptySavedObjectsResponse = (): SavedObjectsFindResponse => ({ saved_objects: [], }); -// TODO: https://github.com/elastic/kibana/pull/121644 clean up -export const getRuleExecutionSummarySucceeded = (): RuleExecutionSummary => ({ - last_execution: { - date: '2020-02-18T15:26:49.783Z', - status: RuleExecutionStatus.succeeded, - status_order: 0, - message: 'succeeded', - metrics: { - total_search_duration_ms: 200, - total_indexing_duration_ms: 800, - execution_gap_duration_s: 500, - }, - }, -}); - -// TODO: https://github.com/elastic/kibana/pull/121644 clean up -export const getRuleExecutionSummaryFailed = (): RuleExecutionSummary => ({ - last_execution: { - date: '2020-02-18T15:15:58.806Z', - status: RuleExecutionStatus.failed, - status_order: 30, - message: - 'Signal rule name: "Query with a rule id Number 1", id: "1ea5a820-4da1-4e82-92a1-2b43a7bece08", rule_id: "query-rule-id-1" has a time gap of 5 days (412682928ms), and could be missing signals within that time. Consider increasing your look behind time or adding more Kibana instances.', - metrics: { - total_search_duration_ms: 200, - total_indexing_duration_ms: 800, - execution_gap_duration_s: 500, - }, - }, -}); - -// TODO: https://github.com/elastic/kibana/pull/121644 clean up -export const getRuleExecutionSummaries = (): RuleExecutionSummariesByRuleId => ({ - '04128c15-0d1b-4716-a4c5-46997ac7f3bd': getRuleExecutionSummarySucceeded(), - '1ea5a820-4da1-4e82-92a1-2b43a7bece08': getRuleExecutionSummaryFailed(), -}); - -export const getAggregateExecutionEvents = (): GetAggregateRuleExecutionEventsResponse => ({ - events: [ - { - execution_uuid: '34bab6e0-89b6-4d10-9cbb-cda76d362db6', - timestamp: '2022-03-11T22:04:05.931Z', - duration_ms: 1975, - status: 'success', - message: - "rule executed: siem.queryRule:f78f3550-a186-11ec-89a1-0bce95157aba: 'This Rule Makes Alerts, Actions, AND Moar!'", - num_active_alerts: 0, - num_new_alerts: 0, - num_recovered_alerts: 0, - num_triggered_actions: 0, - num_succeeded_actions: 0, - num_errored_actions: 0, - total_search_duration_ms: 0, - es_search_duration_ms: 538, - schedule_delay_ms: 2091, - timed_out: false, - indexing_duration_ms: 7, - search_duration_ms: 551, - gap_duration_s: 0, - security_status: 'succeeded', - security_message: 'succeeded', - }, - { - execution_uuid: '254d8400-9dc7-43c5-ad4b-227273d1a44b', - timestamp: '2022-03-11T22:02:41.923Z', - duration_ms: 11916, - status: 'success', - message: - "rule executed: siem.queryRule:f78f3550-a186-11ec-89a1-0bce95157aba: 'This Rule Makes Alerts, Actions, AND Moar!'", - num_active_alerts: 0, - num_new_alerts: 0, - num_recovered_alerts: 0, - num_triggered_actions: 1, - num_succeeded_actions: 1, - num_errored_actions: 0, - total_search_duration_ms: 0, - es_search_duration_ms: 1406, - schedule_delay_ms: 1583, - timed_out: false, - indexing_duration_ms: 0, - search_duration_ms: 0, - gap_duration_s: 0, - security_status: 'partial failure', - security_message: - 'Check privileges failed to execute ResponseError: index_not_found_exception: [index_not_found_exception] Reason: no such index [broken-index] name: "This Rule Makes Alerts, Actions, AND Moar!" id: "f78f3550-a186-11ec-89a1-0bce95157aba" rule id: "b64b4540-d035-4826-a1e7-f505bf4b9653" execution id: "254d8400-9dc7-43c5-ad4b-227273d1a44b" space ID: "default"', - }, - ], - total: 2, -}); - export const getBasicEmptySearchResponse = (): estypes.SearchResponse => ({ took: 1, timed_out: false, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.test.ts index 658461f148cb8..f3c4a698d83d2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.test.ts @@ -10,7 +10,6 @@ import { getEmptyFindResult, getRuleMock, getCreateRequest, - getRuleExecutionSummarySucceeded, getFindResultWithSingleHit, createMlRuleRequest, getBasicEmptySearchResponse, @@ -37,9 +36,6 @@ describe('create_rules', () => { clients.rulesClient.find.mockResolvedValue(getEmptyFindResult()); // no current rules clients.rulesClient.create.mockResolvedValue(getRuleMock(getQueryRuleParams())); // creation succeeds - clients.ruleExecutionLog.getExecutionSummary.mockResolvedValue( - getRuleExecutionSummarySucceeded() - ); context.core.elasticsearch.client.asCurrentUser.search.mockResolvedValue( elasticsearchClientMock.createSuccessTransportRequestPromise(getBasicEmptySearchResponse()) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_route.test.ts index b4aada2a60b69..0e26601d64547 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_route.test.ts @@ -14,7 +14,6 @@ import { getFindRequest, getFindResultWithSingleHit, getEmptySavedObjectsResponse, - getRuleExecutionSummaries, } from '../__mocks__/request_responses'; import { findRulesRoute } from './find_rules_route'; @@ -31,9 +30,6 @@ describe('find_rules', () => { clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); clients.rulesClient.get.mockResolvedValue(getRuleMock(getQueryRuleParams())); clients.savedObjectsClient.find.mockResolvedValue(getEmptySavedObjectsResponse()); - clients.ruleExecutionLog.getExecutionSummariesBulk.mockResolvedValue( - getRuleExecutionSummaries() - ); findRulesRoute(server.router, logger); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_rule_execution_events_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_rule_execution_events_route.test.ts deleted file mode 100644 index c59a0e4dfe176..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_rule_execution_events_route.test.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { serverMock, requestContextMock } from '../__mocks__'; -import { - getRuleExecutionEventsRequest, - getAggregateExecutionEvents, -} from '../__mocks__/request_responses'; -import { getRuleExecutionEventsRoute } from './get_rule_execution_events_route'; - -describe('getRuleExecutionEventsRoute', () => { - let server: ReturnType; - let { clients, context } = requestContextMock.createTools(); - - beforeEach(async () => { - server = serverMock.create(); - ({ clients, context } = requestContextMock.createTools()); - - getRuleExecutionEventsRoute(server.router); - }); - - describe('when it finds events in rule execution log', () => { - it('returns 200 response with the events', async () => { - const executionEvents = getAggregateExecutionEvents(); - clients.ruleExecutionLog.getAggregateExecutionEvents.mockResolvedValue(executionEvents); - - const response = await server.inject( - getRuleExecutionEventsRequest(), - requestContextMock.convertContext(context) - ); - - expect(response.status).toEqual(200); - expect(response.body).toEqual(executionEvents); - }); - }); - - describe('when rule execution log client throws an error', () => { - it('returns 500 response with it', async () => { - clients.ruleExecutionLog.getAggregateExecutionEvents.mockRejectedValue(new Error('Boom!')); - - const response = await server.inject( - getRuleExecutionEventsRequest(), - requestContextMock.convertContext(context) - ); - - expect(response.status).toEqual(500); - expect(response.body).toEqual({ - message: 'Boom!', - status_code: 500, - }); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts index 40a903ba1b61b..743fdefa7947f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts @@ -10,7 +10,6 @@ import { mlServicesMock, mlAuthzMock as mockMlAuthzFactory } from '../../../mach import { buildMlAuthz } from '../../../machine_learning/authz'; import { getEmptyFindResult, - getRuleExecutionSummarySucceeded, getRuleMock, getPatchRequest, getFindResultWithSingleHit, @@ -46,9 +45,6 @@ describe('patch_rules', () => { clients.rulesClient.get.mockResolvedValue(getRuleMock(getQueryRuleParams())); // existing rule clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); // existing rule clients.rulesClient.update.mockResolvedValue(getRuleMock(getQueryRuleParams())); // successful update - clients.ruleExecutionLog.getExecutionSummary.mockResolvedValue( - getRuleExecutionSummarySucceeded() - ); (legacyMigrate as jest.Mock).mockResolvedValue(getRuleMock(getQueryRuleParams())); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/preview_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/preview_rules_route.ts index 1589afed7bac5..21555117a1449 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/preview_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/preview_rules_route.ts @@ -36,7 +36,8 @@ import { import { wrapScopedClusterClient } from './utils/wrap_scoped_cluster_client'; import type { RulePreviewLogs } from '../../../../../common/detection_engine/schemas/request'; import { previewRulesSchema } from '../../../../../common/detection_engine/schemas/request'; -import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common'; +import { RuleExecutionStatus } from '../../../../../common/detection_engine/rule_monitoring'; +import type { RuleExecutionContext, StatusChangeArgs } from '../../rule_monitoring'; import type { ConfigType } from '../../../../config'; import { alertInstanceFactoryStub } from '../../signals/preview/alert_instance_factory_stub'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/read_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/read_rules_route.test.ts index 44c2c3a075508..368a2b50cd962 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/read_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/read_rules_route.test.ts @@ -16,7 +16,6 @@ import { getFindResultWithSingleHit, nonRuleFindResult, getEmptySavedObjectsResponse, - getRuleExecutionSummarySucceeded, resolveRuleMock, } from '../__mocks__/request_responses'; import { requestMock, requestContextMock, serverMock } from '../__mocks__'; @@ -35,9 +34,6 @@ describe('read_rules', () => { clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); // rule exists clients.savedObjectsClient.find.mockResolvedValue(getEmptySavedObjectsResponse()); // successful transform - clients.ruleExecutionLog.getExecutionSummary.mockResolvedValue( - getRuleExecutionSummarySucceeded() - ); clients.rulesClient.resolve.mockResolvedValue({ ...resolveRuleMock({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.test.ts index 7f2d5c3bde7e0..ad7a7d1fe5365 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.test.ts @@ -12,7 +12,6 @@ import { getRuleMock, getUpdateRequest, getFindResultWithSingleHit, - getRuleExecutionSummarySucceeded, nonRuleFindResult, typicalMlRulePayload, } from '../__mocks__/request_responses'; @@ -46,9 +45,6 @@ describe('update_rules', () => { clients.rulesClient.get.mockResolvedValue(getRuleMock(getQueryRuleParams())); // existing rule clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit()); // rule exists clients.rulesClient.update.mockResolvedValue(getRuleMock(getQueryRuleParams())); // successful update - clients.ruleExecutionLog.getExecutionSummary.mockResolvedValue( - getRuleExecutionSummarySucceeded() - ); clients.appClient.getSignalsIndex.mockReturnValue('.siem-signals-test-index'); (legacyMigrate as jest.Mock).mockResolvedValue(getRuleMock(getQueryRuleParams())); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts index 857b0df46ba24..2df4cb712ddd2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts @@ -13,7 +13,7 @@ import pMap from 'p-map'; import type { PartialRule, FindResult } from '@kbn/alerting-plugin/server'; import type { ActionsClient, FindActionResult } from '@kbn/actions-plugin/server'; -import type { RuleExecutionSummary } from '../../../../../common/detection_engine/schemas/common'; +import type { RuleExecutionSummary } from '../../../../../common/detection_engine/rule_monitoring'; import type { RulesSchema } from '../../../../../common/detection_engine/schemas/response/rules_schema'; import type { ImportRulesSchema } from '../../../../../common/detection_engine/schemas/request/import_rules_schema'; import type { CreateRulesBulkSchema } from '../../../../../common/detection_engine/schemas/request/create_rules_bulk_schema'; @@ -25,7 +25,7 @@ import { internalRuleToAPIResponse } from '../../schemas/rule_converters'; import type { RuleParams } from '../../schemas/rule_schemas'; // eslint-disable-next-line no-restricted-imports import type { LegacyRulesActionsSavedObject } from '../../rule_actions/legacy_get_rule_actions_saved_object'; -import type { RuleExecutionSummariesByRuleId } from '../../rule_execution_log'; +import type { RuleExecutionSummariesByRuleId } from '../../rule_monitoring'; type PromiseFromStreams = ImportRulesSchema | Error; const MAX_CONCURRENT_SEARCHES = 10; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.test.ts index fb038ebabe08e..21db7e52e4f8d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.test.ts @@ -8,7 +8,8 @@ import { transformValidate, transformValidateBulkError } from './validate'; import type { BulkError } from '../utils'; import type { RulesSchema } from '../../../../../common/detection_engine/schemas/response'; -import { getRuleMock, getRuleExecutionSummarySucceeded } from '../__mocks__/request_responses'; +import { getRuleMock } from '../__mocks__/request_responses'; +import { ruleExecutionSummaryMock } from '../../../../../common/detection_engine/rule_monitoring/mocks'; import { getListArrayMock } from '../../../../../common/detection_engine/schemas/types/lists.mock'; import { getThreatMock } from '../../../../../common/detection_engine/schemas/types/threat.mock'; import { getQueryRuleParams } from '../../schemas/rule_schemas.mock'; @@ -111,7 +112,7 @@ describe('validate', () => { test('it should do a validation correctly of a rule id with rule execution summary passed in', () => { const rule = getRuleMock(getQueryRuleParams()); - const ruleExecutionSumary = getRuleExecutionSummarySucceeded(); + const ruleExecutionSumary = ruleExecutionSummaryMock.getSummarySucceeded(); const validatedOrError = transformValidateBulkError('rule-1', rule, ruleExecutionSumary); const expected: RulesSchema = { ...ruleOutput(), diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.ts index cdecd3abf5960..4183f217a61fe 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.ts @@ -8,7 +8,7 @@ import { validateNonExact } from '@kbn/securitysolution-io-ts-utils'; import type { PartialRule } from '@kbn/alerting-plugin/server'; -import type { RuleExecutionSummary } from '../../../../../common/detection_engine/schemas/common'; +import type { RuleExecutionSummary } from '../../../../../common/detection_engine/rule_monitoring'; import type { FullResponseSchema } from '../../../../../common/detection_engine/schemas/request'; import { fullResponseSchema } from '../../../../../common/detection_engine/schemas/request'; import type { RulesSchema } from '../../../../../common/detection_engine/schemas/response/rules_schema'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/__mocks__/index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/__mocks__/index.ts deleted file mode 100644 index 666b35ed93f56..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/__mocks__/index.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { IRuleExecutionLogForRoutes } from '../client_for_routes/client_interface'; -import type { - IRuleExecutionLogForExecutors, - RuleExecutionContext, -} from '../client_for_executors/client_interface'; - -const ruleExecutionLogForRoutesMock = { - create: (): jest.Mocked => ({ - getAggregateExecutionEvents: jest.fn(), - getExecutionSummariesBulk: jest.fn(), - getExecutionSummary: jest.fn(), - clearExecutionSummary: jest.fn(), - getLastFailures: jest.fn(), - }), -}; - -const ruleExecutionLogForExecutorsMock = { - create: ( - context: Partial = {} - ): jest.Mocked => ({ - context: { - executionId: context.executionId ?? 'some execution id', - ruleId: context.ruleId ?? 'some rule id', - ruleName: context.ruleName ?? 'Some rule', - ruleType: context.ruleType ?? 'some rule type', - spaceId: context.spaceId ?? 'some space id', - }, - - logStatusChange: jest.fn(), - }), -}; - -export const ruleExecutionLogMock = { - forRoutes: ruleExecutionLogForRoutesMock, - forExecutors: ruleExecutionLogForExecutorsMock, -}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/client_factories.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/client_factories.ts deleted file mode 100644 index 330010acfbcc4..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/client_factories.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { Logger, SavedObjectsClientContract } from '@kbn/core/server'; -import type { IEventLogClient, IEventLogService } from '@kbn/event-log-plugin/server'; - -import type { IRuleExecutionLogForRoutes } from './client_for_routes/client_interface'; -import { createClientForRoutes } from './client_for_routes/client'; - -import type { - IRuleExecutionLogForExecutors, - RuleExecutionContext, -} from './client_for_executors/client_interface'; -import { createClientForExecutors } from './client_for_executors/client'; - -import { createEventLogReader } from './event_log/event_log_reader'; -import { createEventLogWriter } from './event_log/event_log_writer'; -import { createRuleExecutionSavedObjectsClient } from './execution_saved_object/saved_objects_client'; - -export type RuleExecutionLogForRoutesFactory = ( - savedObjectsClient: SavedObjectsClientContract, - eventLogClient: IEventLogClient, - logger: Logger -) => IRuleExecutionLogForRoutes; - -export const ruleExecutionLogForRoutesFactory: RuleExecutionLogForRoutesFactory = ( - savedObjectsClient, - eventLogClient, - logger -) => { - const soClient = createRuleExecutionSavedObjectsClient(savedObjectsClient, logger); - const eventLogReader = createEventLogReader(eventLogClient); - return createClientForRoutes(soClient, eventLogReader, logger); -}; - -export type RuleExecutionLogForExecutorsFactory = ( - savedObjectsClient: SavedObjectsClientContract, - eventLogService: IEventLogService, - logger: Logger, - context: RuleExecutionContext -) => IRuleExecutionLogForExecutors; - -export const ruleExecutionLogForExecutorsFactory: RuleExecutionLogForExecutorsFactory = ( - savedObjectsClient, - eventLogService, - logger, - context -) => { - const soClient = createRuleExecutionSavedObjectsClient(savedObjectsClient, logger); - const eventLogWriter = createEventLogWriter(eventLogService); - return createClientForExecutors(soClient, eventLogWriter, logger, context); -}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/client_for_executors/client.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/client_for_executors/client.ts deleted file mode 100644 index 46a16d567de7f..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/client_for_executors/client.ts +++ /dev/null @@ -1,156 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { sum } from 'lodash'; -import type { Duration } from 'moment'; -import type { Logger } from '@kbn/core/server'; - -import type { - RuleExecutionStatus, - RuleExecutionMetrics, -} from '../../../../../common/detection_engine/schemas/common'; -import { ruleExecutionStatusOrderByStatus } from '../../../../../common/detection_engine/schemas/common'; - -import { withSecuritySpan } from '../../../../utils/with_security_span'; -import type { ExtMeta } from '../utils/console_logging'; -import { truncateValue } from '../utils/normalization'; - -import type { IEventLogWriter } from '../event_log/event_log_writer'; -import type { IRuleExecutionSavedObjectsClient } from '../execution_saved_object/saved_objects_client'; -import type { - IRuleExecutionLogForExecutors, - RuleExecutionContext, - StatusChangeArgs, -} from './client_interface'; - -export const createClientForExecutors = ( - soClient: IRuleExecutionSavedObjectsClient, - eventLog: IEventLogWriter, - logger: Logger, - context: RuleExecutionContext -): IRuleExecutionLogForExecutors => { - const { executionId, ruleId, ruleName, ruleType, spaceId } = context; - - const client: IRuleExecutionLogForExecutors = { - get context() { - return context; - }, - - async logStatusChange(args) { - await withSecuritySpan('IRuleExecutionLogForExecutors.logStatusChange', async () => { - try { - const normalizedArgs = normalizeStatusChangeArgs(args); - await Promise.all([ - writeStatusChangeToSavedObjects(normalizedArgs), - writeStatusChangeToEventLog(normalizedArgs), - ]); - } catch (e) { - const logMessage = 'Error logging rule execution status change'; - const logAttributes = `status: "${args.newStatus}", rule id: "${ruleId}", rule name: "${ruleName}", execution uuid: "${executionId}"`; - const logReason = e instanceof Error ? e.stack ?? e.message : String(e); - const logMeta: ExtMeta = { - rule: { - id: ruleId, - name: ruleName, - type: ruleType, - execution: { - status: args.newStatus, - uuid: executionId, - }, - }, - kibana: { - spaceId, - }, - }; - - logger.error(`${logMessage}; ${logAttributes}; ${logReason}`, logMeta); - } - }); - }, - }; - - // TODO: Add executionId to new status SO? - const writeStatusChangeToSavedObjects = async ( - args: NormalizedStatusChangeArgs - ): Promise => { - const { newStatus, message, metrics } = args; - - await soClient.createOrUpdate(ruleId, { - last_execution: { - date: nowISO(), - status: newStatus, - status_order: ruleExecutionStatusOrderByStatus[newStatus], - message, - metrics: metrics ?? {}, - }, - }); - }; - - const writeStatusChangeToEventLog = (args: NormalizedStatusChangeArgs): void => { - const { newStatus, message, metrics } = args; - - if (metrics) { - eventLog.logExecutionMetrics({ - executionId, - ruleId, - ruleName, - ruleType, - spaceId, - metrics, - }); - } - - eventLog.logStatusChange({ - executionId, - ruleId, - ruleName, - ruleType, - spaceId, - newStatus, - message, - }); - }; - - return client; -}; - -const nowISO = () => new Date().toISOString(); - -interface NormalizedStatusChangeArgs { - newStatus: RuleExecutionStatus; - message: string; - metrics?: RuleExecutionMetrics; -} - -const normalizeStatusChangeArgs = (args: StatusChangeArgs): NormalizedStatusChangeArgs => { - const { newStatus, message, metrics } = args; - - return { - newStatus, - message: truncateValue(message) ?? '', - metrics: metrics - ? { - total_search_duration_ms: normalizeDurations(metrics.searchDurations), - total_indexing_duration_ms: normalizeDurations(metrics.indexingDurations), - execution_gap_duration_s: normalizeGap(metrics.executionGap), - } - : undefined, - }; -}; - -const normalizeDurations = (durations?: string[]): number | undefined => { - if (durations == null) { - return undefined; - } - - const sumAsFloat = sum(durations.map(Number)); - return Math.round(sumAsFloat); -}; - -const normalizeGap = (duration?: Duration): number | undefined => { - return duration ? Math.round(duration.asSeconds()) : undefined; -}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/client_for_executors/client_interface.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/client_for_executors/client_interface.ts deleted file mode 100644 index eca9c0d82c870..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/client_for_executors/client_interface.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { Duration } from 'moment'; -import type { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common'; - -/** - * Used from rule executors to log various information about the rule execution: - * - rule status changes - * - rule execution metrics - * - later - generic messages and any kind of info we'd need to log for rule - * monitoring or debugging purposes - */ -export interface IRuleExecutionLogForExecutors { - context: RuleExecutionContext; - - /** - * Writes information about new rule statuses and measured execution metrics: - * 1. To .kibana-* index as a custom `siem-detection-engine-rule-execution-info` saved object. - * This SO is used for fast access to last execution info of a large amount of rules. - * 2. To .kibana-event-log-* index in order to track history of rule executions. - * @param args Information about the status change event. - */ - logStatusChange(args: StatusChangeArgs): Promise; -} - -export interface RuleExecutionContext { - executionId: string; - ruleId: string; - ruleName: string; - ruleType: string; - spaceId: string; -} - -export interface StatusChangeArgs { - newStatus: RuleExecutionStatus; - message?: string; - metrics?: MetricsArgs; -} - -export interface MetricsArgs { - searchDurations?: string[]; - indexingDurations?: string[]; - executionGap?: Duration; -} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/client_for_routes/client_interface.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/client_for_routes/client_interface.ts deleted file mode 100644 index 0df4edc8ecdf3..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/client_for_routes/client_interface.ts +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import type { - ExecutionLogTableSortColumns, - RuleExecutionEvent, - RuleExecutionStatus, - RuleExecutionSummary, -} from '../../../../../common/detection_engine/schemas/common'; -import type { GetAggregateRuleExecutionEventsResponse } from '../../../../../common/detection_engine/schemas/response'; - -export interface GetAggregateExecutionEventsArgs { - ruleId: string; - start: string; - end: string; - queryText: string; - statusFilters: RuleExecutionStatus[]; - page: number; - perPage: number; - sortField: ExecutionLogTableSortColumns; - sortOrder: estypes.SortOrder; -} - -/** - * Used from route handlers to fetch and manage various information about the rule execution: - * - execution summary of a rule containing such data as the last status and metrics - * - execution events such as recent failures and status changes - */ -export interface IRuleExecutionLogForRoutes { - /** - * Fetches list of execution events aggregated by executionId, combining data from both alerting - * and security-solution event-log documents - * @param ruleId Saved object id of the rule (`rule.id`). - * @param start start of daterange to filter to - * @param end end of daterange to filter to - * @param queryText string of field-based filters, e.g. kibana.alert.rule.execution.status:* - * @param statusFilters array of status filters, e.g. ['succeeded', 'going to run'] - * @param page current page to fetch - * @param perPage number of results to fetch per page - * @param sortField field to sort by - * @param sortOrder what order to sort by (e.g. `asc` or `desc`) - */ - getAggregateExecutionEvents({ - ruleId, - start, - end, - queryText, - statusFilters, - page, - perPage, - sortField, - sortOrder, - }: GetAggregateExecutionEventsArgs): Promise; - - /** - * Fetches a list of current execution summaries of multiple rules. - * @param ruleIds A list of saved object ids of multiple rules (`rule.id`). - */ - getExecutionSummariesBulk(ruleIds: string[]): Promise; - - /** - * Fetches current execution summary of a given rule. - * @param ruleId Saved object id of the rule (`rule.id`). - */ - getExecutionSummary(ruleId: string): Promise; - - /** - * Deletes the current execution summary if it exists. - * @param ruleId Saved object id of the rule (`rule.id`). - */ - clearExecutionSummary(ruleId: string): Promise; - - /** - * Fetches last 5 failures (`RuleExecutionStatus.failed`) of a given rule. - * @param ruleId Saved object id of the rule (`rule.id`). - * @deprecated Will be replaced with a more flexible method for fetching execution events. - */ - getLastFailures(ruleId: string): Promise; -} - -export type RuleExecutionSummariesByRuleId = Record; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log/event_log_reader.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log/event_log_reader.ts deleted file mode 100644 index f8cc46e9419f9..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log/event_log_reader.ts +++ /dev/null @@ -1,178 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import type { IEventLogClient } from '@kbn/event-log-plugin/server'; -import { MAX_EXECUTION_EVENTS_DISPLAYED } from '@kbn/securitysolution-rules'; - -import type { - RuleExecutionEvent, - RuleExecutionStatus, -} from '../../../../../common/detection_engine/schemas/common'; -import type { GetAggregateRuleExecutionEventsResponse } from '../../../../../common/detection_engine/schemas/response'; -import { invariant } from '../../../../../common/utils/invariant'; -import { withSecuritySpan } from '../../../../utils/with_security_span'; -import type { GetAggregateExecutionEventsArgs } from '../client_for_routes/client_interface'; -import { - RULE_EXECUTION_LOG_PROVIDER, - RULE_SAVED_OBJECT_TYPE, - RuleExecutionLogAction, -} from './constants'; -import { - formatExecutionEventResponse, - getExecutionEventAggregation, - mapRuleExecutionStatusToPlatformStatus, -} from './get_execution_event_aggregation'; -import type { ExecutionUuidAggResult } from './get_execution_event_aggregation/types'; -import { EXECUTION_UUID_FIELD } from './get_execution_event_aggregation/types'; - -export interface IEventLogReader { - getAggregateExecutionEvents( - args: GetAggregateExecutionEventsArgs - ): Promise; - - getLastStatusChanges(args: GetLastStatusChangesArgs): Promise; -} - -export interface GetLastStatusChangesArgs { - ruleId: string; - count: number; - includeStatuses?: RuleExecutionStatus[]; -} - -export const createEventLogReader = (eventLog: IEventLogClient): IEventLogReader => { - return { - async getAggregateExecutionEvents( - args: GetAggregateExecutionEventsArgs - ): Promise { - const { ruleId, start, end, statusFilters, page, perPage, sortField, sortOrder } = args; - const soType = RULE_SAVED_OBJECT_TYPE; - const soIds = [ruleId]; - - // Current workaround to support root level filters without missing fields in the aggregate event - // or including events from statuses that aren't selected - // TODO: See: https://github.com/elastic/kibana/pull/127339/files#r825240516 - // First fetch execution uuid's by status filter if provided - let statusIds: string[] = []; - let totalExecutions: number | undefined; - // If 0 or 3 statuses are selected we can search for all statuses and don't need this pre-filter by ID - if (statusFilters.length > 0 && statusFilters.length < 3) { - const outcomes = mapRuleExecutionStatusToPlatformStatus(statusFilters); - const outcomeFilter = outcomes.length ? `OR event.outcome:(${outcomes.join(' OR ')})` : ''; - const statusResults = await eventLog.aggregateEventsBySavedObjectIds(soType, soIds, { - start, - end, - // Also query for `event.outcome` to catch executions that only contain platform events - filter: `kibana.alert.rule.execution.status:(${statusFilters.join( - ' OR ' - )}) ${outcomeFilter}`, - aggs: { - totalExecutions: { - cardinality: { - field: EXECUTION_UUID_FIELD, - }, - }, - filteredExecutionUUIDs: { - terms: { - field: EXECUTION_UUID_FIELD, - order: { executeStartTime: 'desc' }, - size: MAX_EXECUTION_EVENTS_DISPLAYED, - }, - aggs: { - executeStartTime: { - min: { - field: '@timestamp', - }, - }, - }, - }, - }, - }); - const filteredExecutionUUIDs = statusResults.aggregations - ?.filteredExecutionUUIDs as ExecutionUuidAggResult; - statusIds = filteredExecutionUUIDs?.buckets?.map((b) => b.key) ?? []; - totalExecutions = ( - statusResults.aggregations?.totalExecutions as estypes.AggregationsCardinalityAggregate - ).value; - // Early return if no results based on status filter - if (statusIds.length === 0) { - return { - total: 0, - events: [], - }; - } - } - - // Now query for aggregate events, and pass any ID's as filters as determined from the above status/queryText results - const idsFilter = statusIds.length - ? `kibana.alert.rule.execution.uuid:(${statusIds.join(' OR ')})` - : ''; - const results = await eventLog.aggregateEventsBySavedObjectIds(soType, soIds, { - start, - end, - filter: idsFilter, - aggs: getExecutionEventAggregation({ - maxExecutions: MAX_EXECUTION_EVENTS_DISPLAYED, - page, - perPage, - sort: [{ [sortField]: { order: sortOrder } }], - }), - }); - - return formatExecutionEventResponse(results, totalExecutions); - }, - async getLastStatusChanges(args) { - const soType = RULE_SAVED_OBJECT_TYPE; - const soIds = [args.ruleId]; - const count = args.count; - const includeStatuses = (args.includeStatuses ?? []).map((status) => `"${status}"`); - - const filterBy: string[] = [ - `event.provider: ${RULE_EXECUTION_LOG_PROVIDER}`, - 'event.kind: event', - `event.action: ${RuleExecutionLogAction['status-change']}`, - includeStatuses.length > 0 - ? `kibana.alert.rule.execution.status:${includeStatuses.join(' ')}` - : '', - ]; - - const kqlFilter = filterBy - .filter(Boolean) - .map((item) => `(${item})`) - .join(' and '); - - const findResult = await withSecuritySpan('findEventsBySavedObjectIds', () => { - return eventLog.findEventsBySavedObjectIds(soType, soIds, { - page: 1, - per_page: count, - sort: [{ sort_field: '@timestamp', sort_order: 'desc' }], - filter: kqlFilter, - }); - }); - - return findResult.data.map((event) => { - invariant(event, 'Event not found'); - invariant(event['@timestamp'], 'Required "@timestamp" field is not found'); - invariant( - event.kibana?.alert?.rule?.execution?.status, - 'Required "kibana.alert.rule.execution.status" field is not found' - ); - - const date = event['@timestamp']; - const status = event.kibana?.alert?.rule?.execution?.status as RuleExecutionStatus; - const message = event.message ?? ''; - const result: RuleExecutionEvent = { - date, - status, - message, - }; - - return result; - }); - }, - }; -}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log/event_log_writer.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log/event_log_writer.ts deleted file mode 100644 index be212cd80bd14..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log/event_log_writer.ts +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { SavedObjectsUtils } from '@kbn/core/server'; -import type { IEventLogService } from '@kbn/event-log-plugin/server'; -import { SAVED_OBJECT_REL_PRIMARY } from '@kbn/event-log-plugin/server'; -import type { - RuleExecutionStatus, - RuleExecutionMetrics, -} from '../../../../../common/detection_engine/schemas/common'; -import { ruleExecutionStatusOrderByStatus } from '../../../../../common/detection_engine/schemas/common'; -import { - RULE_SAVED_OBJECT_TYPE, - RULE_EXECUTION_LOG_PROVIDER, - RuleExecutionLogAction, -} from './constants'; - -export interface IEventLogWriter { - logStatusChange(args: StatusChangeArgs): void; - logExecutionMetrics(args: ExecutionMetricsArgs): void; -} - -export interface BaseArgs { - executionId: string; - ruleId: string; - ruleName: string; - ruleType: string; - spaceId: string; -} - -export interface StatusChangeArgs extends BaseArgs { - newStatus: RuleExecutionStatus; - message?: string; -} - -export interface ExecutionMetricsArgs extends BaseArgs { - metrics: RuleExecutionMetrics; -} - -export const createEventLogWriter = (eventLogService: IEventLogService): IEventLogWriter => { - const eventLogger = eventLogService.getLogger({ - event: { provider: RULE_EXECUTION_LOG_PROVIDER }, - }); - - let sequence = 0; - - return { - logStatusChange({ executionId, ruleId, ruleName, ruleType, spaceId, newStatus, message }) { - eventLogger.logEvent({ - '@timestamp': nowISO(), - message, - rule: { - id: ruleId, - name: ruleName, - category: ruleType, - }, - event: { - kind: 'event', - action: RuleExecutionLogAction['status-change'], - sequence: sequence++, - }, - kibana: { - alert: { - rule: { - execution: { - status: newStatus, - status_order: ruleExecutionStatusOrderByStatus[newStatus], - uuid: executionId, - }, - }, - }, - space_ids: [spaceId], - saved_objects: [ - { - rel: SAVED_OBJECT_REL_PRIMARY, - type: RULE_SAVED_OBJECT_TYPE, - id: ruleId, - namespace: spaceIdToNamespace(spaceId), - }, - ], - }, - }); - }, - - logExecutionMetrics({ executionId, ruleId, ruleName, ruleType, spaceId, metrics }) { - eventLogger.logEvent({ - '@timestamp': nowISO(), - rule: { - id: ruleId, - name: ruleName, - category: ruleType, - }, - event: { - kind: 'metric', - action: RuleExecutionLogAction['execution-metrics'], - sequence: sequence++, - }, - kibana: { - alert: { - rule: { - execution: { - metrics, - uuid: executionId, - }, - }, - }, - space_ids: [spaceId], - saved_objects: [ - { - rel: SAVED_OBJECT_REL_PRIMARY, - type: RULE_SAVED_OBJECT_TYPE, - id: ruleId, - namespace: spaceIdToNamespace(spaceId), - }, - ], - }, - }); - }, - }; -}; - -const nowISO = () => new Date().toISOString(); - -const spaceIdToNamespace = SavedObjectsUtils.namespaceStringToId; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/utils/console_logging.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/utils/console_logging.ts deleted file mode 100644 index 89f776a06dd11..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/utils/console_logging.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { LogMeta } from '@kbn/core/server'; -import type { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common'; - -/** - * Custom extended log metadata that rule execution logger can attach to every log record. - */ -export type ExtMeta = LogMeta & { - rule?: LogMeta['rule'] & { - type?: string; - execution?: { - status?: RuleExecutionStatus; - }; - }; - kibana?: { - spaceId?: string; - }; -}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/api/get_rule_execution_events/route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/api/get_rule_execution_events/route.test.ts new file mode 100644 index 0000000000000..519be6d429e9d --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/api/get_rule_execution_events/route.test.ts @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { serverMock, requestContextMock, requestMock } from '../../../routes/__mocks__'; + +import { + GET_RULE_EXECUTION_EVENTS_URL, + LogLevel, + RuleExecutionEventType, +} from '../../../../../../common/detection_engine/rule_monitoring'; +import { getRuleExecutionEventsResponseMock } from '../../../../../../common/detection_engine/rule_monitoring/mocks'; +import type { GetExecutionEventsArgs } from '../../logic/rule_execution_log'; +import { getRuleExecutionEventsRoute } from './route'; + +describe('getRuleExecutionEventsRoute', () => { + let server: ReturnType; + let { clients, context } = requestContextMock.createTools(); + + beforeEach(async () => { + server = serverMock.create(); + ({ clients, context } = requestContextMock.createTools()); + + getRuleExecutionEventsRoute(server.router); + }); + + const getRuleExecutionEventsRequest = () => + requestMock.create({ + method: 'get', + path: GET_RULE_EXECUTION_EVENTS_URL, + params: { + ruleId: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + }, + query: { + event_types: `${RuleExecutionEventType['status-change']}`, + log_levels: `${LogLevel.debug},${LogLevel.info}`, + page: 3, + }, + }); + + it('passes request arguments to rule execution log', async () => { + const expectedArgs: GetExecutionEventsArgs = { + ruleId: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + eventTypes: [RuleExecutionEventType['status-change']], + logLevels: [LogLevel.debug, LogLevel.info], + sortOrder: 'desc', + page: 3, + perPage: 20, + }; + + await server.inject( + getRuleExecutionEventsRequest(), + requestContextMock.convertContext(context) + ); + + expect(clients.ruleExecutionLog.getExecutionEvents).toHaveBeenCalledTimes(1); + expect(clients.ruleExecutionLog.getExecutionEvents).toHaveBeenCalledWith(expectedArgs); + }); + + describe('when it finds events in rule execution log', () => { + it('returns 200 response with the events', async () => { + const events = getRuleExecutionEventsResponseMock.getSomeResponse(); + clients.ruleExecutionLog.getExecutionEvents.mockResolvedValue(events); + + const response = await server.inject( + getRuleExecutionEventsRequest(), + requestContextMock.convertContext(context) + ); + + expect(response.status).toEqual(200); + expect(response.body).toEqual(events); + }); + }); + + describe('when rule execution log client throws an error', () => { + it('returns 500 response with it', async () => { + clients.ruleExecutionLog.getExecutionEvents.mockRejectedValue(new Error('Boom!')); + + const response = await server.inject( + getRuleExecutionEventsRequest(), + requestContextMock.convertContext(context) + ); + + expect(response.status).toEqual(500); + expect(response.body).toEqual({ + message: 'Boom!', + status_code: 500, + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/api/get_rule_execution_events/route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/api/get_rule_execution_events/route.ts new file mode 100644 index 0000000000000..109c15409afae --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/api/get_rule_execution_events/route.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { transformError } from '@kbn/securitysolution-es-utils'; +import { buildRouteValidation } from '../../../../../utils/build_validation/route_validation'; +import { buildSiemResponse } from '../../../routes/utils'; +import type { SecuritySolutionPluginRouter } from '../../../../../types'; + +import type { GetRuleExecutionEventsResponse } from '../../../../../../common/detection_engine/rule_monitoring'; +import { + GET_RULE_EXECUTION_EVENTS_URL, + GetRuleExecutionEventsRequestParams, + GetRuleExecutionEventsRequestQuery, +} from '../../../../../../common/detection_engine/rule_monitoring'; + +/** + * Returns execution events of a given rule (e.g. status changes) from Event Log. + * Accepts rule's saved object ID (`rule.id`) and options for filtering, sorting and pagination. + */ +export const getRuleExecutionEventsRoute = (router: SecuritySolutionPluginRouter) => { + router.get( + { + path: GET_RULE_EXECUTION_EVENTS_URL, + validate: { + params: buildRouteValidation(GetRuleExecutionEventsRequestParams), + query: buildRouteValidation(GetRuleExecutionEventsRequestQuery), + }, + options: { + tags: ['access:securitySolution'], + }, + }, + async (context, request, response) => { + const { params, query } = request; + const siemResponse = buildSiemResponse(response); + + try { + const ctx = await context.resolve(['securitySolution']); + const executionLog = ctx.securitySolution.getRuleExecutionLog(); + const executionEventsResponse = await executionLog.getExecutionEvents({ + ruleId: params.ruleId, + eventTypes: query.event_types, + logLevels: query.log_levels, + sortOrder: query.sort_order, + page: query.page, + perPage: query.per_page, + }); + + const responseBody: GetRuleExecutionEventsResponse = executionEventsResponse; + + return response.ok({ body: responseBody }); + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/api/get_rule_execution_results/route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/api/get_rule_execution_results/route.test.ts new file mode 100644 index 0000000000000..e041670c0b631 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/api/get_rule_execution_results/route.test.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { serverMock, requestContextMock, requestMock } from '../../../routes/__mocks__'; + +import { GET_RULE_EXECUTION_RESULTS_URL } from '../../../../../../common/detection_engine/rule_monitoring'; +import { getRuleExecutionResultsResponseMock } from '../../../../../../common/detection_engine/rule_monitoring/mocks'; +import { getRuleExecutionResultsRoute } from './route'; + +describe('getRuleExecutionResultsRoute', () => { + let server: ReturnType; + let { clients, context } = requestContextMock.createTools(); + + const getRuleExecutionResultsRequest = () => + requestMock.create({ + method: 'get', + path: GET_RULE_EXECUTION_RESULTS_URL, + params: { + ruleId: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + }, + query: { + start: '2022-03-31T22:02:01.622Z', + end: '2022-03-31T22:02:31.622Z', + }, + }); + + beforeEach(async () => { + server = serverMock.create(); + ({ clients, context } = requestContextMock.createTools()); + + getRuleExecutionResultsRoute(server.router); + }); + + describe('when it finds results in rule execution log', () => { + it('returns 200 response with the results', async () => { + const results = getRuleExecutionResultsResponseMock.getSomeResponse(); + clients.ruleExecutionLog.getExecutionResults.mockResolvedValue(results); + + const response = await server.inject( + getRuleExecutionResultsRequest(), + requestContextMock.convertContext(context) + ); + + expect(response.status).toEqual(200); + expect(response.body).toEqual(results); + }); + }); + + describe('when rule execution log client throws an error', () => { + it('returns 500 response with it', async () => { + clients.ruleExecutionLog.getExecutionResults.mockRejectedValue(new Error('Boom!')); + + const response = await server.inject( + getRuleExecutionResultsRequest(), + requestContextMock.convertContext(context) + ); + + expect(response.status).toEqual(500); + expect(response.body).toEqual({ + message: 'Boom!', + status_code: 500, + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_rule_execution_events_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/api/get_rule_execution_results/route.ts similarity index 52% rename from x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_rule_execution_events_route.ts rename to x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/api/get_rule_execution_results/route.ts index 1cfb7871dbf0f..ff1523502aaea 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/get_rule_execution_events_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/api/get_rule_execution_results/route.ts @@ -6,28 +6,28 @@ */ import { transformError } from '@kbn/securitysolution-es-utils'; -import type { GetAggregateRuleExecutionEventsResponse } from '../../../../../common/detection_engine/schemas/response'; -import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; -import { buildSiemResponse } from '../utils'; -import type { SecuritySolutionPluginRouter } from '../../../../types'; +import { buildRouteValidation } from '../../../../../utils/build_validation/route_validation'; +import { buildSiemResponse } from '../../../routes/utils'; +import type { SecuritySolutionPluginRouter } from '../../../../../types'; -import { DETECTION_ENGINE_RULE_EXECUTION_EVENTS_URL } from '../../../../../common/constants'; +import type { GetRuleExecutionResultsResponse } from '../../../../../../common/detection_engine/rule_monitoring'; import { - GetRuleExecutionEventsQueryParams, - GetRuleExecutionEventsRequestParams, -} from '../../../../../common/detection_engine/schemas/request/get_rule_execution_events_schema'; + GET_RULE_EXECUTION_RESULTS_URL, + GetRuleExecutionResultsRequestParams, + GetRuleExecutionResultsRequestQuery, +} from '../../../../../../common/detection_engine/rule_monitoring'; /** - * Returns execution events of a given rule (aggregated by executionId) from Event Log. + * Returns execution results of a given rule (aggregated by execution UUID) from Event Log. * Accepts rule's saved object ID (`rule.id`), `start`, `end` and `filters` query params. */ -export const getRuleExecutionEventsRoute = (router: SecuritySolutionPluginRouter) => { +export const getRuleExecutionResultsRoute = (router: SecuritySolutionPluginRouter) => { router.get( { - path: DETECTION_ENGINE_RULE_EXECUTION_EVENTS_URL, + path: GET_RULE_EXECUTION_RESULTS_URL, validate: { - params: buildRouteValidation(GetRuleExecutionEventsRequestParams), - query: buildRouteValidation(GetRuleExecutionEventsQueryParams), + params: buildRouteValidation(GetRuleExecutionResultsRequestParams), + query: buildRouteValidation(GetRuleExecutionResultsRequestQuery), }, options: { tags: ['access:securitySolution'], @@ -45,11 +45,13 @@ export const getRuleExecutionEventsRoute = (router: SecuritySolutionPluginRouter sort_field: sortField, sort_order: sortOrder, } = request.query; + const siemResponse = buildSiemResponse(response); try { - const executionLog = (await context.securitySolution).getRuleExecutionLog(); - const { events, total } = await executionLog.getAggregateExecutionEvents({ + const ctx = await context.resolve(['securitySolution']); + const executionLog = ctx.securitySolution.getRuleExecutionLog(); + const executionResultsResponse = await executionLog.getExecutionResults({ ruleId, start, end, @@ -61,10 +63,7 @@ export const getRuleExecutionEventsRoute = (router: SecuritySolutionPluginRouter sortOrder, }); - const responseBody: GetAggregateRuleExecutionEventsResponse = { - events, - total, - }; + const responseBody: GetRuleExecutionResultsResponse = executionResultsResponse; return response.ok({ body: responseBody }); } catch (err) { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/api/register_routes.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/api/register_routes.ts new file mode 100644 index 0000000000000..c63cf638e7df2 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/api/register_routes.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { SecuritySolutionPluginRouter } from '../../../../types'; +import { getRuleExecutionEventsRoute } from './get_rule_execution_events/route'; +import { getRuleExecutionResultsRoute } from './get_rule_execution_results/route'; + +export const registerRuleMonitoringRoutes = (router: SecuritySolutionPluginRouter) => { + getRuleExecutionEventsRoute(router); + getRuleExecutionResultsRoute(router); +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/index.ts new file mode 100644 index 0000000000000..ca1b22776c247 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './api/register_routes'; +export * from './logic/rule_execution_log'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/README.md b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/README.md new file mode 100644 index 0000000000000..01372bf0c4da1 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/README.md @@ -0,0 +1,195 @@ +# Rule Execution Log + +## Summary + +Rule Execution Log is used to write various execution events to a number of destinations, and then query those +destinations to be able to show plain or aggregated execution data in the app. + +Events we log: + +- Rule execution status changes. See `RuleExecutionEventType['status-change']` and `RuleExecutionStatus`. +- Execution metrics. See `RuleExecutionEventType['execution-metrics']` and `RuleExecutionMetrics`. +- Simple messages. See `RuleExecutionEventType.message`. + +Destinations we write execution logs to: + +- Console Kibana logs. + - Written via an instance of Kibana system `Logger`. +- Event Log (`.kibana-event-log-*` indices). + - Written via an instance of `IEventLogger` from the `event_log` plugin. +- Rule's sidecar saved objects of type `siem-detection-engine-rule-execution-info`. + - Written via an instance of `SavedObjectsClientContract`. + +There are two main interfaces for using Rule Execution Log, these are entrypoints that you can use to start +exploring this implementation: + +- `IRuleExecutionLogForExecutors` - intended to be used from rule executor functions, mainly for the purpose + of writing execution events. +- `IRuleExecutionLogForRoutes` - intended to be used from the API route handlers, mainly for the purpose + of reading (filtering, sorting, searching, aggregating) execution events. + +## Writing status changes + +When we log a rule status change, we do several things: + +- Create or update a `siem-detection-engine-rule-execution-info` sidecar saved object. + Every rule can have exactly 0 or 1 execution info SOs associated with it. + We use it to quickly fetch N execution SOs for N rules to show the rules in a table. +- Write 2 events to Event Log: `execution-metrics` and `status-change`. + These events can be used to show the Rule Execution Log UI on the Rule Details page. +- Write the status change message to console logs (if provided). +- Write the new status itself to console logs. + +This is done by calling the `IRuleExecutionLogForExecutors.logStatusChange` method. + +## Writing console logs + +Console logs from rule executors are written via a logger with the name `plugins.securitySolution.ruleExecution`. +This allows to turn on _only_ rule execution logs in the Kibana config (could be useful when debugging): + +```yaml +logging: + appenders: + custom_console: + type: console + layout: + type: pattern + highlight: true + pattern: "[%date][%level][%logger] %message" + root: + appenders: [custom_console] + level: off + loggers: + - name: plugins.securitySolution.ruleExecution + level: debug # or trace +``` + +Every log message has a suffix with correlation ids: + +```txt +[siem.queryRule][Endpoint Security][rule id 825b2fab-8b3e-11ec-a4a0-cf820453283c][rule uuid 9a1a2dae-0b5f-4c3d-8305-a268d404c306][exec id ebb7f713-b216-4c90-a456-6c1a6815a065][space default] +``` + +You can also enable printing additional log metadata objects associated with every log record by changing the pattern: + +```yaml +# The metadata object will be printed after the log message +pattern: "[%date][%level][%logger] %message %meta" +``` + +Example of such an object (see `ExtMeta` type for details): + +```txt +{"rule":{"id":"420e1ed0-8f75-11ec-9aaf-c925ad1b24ee","uuid":"9be1325f-7b00-467b-80f1-90d594c22bf4","name":"Test ip range - exceptions with is operator","type":"siem.queryRule","execution":{"uuid":"8d79919b-b09e-4243-ac0c-a4115cd1225f"}},"kibana":{"spaceId":"default"}} +``` + +Example of logs written during a single execution of the "Endpoint Security" rule: + +```txt +[2022-02-23T17:05:09.901+03:00][DEBUG][plugins.securitySolution.ruleExecution] [+] Starting Signal Rule execution [siem.queryRule][Endpoint Security][rule id 825b2fab-8b3e-11ec-a4a0-cf820453283c][rule uuid 9a1a2dae-0b5f-4c3d-8305-a268d404c306][exec id ebb7f713-b216-4c90-a456-6c1a6815a065][space default] +[2022-02-23T17:05:09.907+03:00][DEBUG][plugins.securitySolution.ruleExecution] interval: 5m [siem.queryRule][Endpoint Security][rule id 825b2fab-8b3e-11ec-a4a0-cf820453283c][rule uuid 9a1a2dae-0b5f-4c3d-8305-a268d404c306][exec id ebb7f713-b216-4c90-a456-6c1a6815a065][space default] +[2022-02-23T17:05:09.908+03:00][INFO ][plugins.securitySolution.ruleExecution] Changing rule status to "running" [siem.queryRule][Endpoint Security][rule id 825b2fab-8b3e-11ec-a4a0-cf820453283c][rule uuid 9a1a2dae-0b5f-4c3d-8305-a268d404c306][exec id ebb7f713-b216-4c90-a456-6c1a6815a065][space default] +[2022-02-23T17:05:10.595+03:00][WARN ][plugins.securitySolution.ruleExecution] This rule is attempting to query data from Elasticsearch indices listed in the "Index pattern" section of the rule definition, however no index matching: ["logs-endpoint.alerts-*"] was found. This warning will continue to appear until a matching index is created or this rule is de-activated. If you have recently enrolled agents enabled with Endpoint Security through Fleet, this warning should stop once an alert is sent from an agent. [siem.queryRule][Endpoint Security][rule id 825b2fab-8b3e-11ec-a4a0-cf820453283c][rule uuid 9a1a2dae-0b5f-4c3d-8305-a268d404c306][exec id ebb7f713-b216-4c90-a456-6c1a6815a065][space default] +[2022-02-23T17:05:10.595+03:00][WARN ][plugins.securitySolution.ruleExecution] Changing rule status to "partial failure" [siem.queryRule][Endpoint Security][rule id 825b2fab-8b3e-11ec-a4a0-cf820453283c][rule uuid 9a1a2dae-0b5f-4c3d-8305-a268d404c306][exec id ebb7f713-b216-4c90-a456-6c1a6815a065][space default] +[2022-02-23T17:05:11.630+03:00][DEBUG][plugins.securitySolution.ruleExecution] sortIds: undefined [siem.queryRule][Endpoint Security][rule id 825b2fab-8b3e-11ec-a4a0-cf820453283c][rule uuid 9a1a2dae-0b5f-4c3d-8305-a268d404c306][exec id ebb7f713-b216-4c90-a456-6c1a6815a065][space default] +[2022-02-23T17:05:11.634+03:00][DEBUG][plugins.securitySolution.ruleExecution] totalHits: 0 [siem.queryRule][Endpoint Security][rule id 825b2fab-8b3e-11ec-a4a0-cf820453283c][rule uuid 9a1a2dae-0b5f-4c3d-8305-a268d404c306][exec id ebb7f713-b216-4c90-a456-6c1a6815a065][space default] +[2022-02-23T17:05:11.634+03:00][DEBUG][plugins.securitySolution.ruleExecution] searchResult.hit.hits.length: 0 [siem.queryRule][Endpoint Security][rule id 825b2fab-8b3e-11ec-a4a0-cf820453283c][rule uuid 9a1a2dae-0b5f-4c3d-8305-a268d404c306][exec id ebb7f713-b216-4c90-a456-6c1a6815a065][space default] +[2022-02-23T17:05:11.635+03:00][DEBUG][plugins.securitySolution.ruleExecution] totalHits was 0, exiting early [siem.queryRule][Endpoint Security][rule id 825b2fab-8b3e-11ec-a4a0-cf820453283c][rule uuid 9a1a2dae-0b5f-4c3d-8305-a268d404c306][exec id ebb7f713-b216-4c90-a456-6c1a6815a065][space default] +[2022-02-23T17:05:11.636+03:00][DEBUG][plugins.securitySolution.ruleExecution] [+] completed bulk index of 0 [siem.queryRule][Endpoint Security][rule id 825b2fab-8b3e-11ec-a4a0-cf820453283c][rule uuid 9a1a2dae-0b5f-4c3d-8305-a268d404c306][exec id ebb7f713-b216-4c90-a456-6c1a6815a065][space default] +[2022-02-23T17:05:11.636+03:00][DEBUG][plugins.securitySolution.ruleExecution] [+] Signal Rule execution completed. [siem.queryRule][Endpoint Security][rule id 825b2fab-8b3e-11ec-a4a0-cf820453283c][rule uuid 9a1a2dae-0b5f-4c3d-8305-a268d404c306][exec id ebb7f713-b216-4c90-a456-6c1a6815a065][space default] +[2022-02-23T17:05:11.638+03:00][DEBUG][plugins.securitySolution.ruleExecution] [+] Finished indexing 0 signals into .alerts-security.alerts [siem.queryRule][Endpoint Security][rule id 825b2fab-8b3e-11ec-a4a0-cf820453283c][rule uuid 9a1a2dae-0b5f-4c3d-8305-a268d404c306][exec id ebb7f713-b216-4c90-a456-6c1a6815a065][space default] +[2022-02-23T17:05:11.639+03:00][DEBUG][plugins.securitySolution.ruleExecution] [+] Finished indexing 0 signals searched between date ranges [ + { + "to": "2022-02-23T14:05:09.775Z", + "from": "2022-02-23T13:55:09.775Z", + "maxSignals": 10000 + } +] [siem.queryRule][Endpoint Security][rule id 825b2fab-8b3e-11ec-a4a0-cf820453283c][rule uuid 9a1a2dae-0b5f-4c3d-8305-a268d404c306][exec id ebb7f713-b216-4c90-a456-6c1a6815a065][space default] +``` + +## Finding rule execution data in Elasticsearch + +These are some queries for Kibana Dev Tools that you can use to find execution data associated with a given rule. + +```txt +# Sidecar siem-detection-engine-rule-execution-info saved object +# Rule id: 825b2fab-8b3e-11ec-a4a0-cf820453283c +GET /.kibana/_search +{ + "query": { + "bool": { + "filter": [ + { + "term": { + "type": "siem-detection-engine-rule-execution-info" + } + }, + { + "nested": { + "path": "references", + "query": { + "term": { + "references.id": "825b2fab-8b3e-11ec-a4a0-cf820453283c" + } + } + } + } + ] + } + } +} +``` + +```txt +# Events of type "status-change" written to Event Log +# Rule id: 825b2fab-8b3e-11ec-a4a0-cf820453283c +GET /.kibana-event-log-*/_search +{ + "query": { + "bool": { + "filter": [ + { + "term": { "event.provider": "securitySolution.ruleExecution" } + }, + { + "term": { "event.action": "status-change" } + }, + { + "term": { "rule.id": "825b2fab-8b3e-11ec-a4a0-cf820453283c" } + } + ] + } + }, + "sort": [ + { "@timestamp": { "order": "desc" } }, + { "event.sequence": { "order": "desc" } } + ] +} +``` + +```txt +# Events of type "execution-metrics" written to Event Log +# Rule id: 825b2fab-8b3e-11ec-a4a0-cf820453283c +GET /.kibana-event-log-*/_search +{ + "query": { + "bool": { + "filter": [ + { + "term": { "event.provider": "securitySolution.ruleExecution" } + }, + { + "term": { "event.action": "execution-metrics" } + }, + { + "term": { "rule.id": "825b2fab-8b3e-11ec-a4a0-cf820453283c" } + } + ] + } + }, + "sort": [ + { "@timestamp": { "order": "desc" } }, + { "event.sequence": { "order": "desc" } } + ] +} +``` diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/__mocks__/index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/__mocks__/index.ts new file mode 100644 index 0000000000000..e43a03c46a906 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/__mocks__/index.ts @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + getRuleExecutionEventsResponseMock, + getRuleExecutionResultsResponseMock, + ruleExecutionSummaryMock, +} from '../../../../../../../common/detection_engine/rule_monitoring/mocks'; + +import type { IRuleExecutionLogForRoutes } from '../client_for_routes/client_interface'; +import type { + IRuleExecutionLogForExecutors, + RuleExecutionContext, +} from '../client_for_executors/client_interface'; + +type GetExecutionSummariesBulk = IRuleExecutionLogForRoutes['getExecutionSummariesBulk']; +type GetExecutionSummary = IRuleExecutionLogForRoutes['getExecutionSummary']; +type ClearExecutionSummary = IRuleExecutionLogForRoutes['clearExecutionSummary']; +type GetExecutionEvents = IRuleExecutionLogForRoutes['getExecutionEvents']; +type GetExecutionResults = IRuleExecutionLogForRoutes['getExecutionResults']; + +const ruleExecutionLogForRoutesMock = { + create: (): jest.Mocked => ({ + getExecutionSummariesBulk: jest + .fn, Parameters>() + .mockResolvedValue({ + '04128c15-0d1b-4716-a4c5-46997ac7f3bd': ruleExecutionSummaryMock.getSummarySucceeded(), + '1ea5a820-4da1-4e82-92a1-2b43a7bece08': ruleExecutionSummaryMock.getSummaryFailed(), + }), + + getExecutionSummary: jest + .fn, Parameters>() + .mockResolvedValue(ruleExecutionSummaryMock.getSummarySucceeded()), + + clearExecutionSummary: jest + .fn, Parameters>() + .mockResolvedValue(), + + getExecutionEvents: jest + .fn, Parameters>() + .mockResolvedValue(getRuleExecutionEventsResponseMock.getSomeResponse()), + + getExecutionResults: jest + .fn, Parameters>() + .mockResolvedValue(getRuleExecutionResultsResponseMock.getSomeResponse()), + }), +}; + +const ruleExecutionLogForExecutorsMock = { + create: ( + context: Partial = {} + ): jest.Mocked => ({ + context: { + executionId: context.executionId ?? 'some execution id', + ruleId: context.ruleId ?? 'some rule id', + ruleUuid: context.ruleUuid ?? 'some rule uuid', + ruleName: context.ruleName ?? 'Some rule', + ruleType: context.ruleType ?? 'some rule type', + spaceId: context.spaceId ?? 'some space id', + }, + + trace: jest.fn(), + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + + logStatusChange: jest.fn(), + }), +}; + +export const ruleExecutionLogMock = { + forRoutes: ruleExecutionLogForRoutesMock, + forExecutors: ruleExecutionLogForExecutorsMock, +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_executors/client.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_executors/client.ts new file mode 100644 index 0000000000000..4116848b1ffcf --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_executors/client.ts @@ -0,0 +1,242 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { sum } from 'lodash'; +import type { Duration } from 'moment'; +import type { Logger } from '@kbn/core/server'; + +import type { + RuleExecutionMetrics, + RuleExecutionSettings, + RuleExecutionStatus, +} from '../../../../../../../common/detection_engine/rule_monitoring'; +import { + LogLevel, + logLevelFromExecutionStatus, + LogLevelSetting, + logLevelToNumber, + ruleExecutionStatusToNumber, +} from '../../../../../../../common/detection_engine/rule_monitoring'; + +import { assertUnreachable } from '../../../../../../../common/utility_types'; +import { withSecuritySpan } from '../../../../../../utils/with_security_span'; +import { truncateValue } from '../utils/normalization'; +import type { ExtMeta } from '../utils/console_logging'; +import { getCorrelationIds } from './correlation_ids'; + +import type { IEventLogWriter } from '../event_log/event_log_writer'; +import type { IRuleExecutionSavedObjectsClient } from '../execution_saved_object/saved_objects_client'; +import type { + IRuleExecutionLogForExecutors, + RuleExecutionContext, + StatusChangeArgs, +} from './client_interface'; + +export const createClientForExecutors = ( + settings: RuleExecutionSettings, + soClient: IRuleExecutionSavedObjectsClient, + eventLog: IEventLogWriter, + logger: Logger, + context: RuleExecutionContext +): IRuleExecutionLogForExecutors => { + const baseCorrelationIds = getCorrelationIds(context); + const baseLogSuffix = baseCorrelationIds.getLogSuffix(); + const baseLogMeta = baseCorrelationIds.getLogMeta(); + + const { executionId, ruleId, ruleUuid, ruleName, ruleType, spaceId } = context; + + const client: IRuleExecutionLogForExecutors = { + get context() { + return context; + }, + + trace(...messages: string[]): void { + writeMessage(messages, LogLevel.trace); + }, + + debug(...messages: string[]): void { + writeMessage(messages, LogLevel.debug); + }, + + info(...messages: string[]): void { + writeMessage(messages, LogLevel.info); + }, + + warn(...messages: string[]): void { + writeMessage(messages, LogLevel.warn); + }, + + error(...messages: string[]): void { + writeMessage(messages, LogLevel.error); + }, + + async logStatusChange(args: StatusChangeArgs): Promise { + await withSecuritySpan('IRuleExecutionLogForExecutors.logStatusChange', async () => { + const correlationIds = baseCorrelationIds.withStatus(args.newStatus); + const logMeta = correlationIds.getLogMeta(); + + try { + const normalizedArgs = normalizeStatusChangeArgs(args); + + await Promise.all([ + writeStatusChangeToConsole(normalizedArgs, logMeta), + writeStatusChangeToSavedObjects(normalizedArgs), + writeStatusChangeToEventLog(normalizedArgs), + ]); + } catch (e) { + const logMessage = `Error changing rule status to "${args.newStatus}"`; + writeExceptionToConsole(e, logMessage, logMeta); + } + }); + }, + }; + + const writeMessage = (messages: string[], logLevel: LogLevel): void => { + const message = messages.join(' '); + writeMessageToConsole(message, logLevel, baseLogMeta); + writeMessageToEventLog(message, logLevel); + }; + + const writeMessageToConsole = (message: string, logLevel: LogLevel, logMeta: ExtMeta): void => { + switch (logLevel) { + case LogLevel.trace: + logger.trace(`${message} ${baseLogSuffix}`, logMeta); + break; + case LogLevel.debug: + logger.debug(`${message} ${baseLogSuffix}`, logMeta); + break; + case LogLevel.info: + logger.info(`${message} ${baseLogSuffix}`, logMeta); + break; + case LogLevel.warn: + logger.warn(`${message} ${baseLogSuffix}`, logMeta); + break; + case LogLevel.error: + logger.error(`${message} ${baseLogSuffix}`, logMeta); + break; + default: + assertUnreachable(logLevel); + } + }; + + const writeMessageToEventLog = (message: string, logLevel: LogLevel): void => { + const { isEnabled, minLevel } = settings.extendedLogging; + + if (!isEnabled || minLevel === LogLevelSetting.off) { + return; + } + if (logLevelToNumber(logLevel) < logLevelToNumber(minLevel)) { + return; + } + + eventLog.logMessage({ + ruleId, + ruleUuid, + ruleName, + ruleType, + spaceId, + executionId, + message, + logLevel, + }); + }; + + const writeExceptionToConsole = (e: unknown, message: string, logMeta: ExtMeta): void => { + const logReason = e instanceof Error ? e.stack ?? e.message : String(e); + writeMessageToConsole(`${message}. Reason: ${logReason}`, LogLevel.error, logMeta); + }; + + const writeStatusChangeToConsole = (args: NormalizedStatusChangeArgs, logMeta: ExtMeta): void => { + const messageParts: string[] = [`Changing rule status to "${args.newStatus}"`, args.message]; + const logMessage = messageParts.filter(Boolean).join('. '); + const logLevel = logLevelFromExecutionStatus(args.newStatus); + writeMessageToConsole(logMessage, logLevel, logMeta); + }; + + // TODO: Add executionId to new status SO? + const writeStatusChangeToSavedObjects = async ( + args: NormalizedStatusChangeArgs + ): Promise => { + const { newStatus, message, metrics } = args; + + await soClient.createOrUpdate(ruleId, { + last_execution: { + date: nowISO(), + status: newStatus, + status_order: ruleExecutionStatusToNumber(newStatus), + message, + metrics: metrics ?? {}, + }, + }); + }; + + const writeStatusChangeToEventLog = (args: NormalizedStatusChangeArgs): void => { + const { newStatus, message, metrics } = args; + + if (metrics) { + eventLog.logExecutionMetrics({ + ruleId, + ruleUuid, + ruleName, + ruleType, + spaceId, + executionId, + metrics, + }); + } + + eventLog.logStatusChange({ + ruleId, + ruleUuid, + ruleName, + ruleType, + spaceId, + executionId, + newStatus, + message, + }); + }; + + return client; +}; + +const nowISO = () => new Date().toISOString(); + +interface NormalizedStatusChangeArgs { + newStatus: RuleExecutionStatus; + message: string; + metrics?: RuleExecutionMetrics; +} + +const normalizeStatusChangeArgs = (args: StatusChangeArgs): NormalizedStatusChangeArgs => { + const { newStatus, message, metrics } = args; + + return { + newStatus, + message: truncateValue(message) ?? '', + metrics: metrics + ? { + total_search_duration_ms: normalizeDurations(metrics.searchDurations), + total_indexing_duration_ms: normalizeDurations(metrics.indexingDurations), + execution_gap_duration_s: normalizeGap(metrics.executionGap), + } + : undefined, + }; +}; + +const normalizeDurations = (durations?: string[]): number | undefined => { + if (durations == null) { + return undefined; + } + + const sumAsFloat = sum(durations.map(Number)); + return Math.round(sumAsFloat); +}; + +const normalizeGap = (duration?: Duration): number | undefined => { + return duration ? Math.round(duration.asSeconds()) : undefined; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_executors/client_interface.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_executors/client_interface.ts new file mode 100644 index 0000000000000..22392e699fcea --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_executors/client_interface.ts @@ -0,0 +1,119 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Duration } from 'moment'; +import type { RuleExecutionStatus } from '../../../../../../../common/detection_engine/rule_monitoring'; + +/** + * Used from rule executors to log various information about the rule execution: + * - rule status changes + * - rule execution metrics + * - generic logs with debug/info/warning messages and errors + * + * Write targets: console logs, Event Log, saved objects. + * + * We create a new instance of this interface per each rule execution. + */ +export interface IRuleExecutionLogForExecutors { + /** + * Context with correlation ids and data related to the current rule execution. + */ + context: RuleExecutionContext; + + /** + * Writes a trace message to console logs. + * If enabled, writes it to .kibana-event-log-* index as well. + */ + trace(...messages: string[]): void; + + /** + * Writes a debug message to console logs. + * If enabled, writes it to .kibana-event-log-* index as well. + */ + debug(...messages: string[]): void; + + /** + * Writes an info message to console logs. + * If enabled, writes it to .kibana-event-log-* index as well. + */ + info(...messages: string[]): void; + + /** + * Writes a warning message to console logs. + * If enabled, writes it to .kibana-event-log-* index as well. + */ + warn(...messages: string[]): void; + + /** + * Writes an error message to console logs. + * If enabled, writes it to .kibana-event-log-* index as well. + */ + error(...messages: string[]): void; + + /** + * Writes information about new rule statuses and measured execution metrics: + * 1. To .kibana-* index as a custom `siem-detection-engine-rule-execution-info` saved object. + * This SO is used for fast access to last execution info of a large amount of rules. + * 2. To .kibana-event-log-* index in order to track history of rule executions. + * 3. To console logs. + * @param args Information about the status change event. + */ + logStatusChange(args: StatusChangeArgs): Promise; +} + +/** + * Each time a rule gets executed, we build an instance of rule execution context that + * contains correlation ids and data common to this particular rule execution. + */ +export interface RuleExecutionContext { + /** + * Every execution of a rule executor gets assigned its own UUID at the Alerting Framework + * level. We can use this id to filter all console logs, execution events in Event Log, + * and detection alerts written during a particular rule execution. + */ + executionId: string; + + /** + * Dynamic, saved object id of the rule being executed (rule.id). + */ + ruleId: string; + + /** + * Static, global (or "signature") id of the rule being executed (rule.rule_id). + */ + ruleUuid: string; + + /** + * Name of the rule being executed. + */ + ruleName: string; + + /** + * Alerting Framework's rule type id of the rule being executed. + */ + ruleType: string; + + /** + * Kibana space id of the rule being executed. + */ + spaceId: string; +} + +/** + * Information about the status change event. + */ +export interface StatusChangeArgs { + newStatus: RuleExecutionStatus; + message?: string; + metrics?: MetricsArgs; +} + +export interface MetricsArgs { + searchDurations?: string[]; + indexingDurations?: string[]; + executionGap?: Duration; +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_executors/correlation_ids.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_executors/correlation_ids.ts new file mode 100644 index 0000000000000..402635554a0e7 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_executors/correlation_ids.ts @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ExtMeta } from '../utils/console_logging'; +import type { RuleExecutionStatus } from '../../../../../../../common/detection_engine/rule_monitoring'; +import type { RuleExecutionContext } from './client_interface'; + +export interface ICorrelationIds { + withContext(context: RuleExecutionContext): ICorrelationIds; + withStatus(status: RuleExecutionStatus): ICorrelationIds; + + /** + * Returns a string with correlation ids that we append after the log message itself. + */ + getLogSuffix(): string; + + /** + * Returns correlation ids as a metadata object that we include into console log records (structured logs) + */ + getLogMeta(): ExtMeta; +} + +export const getCorrelationIds = (executionContext: RuleExecutionContext): ICorrelationIds => { + return createBuilder({ + context: executionContext, + status: null, + }); +}; + +interface BuilderState { + context: RuleExecutionContext; + status: RuleExecutionStatus | null; +} + +const createBuilder = (state: BuilderState): ICorrelationIds => { + const builder: ICorrelationIds = { + withContext: (context: RuleExecutionContext): ICorrelationIds => { + return createBuilder({ + ...state, + context, + }); + }, + + withStatus: (status: RuleExecutionStatus): ICorrelationIds => { + return createBuilder({ + ...state, + status, + }); + }, + + getLogSuffix: (): string => { + const { executionId, ruleId, ruleUuid, ruleName, ruleType, spaceId } = state.context; + return `[${ruleType}][${ruleName}][rule id ${ruleId}][rule uuid ${ruleUuid}][exec id ${executionId}][space ${spaceId}]`; + }, + + getLogMeta: (): ExtMeta => { + const { context, status } = state; + + const logMeta: ExtMeta = { + rule: { + id: context.ruleId, + uuid: context.ruleUuid, + name: context.ruleName, + type: context.ruleType, + execution: { + uuid: context.executionId, + }, + }, + kibana: { + spaceId: context.spaceId, + }, + }; + + if (status != null && logMeta.rule.execution != null) { + logMeta.rule.execution.status = status; + } + + return logMeta; + }, + }; + + return builder; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/client_for_routes/client.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_routes/client.ts similarity index 53% rename from x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/client_for_routes/client.ts rename to x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_routes/client.ts index b8f5b45098e7f..095c77d86a4ce 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/client_for_routes/client.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_routes/client.ts @@ -7,24 +7,27 @@ import { chunk, mapValues } from 'lodash'; import type { Logger } from '@kbn/core/server'; -import type { GetAggregateRuleExecutionEventsResponse } from '../../../../../common/detection_engine/schemas/response'; -import { initPromisePool } from '../../../../utils/promise_pool'; -import { withSecuritySpan } from '../../../../utils/with_security_span'; +import { initPromisePool } from '../../../../../../utils/promise_pool'; +import { withSecuritySpan } from '../../../../../../utils/with_security_span'; +import type { ExtMeta } from '../utils/console_logging'; +import { truncateList } from '../utils/normalization'; -import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common'; +import type { + GetRuleExecutionEventsResponse, + GetRuleExecutionResultsResponse, + RuleExecutionSummary, +} from '../../../../../../../common/detection_engine/rule_monitoring'; import type { IEventLogReader } from '../event_log/event_log_reader'; import type { IRuleExecutionSavedObjectsClient } from '../execution_saved_object/saved_objects_client'; import type { - GetAggregateExecutionEventsArgs, + GetExecutionEventsArgs, + GetExecutionResultsArgs, IRuleExecutionLogForRoutes, + RuleExecutionSummariesByRuleId, } from './client_interface'; -import type { ExtMeta } from '../utils/console_logging'; -import { truncateList } from '../utils/normalization'; - const RULES_PER_CHUNK = 1000; -const MAX_LAST_FAILURES = 5; export const createClientForRoutes = ( soClient: IRuleExecutionSavedObjectsClient, @@ -32,60 +35,11 @@ export const createClientForRoutes = ( logger: Logger ): IRuleExecutionLogForRoutes => { return { - getAggregateExecutionEvents({ - ruleId, - start, - end, - queryText, - statusFilters, - page, - perPage, - sortField, - sortOrder, - }: GetAggregateExecutionEventsArgs): Promise { - return withSecuritySpan( - 'IRuleExecutionLogForRoutes.getAggregateExecutionEvents', - async () => { - try { - return await eventLog.getAggregateExecutionEvents({ - ruleId, - start, - end, - queryText, - statusFilters, - page, - perPage, - sortField, - sortOrder, - }); - } catch (e) { - const logMessage = - 'Error getting last aggregation of execution failures from event log'; - const logAttributes = `rule id: "${ruleId}"`; - const logReason = e instanceof Error ? e.message : String(e); - const logMeta: ExtMeta = { - rule: { id: ruleId }, - }; - - logger.error(`${logMessage}; ${logAttributes}; ${logReason}`, logMeta); - throw e; - } - } - ); - }, - /** - * Get the current rule execution summary for each of the given rule IDs. - * This method splits work into chunks so not to overwhelm Elasticsearch - * when fetching statuses for a big number of rules. - * - * @param ruleIds A list of rule IDs (`rule.id`) to fetch summaries for - * @returns A dict with rule IDs as keys and execution summaries as values - * - * @throws AggregateError if any of the rule status requests fail - */ - getExecutionSummariesBulk(ruleIds) { + getExecutionSummariesBulk: (ruleIds: string[]): Promise => { return withSecuritySpan('IRuleExecutionLogForRoutes.getExecutionSummariesBulk', async () => { try { + // This method splits work into chunks so not to overwhelm Elasticsearch + // when fetching statuses for a big number of rules. const ruleIdsChunks = chunk(ruleIds, RULES_PER_CHUNK); const { results, errors } = await initPromisePool({ @@ -99,10 +53,10 @@ export const createClientForRoutes = ( const ruleIdsString = `[${truncateList(ruleIdsChunk).join(', ')}]`; const logMessage = 'Error fetching a chunk of rule execution saved objects'; - const logAttributes = `num of rules: ${ruleIdsChunk.length}, rule ids: ${ruleIdsString}`; const logReason = e instanceof Error ? e.stack ?? e.message : String(e); + const logSuffix = `[${ruleIdsChunk.length} rules][rule ids: ${ruleIdsString}]`; - logger.error(`${logMessage}; ${logAttributes}; ${logReason}`); + logger.error(`${logMessage}: ${logReason} ${logSuffix}`); throw e; } }, @@ -121,69 +75,87 @@ export const createClientForRoutes = ( const ruleIdsString = `[${truncateList(ruleIds).join(', ')}]`; const logMessage = 'Error bulk getting rule execution summaries'; - const logAttributes = `num of rules: ${ruleIds.length}, rule ids: ${ruleIdsString}`; const logReason = e instanceof Error ? e.message : String(e); + const logSuffix = `[${ruleIds.length} rules][rule ids: ${ruleIdsString}]`; - logger.error(`${logMessage}; ${logAttributes}; ${logReason}`); + logger.error(`${logMessage}: ${logReason} ${logSuffix}`); throw e; } }); }, - getExecutionSummary(ruleId) { + getExecutionSummary: (ruleId: string): Promise => { return withSecuritySpan('IRuleExecutionLogForRoutes.getExecutionSummary', async () => { try { const savedObject = await soClient.getOneByRuleId(ruleId); return savedObject ? savedObject.attributes : null; } catch (e) { const logMessage = 'Error getting rule execution summary'; - const logAttributes = `rule id: "${ruleId}"`; const logReason = e instanceof Error ? e.message : String(e); + const logSuffix = `[rule id ${ruleId}]`; const logMeta: ExtMeta = { rule: { id: ruleId }, }; - logger.error(`${logMessage}; ${logAttributes}; ${logReason}`, logMeta); + logger.error(`${logMessage}: ${logReason} ${logSuffix}`, logMeta); throw e; } }); }, - clearExecutionSummary(ruleId) { + clearExecutionSummary: (ruleId: string): Promise => { return withSecuritySpan('IRuleExecutionLogForRoutes.clearExecutionSummary', async () => { try { await soClient.delete(ruleId); } catch (e) { const logMessage = 'Error clearing rule execution summary'; - const logAttributes = `rule id: "${ruleId}"`; const logReason = e instanceof Error ? e.message : String(e); + const logSuffix = `[rule id ${ruleId}]`; const logMeta: ExtMeta = { rule: { id: ruleId }, }; - logger.error(`${logMessage}; ${logAttributes}; ${logReason}`, logMeta); + logger.error(`${logMessage}: ${logReason} ${logSuffix}`, logMeta); throw e; } }); }, - getLastFailures(ruleId) { - return withSecuritySpan('IRuleExecutionLogForRoutes.getLastFailures', async () => { + getExecutionEvents: (args: GetExecutionEventsArgs): Promise => { + return withSecuritySpan('IRuleExecutionLogForRoutes.getExecutionEvents', async () => { + const { ruleId } = args; try { - return await eventLog.getLastStatusChanges({ - ruleId, - count: MAX_LAST_FAILURES, - includeStatuses: [RuleExecutionStatus.failed], - }); + return await eventLog.getExecutionEvents(args); + } catch (e) { + const logMessage = 'Error getting plain execution events from event log'; + const logReason = e instanceof Error ? e.message : String(e); + const logSuffix = `[rule id ${ruleId}]`; + const logMeta: ExtMeta = { + rule: { id: ruleId }, + }; + + logger.error(`${logMessage}: ${logReason} ${logSuffix}`, logMeta); + throw e; + } + }); + }, + + getExecutionResults: ( + args: GetExecutionResultsArgs + ): Promise => { + return withSecuritySpan('IRuleExecutionLogForRoutes.getExecutionResults', async () => { + const { ruleId } = args; + try { + return await eventLog.getExecutionResults(args); } catch (e) { - const logMessage = 'Error getting last execution failures from event log'; - const logAttributes = `rule id: "${ruleId}"`; + const logMessage = 'Error getting aggregate execution results from event log'; const logReason = e instanceof Error ? e.message : String(e); + const logSuffix = `[rule id ${ruleId}]`; const logMeta: ExtMeta = { rule: { id: ruleId }, }; - logger.error(`${logMessage}; ${logAttributes}; ${logReason}`, logMeta); + logger.error(`${logMessage}: ${logReason} ${logSuffix}`, logMeta); throw e; } }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_routes/client_interface.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_routes/client_interface.ts new file mode 100644 index 0000000000000..dec3990654b80 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_routes/client_interface.ts @@ -0,0 +1,107 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { SortOrder } from '../../../../../../../common/detection_engine/schemas/common'; +import type { + GetRuleExecutionEventsResponse, + GetRuleExecutionResultsResponse, + LogLevel, + RuleExecutionEventType, + RuleExecutionStatus, + RuleExecutionSummary, + SortFieldOfRuleExecutionResult, +} from '../../../../../../../common/detection_engine/rule_monitoring'; + +/** + * Used from route handlers to fetch and manage various information about the rule execution: + * - execution summary of a rule containing such data as the last status and metrics + * - execution events such as recent failures and status changes + */ +export interface IRuleExecutionLogForRoutes { + /** + * Fetches a list of current execution summaries of multiple rules. + * @param ruleIds A list of saved object ids of multiple rules (`rule.id`). + * @returns A dict with rule IDs as keys and execution summaries as values. + * @throws AggregateError if any of the rule status requests fail. + */ + getExecutionSummariesBulk(ruleIds: string[]): Promise; + + /** + * Fetches current execution summary of a given rule. + * @param ruleId Saved object id of the rule (`rule.id`). + */ + getExecutionSummary(ruleId: string): Promise; + + /** + * Deletes the current execution summary if it exists. + * @param ruleId Saved object id of the rule (`rule.id`). + */ + clearExecutionSummary(ruleId: string): Promise; + + /** + * Fetches plain execution events of a given rule from Event Log. This includes debug, info, and + * error messages that executor functions write during a rule execution to the log. + */ + getExecutionEvents(args: GetExecutionEventsArgs): Promise; + + /** + * Fetches execution results aggregated by execution UUID, combining data from both alerting + * and security-solution event-log documents. + */ + getExecutionResults(args: GetExecutionResultsArgs): Promise; +} + +export interface GetExecutionEventsArgs { + /** Saved object id of the rule (`rule.id`). */ + ruleId: string; + + /** Include events of the specified types. If empty, all types of events will be included. */ + eventTypes: RuleExecutionEventType[]; + + /** Include events having these log levels. If empty, events of all levels will be included. */ + logLevels: LogLevel[]; + + /** What order to sort by (e.g. `asc` or `desc`). */ + sortOrder: SortOrder; + + /** Current page to fetch. */ + page: number; + + /** Number of results to fetch per page. */ + perPage: number; +} + +export interface GetExecutionResultsArgs { + /** Saved object id of the rule (`rule.id`). */ + ruleId: string; + + /** Start of daterange to filter to. */ + start: string; + + /** End of daterange to filter to. */ + end: string; + + /** String of field-based filters, e.g. kibana.alert.rule.execution.status:* */ + queryText: string; + + /** Array of status filters, e.g. ['succeeded', 'going to run'] */ + statusFilters: RuleExecutionStatus[]; + + /** Field to sort by. */ + sortField: SortFieldOfRuleExecutionResult; + + /** What order to sort by (e.g. `asc` or `desc`). */ + sortOrder: SortOrder; + + /** Current page to fetch. */ + page: number; + + /** Number of results to fetch per page. */ + perPage: number; +} + +export type RuleExecutionSummariesByRuleId = Record; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log/constants.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/event_log/constants.ts similarity index 75% rename from x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log/constants.ts rename to x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/event_log/constants.ts index 08bafa92ac02a..3493e49e88135 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log/constants.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/event_log/constants.ts @@ -8,8 +8,3 @@ export const RULE_SAVED_OBJECT_TYPE = 'alert'; export const RULE_EXECUTION_LOG_PROVIDER = 'securitySolution.ruleExecution'; - -export enum RuleExecutionLogAction { - 'status-change' = 'status-change', - 'execution-metrics' = 'execution-metrics', -} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/event_log/event_log_reader.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/event_log/event_log_reader.ts new file mode 100644 index 0000000000000..2428274165b9c --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/event_log/event_log_reader.ts @@ -0,0 +1,243 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { IEventLogClient, IValidatedEvent } from '@kbn/event-log-plugin/server'; +import { MAX_EXECUTION_EVENTS_DISPLAYED } from '@kbn/securitysolution-rules'; + +import { assertUnreachable } from '../../../../../../../common/utility_types'; +import { invariant } from '../../../../../../../common/utils/invariant'; +import { withSecuritySpan } from '../../../../../../utils/with_security_span'; + +import type { + RuleExecutionEvent, + GetRuleExecutionEventsResponse, + GetRuleExecutionResultsResponse, +} from '../../../../../../../common/detection_engine/rule_monitoring'; +import { + LogLevel, + logLevelFromString, + RuleExecutionEventType, + ruleExecutionEventTypeFromString, +} from '../../../../../../../common/detection_engine/rule_monitoring'; +import type { + GetExecutionEventsArgs, + GetExecutionResultsArgs, +} from '../client_for_routes/client_interface'; + +import { RULE_SAVED_OBJECT_TYPE, RULE_EXECUTION_LOG_PROVIDER } from './constants'; +import { + formatExecutionEventResponse, + getExecutionEventAggregation, + mapRuleExecutionStatusToPlatformStatus, +} from './get_execution_event_aggregation'; +import type { ExecutionUuidAggResult } from './get_execution_event_aggregation/types'; +import { EXECUTION_UUID_FIELD } from './get_execution_event_aggregation/types'; + +export interface IEventLogReader { + getExecutionEvents(args: GetExecutionEventsArgs): Promise; + getExecutionResults(args: GetExecutionResultsArgs): Promise; +} + +export const createEventLogReader = (eventLog: IEventLogClient): IEventLogReader => { + return { + async getExecutionEvents( + args: GetExecutionEventsArgs + ): Promise { + const { ruleId, eventTypes, logLevels, sortOrder, page, perPage } = args; + const soType = RULE_SAVED_OBJECT_TYPE; + const soIds = [ruleId]; + + // TODO: include Framework events + const kqlFilter = kqlAnd([ + `event.provider:${RULE_EXECUTION_LOG_PROVIDER}`, + eventTypes.length > 0 ? `event.action:(${kqlOr(eventTypes)})` : '', + logLevels.length > 0 ? `log.level:(${kqlOr(logLevels)})` : '', + ]); + + const findResult = await withSecuritySpan('findEventsBySavedObjectIds', () => { + return eventLog.findEventsBySavedObjectIds(soType, soIds, { + filter: kqlFilter, + sort: [ + { sort_field: '@timestamp', sort_order: sortOrder }, + { sort_field: 'event.sequence', sort_order: sortOrder }, + ], + page, + per_page: perPage, + }); + }); + + return { + events: findResult.data.map((event) => normalizeEvent(event)), + pagination: { + page: findResult.page, + per_page: findResult.per_page, + total: findResult.total, + }, + }; + }, + + async getExecutionResults( + args: GetExecutionResultsArgs + ): Promise { + const { ruleId, start, end, statusFilters, page, perPage, sortField, sortOrder } = args; + const soType = RULE_SAVED_OBJECT_TYPE; + const soIds = [ruleId]; + + // Current workaround to support root level filters without missing fields in the aggregate event + // or including events from statuses that aren't selected + // TODO: See: https://github.com/elastic/kibana/pull/127339/files#r825240516 + // First fetch execution uuid's by status filter if provided + let statusIds: string[] = []; + let totalExecutions: number | undefined; + // If 0 or 3 statuses are selected we can search for all statuses and don't need this pre-filter by ID + if (statusFilters.length > 0 && statusFilters.length < 3) { + const outcomes = mapRuleExecutionStatusToPlatformStatus(statusFilters); + const outcomeFilter = outcomes.length ? `OR event.outcome:(${outcomes.join(' OR ')})` : ''; + const statusResults = await eventLog.aggregateEventsBySavedObjectIds(soType, soIds, { + start, + end, + // Also query for `event.outcome` to catch executions that only contain platform events + filter: `kibana.alert.rule.execution.status:(${statusFilters.join( + ' OR ' + )}) ${outcomeFilter}`, + aggs: { + totalExecutions: { + cardinality: { + field: EXECUTION_UUID_FIELD, + }, + }, + filteredExecutionUUIDs: { + terms: { + field: EXECUTION_UUID_FIELD, + order: { executeStartTime: 'desc' }, + size: MAX_EXECUTION_EVENTS_DISPLAYED, + }, + aggs: { + executeStartTime: { + min: { + field: '@timestamp', + }, + }, + }, + }, + }, + }); + const filteredExecutionUUIDs = statusResults.aggregations + ?.filteredExecutionUUIDs as ExecutionUuidAggResult; + statusIds = filteredExecutionUUIDs?.buckets?.map((b) => b.key) ?? []; + totalExecutions = ( + statusResults.aggregations?.totalExecutions as estypes.AggregationsCardinalityAggregate + ).value; + // Early return if no results based on status filter + if (statusIds.length === 0) { + return { + total: 0, + events: [], + }; + } + } + + // Now query for aggregate events, and pass any ID's as filters as determined from the above status/queryText results + const idsFilter = statusIds.length + ? `kibana.alert.rule.execution.uuid:(${statusIds.join(' OR ')})` + : ''; + const results = await eventLog.aggregateEventsBySavedObjectIds(soType, soIds, { + start, + end, + filter: idsFilter, + aggs: getExecutionEventAggregation({ + maxExecutions: MAX_EXECUTION_EVENTS_DISPLAYED, + page, + perPage, + sort: [{ [sortField]: { order: sortOrder } }], + }), + }); + + return formatExecutionEventResponse(results, totalExecutions); + }, + }; +}; + +const kqlAnd = (items: T[]): string => { + return items.filter(Boolean).map(String).join(' and '); +}; + +const kqlOr = (items: T[]): string => { + return items.filter(Boolean).map(String).join(' or '); +}; + +const normalizeEvent = (rawEvent: IValidatedEvent): RuleExecutionEvent => { + invariant(rawEvent, 'Event not found'); + + const timestamp = normalizeEventTimestamp(rawEvent); + const sequence = normalizeEventSequence(rawEvent); + const level = normalizeLogLevel(rawEvent); + const type = normalizeEventType(rawEvent); + const message = normalizeEventMessage(rawEvent, type); + + return { timestamp, sequence, level, type, message }; +}; + +type RawEvent = NonNullable; + +const normalizeEventTimestamp = (event: RawEvent): string => { + invariant(event['@timestamp'], 'Required "@timestamp" field is not found'); + return event['@timestamp']; +}; + +const normalizeEventSequence = (event: RawEvent): number => { + const value = event.event?.sequence; + if (typeof value === 'number') { + return value; + } + if (typeof value === 'string') { + return Number(value); + } + return 0; +}; + +const normalizeLogLevel = (event: RawEvent): LogLevel => { + const value = event.log?.level; + if (!value) { + return LogLevel.debug; + } + + return logLevelFromString(value) ?? LogLevel.trace; +}; + +const normalizeEventType = (event: RawEvent): RuleExecutionEventType => { + const value = event.event?.action; + invariant(value, 'Required "event.action" field is not found'); + + return ruleExecutionEventTypeFromString(value) ?? RuleExecutionEventType.message; +}; + +const normalizeEventMessage = (event: RawEvent, type: RuleExecutionEventType): string => { + if (type === RuleExecutionEventType.message) { + return event.message || ''; + } + + if (type === RuleExecutionEventType['status-change']) { + invariant( + event.kibana?.alert?.rule?.execution?.status, + 'Required "kibana.alert.rule.execution.status" field is not found' + ); + + const status = event.kibana?.alert?.rule?.execution?.status; + const message = event.message || ''; + + return `Rule changed status to "${status}". ${message}`; + } + + if (type === RuleExecutionEventType['execution-metrics']) { + return ''; + } + + assertUnreachable(type); + return ''; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/event_log/event_log_writer.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/event_log/event_log_writer.ts new file mode 100644 index 0000000000000..aa1fcf36aba68 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/event_log/event_log_writer.ts @@ -0,0 +1,189 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SavedObjectsUtils } from '@kbn/core/server'; +import type { IEventLogService } from '@kbn/event-log-plugin/server'; +import { SAVED_OBJECT_REL_PRIMARY } from '@kbn/event-log-plugin/server'; +import type { + RuleExecutionMetrics, + RuleExecutionStatus, +} from '../../../../../../../common/detection_engine/rule_monitoring'; +import { + LogLevel, + logLevelFromExecutionStatus, + logLevelToNumber, + RuleExecutionEventType, + ruleExecutionStatusToNumber, +} from '../../../../../../../common/detection_engine/rule_monitoring'; +import { RULE_SAVED_OBJECT_TYPE, RULE_EXECUTION_LOG_PROVIDER } from './constants'; + +export interface IEventLogWriter { + logMessage(args: MessageArgs): void; + logStatusChange(args: StatusChangeArgs): void; + logExecutionMetrics(args: ExecutionMetricsArgs): void; +} + +export interface BaseArgs { + ruleId: string; + ruleUuid: string; + ruleName: string; + ruleType: string; + spaceId: string; + executionId: string; +} + +export interface MessageArgs extends BaseArgs { + logLevel: LogLevel; + message: string; +} + +export interface StatusChangeArgs extends BaseArgs { + newStatus: RuleExecutionStatus; + message?: string; +} + +export interface ExecutionMetricsArgs extends BaseArgs { + metrics: RuleExecutionMetrics; +} + +export const createEventLogWriter = (eventLogService: IEventLogService): IEventLogWriter => { + const eventLogger = eventLogService.getLogger({ + event: { provider: RULE_EXECUTION_LOG_PROVIDER }, + }); + + let sequence = 0; + + return { + logMessage: (args: MessageArgs): void => { + eventLogger.logEvent({ + '@timestamp': nowISO(), + message: args.message, + rule: { + id: args.ruleId, + uuid: args.ruleUuid, + name: args.ruleName, + category: args.ruleType, + }, + event: { + kind: 'event', + action: RuleExecutionEventType.message, + sequence: sequence++, + severity: logLevelToNumber(args.logLevel), + }, + log: { + level: args.logLevel, + }, + kibana: { + alert: { + rule: { + execution: { + uuid: args.executionId, + }, + }, + }, + space_ids: [args.spaceId], + saved_objects: [ + { + rel: SAVED_OBJECT_REL_PRIMARY, + type: RULE_SAVED_OBJECT_TYPE, + id: args.ruleId, + namespace: spaceIdToNamespace(args.spaceId), + }, + ], + }, + }); + }, + + logStatusChange: (args: StatusChangeArgs): void => { + const logLevel = logLevelFromExecutionStatus(args.newStatus); + eventLogger.logEvent({ + '@timestamp': nowISO(), + message: args.message, + rule: { + id: args.ruleId, + uuid: args.ruleUuid, + name: args.ruleName, + category: args.ruleType, + }, + event: { + kind: 'event', + action: RuleExecutionEventType['status-change'], + sequence: sequence++, + severity: logLevelToNumber(logLevel), + }, + log: { + level: logLevel, + }, + kibana: { + alert: { + rule: { + execution: { + uuid: args.executionId, + status: args.newStatus, + status_order: ruleExecutionStatusToNumber(args.newStatus), + }, + }, + }, + space_ids: [args.spaceId], + saved_objects: [ + { + rel: SAVED_OBJECT_REL_PRIMARY, + type: RULE_SAVED_OBJECT_TYPE, + id: args.ruleId, + namespace: spaceIdToNamespace(args.spaceId), + }, + ], + }, + }); + }, + + logExecutionMetrics: (args: ExecutionMetricsArgs): void => { + const logLevel = LogLevel.debug; + eventLogger.logEvent({ + '@timestamp': nowISO(), + rule: { + id: args.ruleId, + uuid: args.ruleUuid, + name: args.ruleName, + category: args.ruleType, + }, + event: { + kind: 'metric', + action: RuleExecutionEventType['execution-metrics'], + sequence: sequence++, + severity: logLevelToNumber(logLevel), + }, + log: { + level: logLevel, + }, + kibana: { + alert: { + rule: { + execution: { + uuid: args.executionId, + metrics: args.metrics, + }, + }, + }, + space_ids: [args.spaceId], + saved_objects: [ + { + rel: SAVED_OBJECT_REL_PRIMARY, + type: RULE_SAVED_OBJECT_TYPE, + id: args.ruleId, + namespace: spaceIdToNamespace(args.spaceId), + }, + ], + }, + }); + }, + }; +}; + +const nowISO = () => new Date().toISOString(); + +const spaceIdToNamespace = SavedObjectsUtils.namespaceStringToId; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log/get_execution_event_aggregation/index.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/event_log/get_execution_event_aggregation/index.test.ts similarity index 99% rename from x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log/get_execution_event_aggregation/index.test.ts rename to x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/event_log/get_execution_event_aggregation/index.test.ts index dcd592d7a70fc..83bf237747dda 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log/get_execution_event_aggregation/index.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/event_log/get_execution_event_aggregation/index.test.ts @@ -12,8 +12,8 @@ * 2.0. */ -import { RuleExecutionStatus } from '../../../../../../common/detection_engine/schemas/common'; import { MAX_EXECUTION_EVENTS_DISPLAYED } from '@kbn/securitysolution-rules'; +import { RuleExecutionStatus } from '../../../../../../../../common/detection_engine/rule_monitoring'; import { formatExecutionEventResponse, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log/get_execution_event_aggregation/index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/event_log/get_execution_event_aggregation/index.ts similarity index 97% rename from x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log/get_execution_event_aggregation/index.ts rename to x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/event_log/get_execution_event_aggregation/index.ts index dc415bbc200b9..c1ebb5e77f98a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log/get_execution_event_aggregation/index.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/event_log/get_execution_event_aggregation/index.ts @@ -5,14 +5,17 @@ * 2.0. */ +import { flatMap, get } from 'lodash'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { BadRequestError } from '@kbn/securitysolution-es-utils'; -import { flatMap, get } from 'lodash'; import { MAX_EXECUTION_EVENTS_DISPLAYED } from '@kbn/securitysolution-rules'; import type { AggregateEventsBySavedObjectResult } from '@kbn/event-log-plugin/server'; -import type { AggregateRuleExecutionEvent } from '../../../../../../common/detection_engine/schemas/common'; -import { RuleExecutionStatus } from '../../../../../../common/detection_engine/schemas/common'; -import type { GetAggregateRuleExecutionEventsResponse } from '../../../../../../common/detection_engine/schemas/response'; + +import type { + RuleExecutionResult, + GetRuleExecutionResultsResponse, +} from '../../../../../../../../common/detection_engine/rule_monitoring'; +import { RuleExecutionStatus } from '../../../../../../../../common/detection_engine/rule_monitoring'; import type { ExecutionEventAggregationOptions, ExecutionUuidAggResult, @@ -267,7 +270,7 @@ export const getProviderAndActionFilter = (provider: string, action: string) => */ export const formatAggExecutionEventFromBucket = ( bucket: ExecutionUuidAggBucket -): AggregateRuleExecutionEvent => { +): RuleExecutionResult => { const durationUs = bucket?.ruleExecution?.executionDuration?.value ?? 0; const scheduleDelayUs = bucket?.ruleExecution?.scheduleDelay?.value ?? 0; const timedOut = (bucket?.timeoutMessage?.doc_count ?? 0) > 0; @@ -318,7 +321,7 @@ export const formatAggExecutionEventFromBucket = ( export const formatExecutionEventResponse = ( results: AggregateEventsBySavedObjectResult, totalExecutions?: number -): GetAggregateRuleExecutionEventsResponse => { +): GetRuleExecutionResultsResponse => { const { aggregations } = results; if (!aggregations) { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log/get_execution_event_aggregation/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/event_log/get_execution_event_aggregation/types.ts similarity index 100% rename from x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log/get_execution_event_aggregation/types.ts rename to x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/event_log/get_execution_event_aggregation/types.ts diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log/register_event_log_provider.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/event_log/register_event_log_provider.ts similarity index 70% rename from x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log/register_event_log_provider.ts rename to x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/event_log/register_event_log_provider.ts index c05724198e5b2..94542e913d454 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log/register_event_log_provider.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/event_log/register_event_log_provider.ts @@ -6,11 +6,12 @@ */ import type { IEventLogService } from '@kbn/event-log-plugin/server'; -import { RuleExecutionLogAction, RULE_EXECUTION_LOG_PROVIDER } from './constants'; +import { RuleExecutionEventType } from '../../../../../../../common/detection_engine/rule_monitoring'; +import { RULE_EXECUTION_LOG_PROVIDER } from './constants'; export const registerEventLogProvider = (eventLogService: IEventLogService) => { eventLogService.registerProviderActions( RULE_EXECUTION_LOG_PROVIDER, - Object.keys(RuleExecutionLogAction) + Object.keys(RuleExecutionEventType) ); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/execution_saved_object/saved_objects_client.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/execution_saved_object/saved_objects_client.ts similarity index 98% rename from x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/execution_saved_object/saved_objects_client.ts rename to x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/execution_saved_object/saved_objects_client.ts index 3333d87c7e8ef..08a7c49f739c0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/execution_saved_object/saved_objects_client.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/execution_saved_object/saved_objects_client.ts @@ -8,7 +8,7 @@ import type { Logger, SavedObjectsClientContract } from '@kbn/core/server'; import { SavedObjectsErrorHelpers } from '@kbn/core/server'; -import { withSecuritySpan } from '../../../../utils/with_security_span'; +import { withSecuritySpan } from '../../../../../../utils/with_security_span'; import type { RuleExecutionSavedObject, RuleExecutionAttributes } from './saved_objects_type'; import { RULE_EXECUTION_SO_TYPE } from './saved_objects_type'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/execution_saved_object/saved_objects_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/execution_saved_object/saved_objects_type.ts similarity index 96% rename from x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/execution_saved_object/saved_objects_type.ts rename to x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/execution_saved_object/saved_objects_type.ts index 788eb26c3c5ae..ac3b28e87e0d9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/execution_saved_object/saved_objects_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/execution_saved_object/saved_objects_type.ts @@ -10,7 +10,7 @@ import type { RuleExecutionMetrics, RuleExecutionStatus, RuleExecutionStatusOrder, -} from '../../../../../common/detection_engine/schemas/common'; +} from '../../../../../../../common/detection_engine/rule_monitoring'; export const RULE_EXECUTION_SO_TYPE = 'siem-detection-engine-rule-execution-info'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/execution_saved_object/saved_objects_utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/execution_saved_object/saved_objects_utils.ts similarity index 100% rename from x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/execution_saved_object/saved_objects_utils.ts rename to x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/execution_saved_object/saved_objects_utils.ts diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/execution_settings/fetch_rule_execution_settings.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/execution_settings/fetch_rule_execution_settings.ts new file mode 100644 index 0000000000000..a73dba17b6b9f --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/execution_settings/fetch_rule_execution_settings.ts @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Logger, SavedObjectsClientContract } from '@kbn/core/server'; +import type { ConfigType } from '../../../../../../config'; +import { withSecuritySpan } from '../../../../../../utils/with_security_span'; +import type { SecuritySolutionPluginCoreSetupDependencies } from '../../../../../../plugin_contract'; + +import { + EXTENDED_RULE_EXECUTION_LOGGING_ENABLED_SETTING, + EXTENDED_RULE_EXECUTION_LOGGING_MIN_LEVEL_SETTING, +} from '../../../../../../../common/constants'; +import type { RuleExecutionSettings } from '../../../../../../../common/detection_engine/rule_monitoring'; +import { LogLevelSetting } from '../../../../../../../common/detection_engine/rule_monitoring'; + +export const fetchRuleExecutionSettings = async ( + config: ConfigType, + logger: Logger, + core: SecuritySolutionPluginCoreSetupDependencies, + savedObjectsClient: SavedObjectsClientContract +): Promise => { + try { + const ruleExecutionSettings = await withSecuritySpan('fetchRuleExecutionSettings', async () => { + const [coreStart] = await withSecuritySpan('getCoreStartServices', () => + core.getStartServices() + ); + + const kibanaAdvancedSettings = await withSecuritySpan('getKibanaAdvancedSettings', () => { + const settingsClient = coreStart.uiSettings.asScopedToClient(savedObjectsClient); + return settingsClient.getAll(); + }); + + return getRuleExecutionSettingsFrom(config, kibanaAdvancedSettings); + }); + + return ruleExecutionSettings; + } catch (e) { + const logMessage = 'Error fetching rule execution settings'; + const logReason = e instanceof Error ? e.stack ?? e.message : String(e); + logger.error(`${logMessage}: ${logReason}`); + + return getRuleExecutionSettingsDefault(config); + } +}; + +const getRuleExecutionSettingsFrom = ( + config: ConfigType, + advancedSettings: Record +): RuleExecutionSettings => { + const featureFlagEnabled = config.experimentalFeatures.extendedRuleExecutionLoggingEnabled; + + const advancedSettingEnabled = getSetting( + advancedSettings, + EXTENDED_RULE_EXECUTION_LOGGING_ENABLED_SETTING, + false + ); + const advancedSettingMinLevel = getSetting( + advancedSettings, + EXTENDED_RULE_EXECUTION_LOGGING_MIN_LEVEL_SETTING, + LogLevelSetting.off + ); + + return { + extendedLogging: { + isEnabled: featureFlagEnabled && advancedSettingEnabled, + minLevel: advancedSettingMinLevel, + }, + }; +}; + +const getRuleExecutionSettingsDefault = (config: ConfigType): RuleExecutionSettings => { + const featureFlagEnabled = config.experimentalFeatures.extendedRuleExecutionLoggingEnabled; + + return { + extendedLogging: { + isEnabled: featureFlagEnabled, + minLevel: featureFlagEnabled ? LogLevelSetting.error : LogLevelSetting.off, + }, + }; +}; + +const getSetting = (settings: Record, key: string, defaultValue: T): T => { + const setting = settings[key]; + return setting != null ? (setting as T) : defaultValue; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/index.ts similarity index 80% rename from x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/index.ts rename to x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/index.ts index c479e95037769..1cc6be1e0c6c4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/index.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/index.ts @@ -7,10 +7,11 @@ export * from './client_for_executors/client_interface'; export * from './client_for_routes/client_interface'; -export * from './client_factories'; +export * from './service_interface'; +export * from './service'; export { ruleExecutionType } from './execution_saved_object/saved_objects_type'; -export { registerEventLogProvider } from './event_log/register_event_log_provider'; +export { RULE_EXECUTION_LOG_PROVIDER } from './event_log/constants'; export { mergeRuleExecutionSummary } from './merge_rule_execution_summary'; export * from './utils/normalization'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/merge_rule_execution_summary.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/merge_rule_execution_summary.ts similarity index 76% rename from x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/merge_rule_execution_summary.ts rename to x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/merge_rule_execution_summary.ts index 2e2ac74e94cc5..2b017d27bb971 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/merge_rule_execution_summary.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/merge_rule_execution_summary.ts @@ -5,12 +5,12 @@ * 2.0. */ -import type { RuleExecutionSummary } from '../../../../common/detection_engine/schemas/common'; +import type { RuleExecutionSummary } from '../../../../../../common/detection_engine/rule_monitoring'; import { RuleExecutionStatus, - ruleExecutionStatusOrderByStatus, -} from '../../../../common/detection_engine/schemas/common'; -import type { RuleAlertType } from '../rules/types'; + ruleExecutionStatusToNumber, +} from '../../../../../../common/detection_engine/rule_monitoring'; +import type { RuleAlertType } from '../../../rules/types'; export const mergeRuleExecutionSummary = ( rule: RuleAlertType, @@ -32,7 +32,7 @@ export const mergeRuleExecutionSummary = ( last_execution: { date: frameworkStatus.lastExecutionDate.toISOString(), status: RuleExecutionStatus.failed, - status_order: ruleExecutionStatusOrderByStatus[RuleExecutionStatus.failed], + status_order: ruleExecutionStatusToNumber(RuleExecutionStatus.failed), message: `Reason: ${frameworkStatus.error?.reason} Message: ${frameworkStatus.error?.message}`, metrics: customStatus.metrics, }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/service.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/service.ts new file mode 100644 index 0000000000000..db5aefdaf7370 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/service.ts @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Logger } from '@kbn/core/server'; +import type { ConfigType } from '../../../../../config'; +import { withSecuritySpan } from '../../../../../utils/with_security_span'; +import type { + SecuritySolutionPluginCoreSetupDependencies, + SecuritySolutionPluginSetupDependencies, +} from '../../../../../plugin_contract'; + +import type { IRuleExecutionLogForRoutes } from './client_for_routes/client_interface'; +import { createClientForRoutes } from './client_for_routes/client'; +import type { IRuleExecutionLogForExecutors } from './client_for_executors/client_interface'; +import { createClientForExecutors } from './client_for_executors/client'; + +import { registerEventLogProvider } from './event_log/register_event_log_provider'; +import { createEventLogReader } from './event_log/event_log_reader'; +import { createEventLogWriter } from './event_log/event_log_writer'; +import { createRuleExecutionSavedObjectsClient } from './execution_saved_object/saved_objects_client'; +import { fetchRuleExecutionSettings } from './execution_settings/fetch_rule_execution_settings'; +import type { + ClientForExecutorsParams, + ClientForRoutesParams, + IRuleExecutionLogService, +} from './service_interface'; + +export const createRuleExecutionLogService = ( + config: ConfigType, + logger: Logger, + core: SecuritySolutionPluginCoreSetupDependencies, + plugins: SecuritySolutionPluginSetupDependencies +): IRuleExecutionLogService => { + return { + registerEventLogProvider: () => { + registerEventLogProvider(plugins.eventLog); + }, + + createClientForRoutes: (params: ClientForRoutesParams): IRuleExecutionLogForRoutes => { + const { savedObjectsClient, eventLogClient } = params; + + const soClient = createRuleExecutionSavedObjectsClient(savedObjectsClient, logger); + const eventLogReader = createEventLogReader(eventLogClient); + + return createClientForRoutes(soClient, eventLogReader, logger); + }, + + createClientForExecutors: ( + params: ClientForExecutorsParams + ): Promise => { + return withSecuritySpan('IRuleExecutionLogService.createClientForExecutors', async () => { + const { savedObjectsClient, context } = params; + + const childLogger = logger.get('ruleExecution'); + + const ruleExecutionSettings = await fetchRuleExecutionSettings( + config, + childLogger, + core, + savedObjectsClient + ); + + const soClient = createRuleExecutionSavedObjectsClient(savedObjectsClient, childLogger); + const eventLogWriter = createEventLogWriter(plugins.eventLog); + + return createClientForExecutors( + ruleExecutionSettings, + soClient, + eventLogWriter, + childLogger, + context + ); + }); + }, + }; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/service_interface.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/service_interface.ts new file mode 100644 index 0000000000000..27207ea2afde0 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/service_interface.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { SavedObjectsClientContract } from '@kbn/core/server'; +import type { IEventLogClient } from '@kbn/event-log-plugin/server'; + +import type { IRuleExecutionLogForRoutes } from './client_for_routes/client_interface'; +import type { + IRuleExecutionLogForExecutors, + RuleExecutionContext, +} from './client_for_executors/client_interface'; + +export interface IRuleExecutionLogService { + registerEventLogProvider(): void; + + createClientForRoutes(params: ClientForRoutesParams): IRuleExecutionLogForRoutes; + + createClientForExecutors( + params: ClientForExecutorsParams + ): Promise; +} + +export interface ClientForRoutesParams { + savedObjectsClient: SavedObjectsClientContract; + eventLogClient: IEventLogClient; +} + +export interface ClientForExecutorsParams { + savedObjectsClient: SavedObjectsClientContract; + context: RuleExecutionContext; +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/utils/console_logging.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/utils/console_logging.ts new file mode 100644 index 0000000000000..d45c5ee7c65d0 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/utils/console_logging.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { LogMeta } from '@kbn/core/server'; +import type { RuleExecutionStatus } from '../../../../../../../common/detection_engine/rule_monitoring'; + +/** + * Extended metadata that rule execution logger can attach to every console log record. + */ +export interface ExtMeta extends LogMeta { + rule: ExtRule; + kibana?: ExtKibana; +} + +interface ExtRule extends NonNullable { + id: string; + type?: string; + execution?: { + uuid: string; + status?: RuleExecutionStatus; + }; +} + +interface ExtKibana { + spaceId?: string; +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/utils/normalization.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/utils/normalization.ts similarity index 100% rename from x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/utils/normalization.ts rename to x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/utils/normalization.ts diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/mocks.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/mocks.ts new file mode 100644 index 0000000000000..73288c05c3a71 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/mocks.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './logic/rule_execution_log/__mocks__'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/rule_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/rule_type.ts index 2f636934cbc0c..e0e985ff23865 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/rule_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/rule_type.ts @@ -88,7 +88,6 @@ export const createRuleTypeMocks = ( return { dependencies: { alerting, - buildRuleMessage: jest.fn(), config$: mockedConfig$, lists: listMock.createSetup(), logger: loggerMock, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts index d7005fc746409..13a8592ca1bd0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts @@ -6,15 +6,13 @@ */ import { isEmpty } from 'lodash'; - -import { parseScheduleDates } from '@kbn/securitysolution-io-ts-utils'; import agent from 'elastic-apm-node'; +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { TIMESTAMP } from '@kbn/rule-data-utils'; import { createPersistenceRuleTypeWrapper } from '@kbn/rule-registry-plugin/server'; -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { parseScheduleDates } from '@kbn/securitysolution-io-ts-utils'; -import { buildRuleMessageFactory } from './factories/build_rule_message_factory'; import { checkPrivilegesFromEsClient, getExceptions, @@ -31,25 +29,17 @@ import { scheduleNotificationActions } from '../notifications/schedule_notificat import { getNotificationResultsLink } from '../notifications/utils'; import { createResultObject } from './utils'; import { bulkCreateFactory, wrapHitsFactory, wrapSequencesFactory } from './factories'; -import { truncateList } from '../rule_execution_log'; -import { RuleExecutionStatus } from '../../../../common/detection_engine/schemas/common'; +import { RuleExecutionStatus } from '../../../../common/detection_engine/rule_monitoring'; +import { truncateList } from '../rule_monitoring'; import { scheduleThrottledNotificationActions } from '../notifications/schedule_throttle_notification_actions'; import aadFieldConversion from '../routes/index/signal_aad_mapping.json'; import { extractReferences, injectReferences } from '../signals/saved_object_references'; import { withSecuritySpan } from '../../../utils/with_security_span'; -import { getInputIndex } from '../signals/get_input_output_index'; +import { getInputIndex, DataViewError } from '../signals/get_input_output_index'; /* eslint-disable complexity */ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = - ({ - lists, - logger, - config, - ruleDataClient, - eventLogService, - ruleExecutionLoggerFactory, - version, - }) => + ({ lists, logger, config, ruleDataClient, ruleExecutionLoggerFactory, version }) => (type) => { const { alertIgnoreFields: ignoreFields, alertMergeStrategy: mergeStrategy } = config; const persistenceRuleType = createPersistenceRuleTypeWrapper({ ruleDataClient, logger }); @@ -63,7 +53,7 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = }, async executor(options) { agent.setTransactionName(`${options.rule.ruleTypeId} execution`); - return withSecuritySpan('scurityRuleTypeExecutor', async () => { + return withSecuritySpan('securityRuleTypeExecutor', async () => { const { alertId, executionId, @@ -77,7 +67,6 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = rule, } = options; let runState = state; - let hasError = false; let inputIndex: string[] = []; let runtimeMappings: estypes.MappingRuntimeFields | undefined; const { @@ -99,18 +88,17 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = const esClient = scopedClusterClient.asCurrentUser; - const ruleExecutionLogger = ruleExecutionLoggerFactory( + const ruleExecutionLogger = await ruleExecutionLoggerFactory({ savedObjectsClient, - eventLogService, - logger, - { + context: { executionId, ruleId: alertId, + ruleUuid: params.ruleId, ruleName: rule.name, ruleType: rule.ruleTypeId, spaceId, - } - ); + }, + }); const completeRule = { ruleConfig: rule, @@ -126,24 +114,16 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = const refresh = actions.length ? 'wait_for' : false; - const buildRuleMessage = buildRuleMessageFactory({ - id: alertId, - executionId, - ruleId, - name, - index: spaceId, - }); - - logger.debug(buildRuleMessage('[+] Starting Signal Rule execution')); - logger.debug(buildRuleMessage(`interval: ${interval}`)); - - let wroteWarningStatus = false; + ruleExecutionLogger.debug('[+] Starting Signal Rule execution'); + ruleExecutionLogger.debug(`interval: ${interval}`); await ruleExecutionLogger.logStatusChange({ newStatus: RuleExecutionStatus.running, }); let result = createResultObject(state); + let wroteWarningStatus = false; + let hasError = false; const notificationRuleParams: NotificationRuleTypeParams = { ...params, @@ -179,8 +159,11 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = inputIndex = index ?? []; runtimeMappings = dataViewRuntimeMappings; } catch (exc) { - const errorMessage = buildRuleMessage(`Check for indices to search failed ${exc}`); - logger.error(errorMessage); + const errorMessage = + exc instanceof DataViewError + ? `Data View not found ${exc}` + : `Check for indices to search failed ${exc}`; + await ruleExecutionLogger.logStatusChange({ newStatus: RuleExecutionStatus.failed, message: errorMessage, @@ -199,8 +182,6 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = wroteWarningStatus = await hasReadIndexPrivileges({ privileges, - logger, - buildRuleMessage, ruleExecutionLogger, uiSettingsClient, }); @@ -225,42 +206,35 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = timestampFieldCapsResponse: timestampFieldCaps, inputIndices: inputIndex, ruleExecutionLogger, - logger, - buildRuleMessage, }); } } } catch (exc) { - const errorMessage = buildRuleMessage(`Check privileges failed to execute ${exc}`); - logger.warn(errorMessage); await ruleExecutionLogger.logStatusChange({ newStatus: RuleExecutionStatus['partial failure'], - message: errorMessage, + message: `Check privileges failed to execute ${exc}`, }); wroteWarningStatus = true; } + const { tuples, remainingGap } = getRuleRangeTuples({ - logger, + startedAt, previousStartedAt, from, to, interval, maxSignals: maxSignals ?? DEFAULT_MAX_SIGNALS, - buildRuleMessage, - startedAt, + ruleExecutionLogger, }); if (remainingGap.asMilliseconds() > 0) { - const gapString = remainingGap.humanize(); - const gapMessage = buildRuleMessage( - `${gapString} (${remainingGap.asMilliseconds()}ms) were not queried between this rule execution and the last execution, so signals may have been missed.`, - 'Consider increasing your look behind time or adding more Kibana instances.' - ); - logger.warn(gapMessage); hasError = true; + + const gapDuration = `${remainingGap.humanize()} (${remainingGap.asMilliseconds()}ms)`; + await ruleExecutionLogger.logStatusChange({ newStatus: RuleExecutionStatus.failed, - message: gapMessage, + message: `${gapDuration} were not queried between this rule execution and the last execution, so signals may have been missed. Consider increasing your look behind time or adding more Kibana instances`, metrics: { executionGap: remainingGap }, }); } @@ -280,10 +254,9 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = }); const bulkCreate = bulkCreateFactory( - logger, alertWithPersistence, - buildRuleMessage, - refresh + refresh, + ruleExecutionLogger ); const legacySignalFields: string[] = Object.keys(aadFieldConversion); @@ -310,21 +283,21 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = services, state: runState, runOpts: { + completeRule, inputIndex, - runtimeMappings, - buildRuleMessage, - bulkCreate, exceptionItems, - listClient, - completeRule, + runtimeMappings, searchAfterSize, tuple, + bulkCreate, wrapHits, wrapSequences, + listClient, ruleDataReader: ruleDataClient.getReader({ namespace: options.spaceId }), mergeStrategy, primaryTimestamp, secondaryTimestamp, + ruleExecutionLogger, }, }); @@ -346,10 +319,9 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = } if (result.warningMessages.length) { - const warningMessage = buildRuleMessage(truncateList(result.warningMessages).join()); await ruleExecutionLogger.logStatusChange({ newStatus: RuleExecutionStatus['partial failure'], - message: warningMessage, + message: truncateList(result.warningMessages).join(), }); } @@ -366,9 +338,7 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = ?.kibana_siem_app_url, }); - logger.debug( - buildRuleMessage(`Found ${createdSignalsCount} signals for notification.`) - ); + ruleExecutionLogger.debug(`Found ${createdSignalsCount} signals for notification.`); if (completeRule.ruleConfig.throttle != null) { // NOTE: Since this is throttled we have to call it even on an error condition, otherwise it will "reset" the throttle and fire early @@ -399,19 +369,17 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = } if (result.success) { - logger.debug(buildRuleMessage('[+] Signal Rule execution completed.')); - logger.debug( - buildRuleMessage( - `[+] Finished indexing ${createdSignalsCount} signals into ${ruleDataClient.indexNameWithNamespace( - spaceId - )}` - ) + ruleExecutionLogger.debug('[+] Signal Rule execution completed.'); + ruleExecutionLogger.debug( + `[+] Finished indexing ${createdSignalsCount} signals into ${ruleDataClient.indexNameWithNamespace( + spaceId + )}` ); if (!hasError && !wroteWarningStatus && !result.warning) { await ruleExecutionLogger.logStatusChange({ newStatus: RuleExecutionStatus.succeeded, - message: 'succeeded', + message: 'Rule execution completed successfully', metrics: { searchDurations: result.searchAfterTimes, indexingDurations: result.bulkCreateTimes, @@ -419,24 +387,17 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = }); } - logger.debug( - buildRuleMessage( - `[+] Finished indexing ${createdSignalsCount} ${ - !isEmpty(tuples) - ? `signals searched between date ranges ${JSON.stringify(tuples, null, 2)}` - : '' - }` - ) + ruleExecutionLogger.debug( + `[+] Finished indexing ${createdSignalsCount} ${ + !isEmpty(tuples) + ? `signals searched between date ranges ${JSON.stringify(tuples, null, 2)}` + : '' + }` ); } else { - const errorMessage = buildRuleMessage( - 'Bulk Indexing of signals failed:', - truncateList(result.errors).join() - ); - logger.error(errorMessage); await ruleExecutionLogger.logStatusChange({ newStatus: RuleExecutionStatus.failed, - message: errorMessage, + message: `Bulk Indexing of signals failed: ${truncateList(result.errors).join()}`, metrics: { searchDurations: result.searchAfterTimes, indexingDurations: result.bulkCreateTimes, @@ -445,15 +406,10 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = } } catch (error) { const errorMessage = error.message ?? '(no error message given)'; - const message = buildRuleMessage( - 'An error occurred during rule execution:', - `message: "${errorMessage}"` - ); - logger.error(message); await ruleExecutionLogger.logStatusChange({ newStatus: RuleExecutionStatus.failed, - message, + message: `An error occurred during rule execution: message: "${errorMessage}"`, metrics: { searchDurations: result.searchAfterTimes, indexingDurations: result.bulkCreateTimes, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/create_eql_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/create_eql_alert_type.ts index 22676f6d120c3..abfdc5fe491a7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/create_eql_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/create_eql_alert_type.ts @@ -14,10 +14,11 @@ import { eqlRuleParams } from '../../schemas/rule_schemas'; import { eqlExecutor } from '../../signals/executors/eql'; import type { CreateRuleOptions, SecurityAlertType } from '../types'; import { validateImmutable, validateIndexPatterns } from '../utils'; + export const createEqlAlertType = ( createOptions: CreateRuleOptions ): SecurityAlertType => { - const { experimentalFeatures, logger, version } = createOptions; + const { version } = createOptions; return { id: EQL_RULE_TYPE_ID, name: 'Event Correlation Rule', @@ -63,12 +64,13 @@ export const createEqlAlertType = ( async executor(execOptions) { const { runOpts: { + completeRule, + tuple, inputIndex, runtimeMappings, - bulkCreate, exceptionItems, - completeRule, - tuple, + ruleExecutionLogger, + bulkCreate, wrapHits, wrapSequences, primaryTimestamp, @@ -79,16 +81,15 @@ export const createEqlAlertType = ( } = execOptions; const result = await eqlExecutor({ + completeRule, + tuple, inputIndex, runtimeMappings, - bulkCreate, exceptionItems, - experimentalFeatures, - logger, - completeRule, + ruleExecutionLogger, services, - tuple, version, + bulkCreate, wrapHits, wrapSequences, primaryTimestamp, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/build_rule_message_factory.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/build_rule_message_factory.ts deleted file mode 100644 index bac112bb3cab1..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/build_rule_message_factory.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export type BuildRuleMessage = (...messages: string[]) => string; -export interface BuildRuleMessageFactoryParams { - executionId: string; - name: string; - id: string; - ruleId: string | null | undefined; - index: string; -} - -// TODO: change `index` param to `spaceId` -export const buildRuleMessageFactory = - ({ executionId, id, ruleId, index, name }: BuildRuleMessageFactoryParams): BuildRuleMessage => - (...messages) => - [ - ...messages, - `name: "${name}"`, - `id: "${id}"`, - `rule id: "${ruleId ?? '(unknown rule id)'}"`, - `execution id: "${executionId}"`, - `space ID: "${index}"`, - ].join(' '); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/bulk_create_factory.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/bulk_create_factory.ts index bb02aba492ed5..fadba855baa32 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/bulk_create_factory.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/bulk_create_factory.ts @@ -8,10 +8,9 @@ import { performance } from 'perf_hooks'; import { isEmpty } from 'lodash'; -import type { Logger } from '@kbn/core/server'; import type { PersistenceAlertService } from '@kbn/rule-registry-plugin/server'; import type { AlertWithCommonFieldsLatest } from '@kbn/rule-registry-plugin/common/schemas'; -import type { BuildRuleMessage } from '../../signals/rule_messages'; +import type { IRuleExecutionLogForExecutors } from '../../rule_monitoring'; import { makeFloatString } from '../../signals/utils'; import type { RefreshTypes } from '../../types'; import type { @@ -30,10 +29,9 @@ export interface GenericBulkCreateResponse { export const bulkCreateFactory = ( - logger: Logger, alertWithPersistence: PersistenceAlertService, - buildRuleMessage: BuildRuleMessage, - refreshForBulkCreate: RefreshTypes + refreshForBulkCreate: RefreshTypes, + ruleExecutionLogger: IRuleExecutionLogForExecutors ) => async ( wrappedDocs: Array>, @@ -64,15 +62,13 @@ export const bulkCreateFactory = const end = performance.now(); - logger.debug( - buildRuleMessage( - `individual bulk process time took: ${makeFloatString(end - start)} milliseconds` - ) + ruleExecutionLogger.debug( + `individual bulk process time took: ${makeFloatString(end - start)} milliseconds` ); if (!isEmpty(errors)) { - logger.debug( - buildRuleMessage(`[-] bulkResponse had errors with responses of: ${JSON.stringify(errors)}`) + ruleExecutionLogger.debug( + `[-] bulkResponse had errors with responses of: ${JSON.stringify(errors)}` ); return { errors: Object.keys(errors), diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/index.ts index 1c1f6fab6322b..4d93238974487 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/index.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/index.ts @@ -5,7 +5,6 @@ * 2.0. */ -export * from './build_rule_message_factory'; export * from './bulk_create_factory'; export * from './wrap_hits_factory'; export * from './wrap_sequences_factory'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/create_indicator_match_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/create_indicator_match_alert_type.ts index 70cf31d8ae7b5..94fc6d78965bb 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/create_indicator_match_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/create_indicator_match_alert_type.ts @@ -14,10 +14,11 @@ import { threatRuleParams } from '../../schemas/rule_schemas'; import { threatMatchExecutor } from '../../signals/executors/threat_match'; import type { CreateRuleOptions, SecurityAlertType } from '../types'; import { validateImmutable, validateIndexPatterns } from '../utils'; + export const createIndicatorMatchAlertType = ( createOptions: CreateRuleOptions ): SecurityAlertType => { - const { eventsTelemetry, experimentalFeatures, logger, version } = createOptions; + const { eventsTelemetry, version } = createOptions; return { id: INDICATOR_RULE_TYPE_ID, name: 'Indicator Match Rule', @@ -66,13 +67,13 @@ export const createIndicatorMatchAlertType = ( runOpts: { inputIndex, runtimeMappings, - buildRuleMessage, - bulkCreate, + completeRule, + tuple, exceptionItems, listClient, - completeRule, + ruleExecutionLogger, searchAfterSize, - tuple, + bulkCreate, wrapHits, primaryTimestamp, secondaryTimestamp, @@ -84,18 +85,16 @@ export const createIndicatorMatchAlertType = ( const result = await threatMatchExecutor({ inputIndex, runtimeMappings, - buildRuleMessage, - bulkCreate, - exceptionItems, - experimentalFeatures, - eventsTelemetry, - listClient, - logger, completeRule, - searchAfterSize, - services, tuple, + listClient, + exceptionItems, + services, version, + searchAfterSize, + ruleExecutionLogger, + eventsTelemetry, + bulkCreate, wrapHits, primaryTimestamp, secondaryTimestamp, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/create_ml_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/create_ml_alert_type.ts index abfc279a6e08f..926615fc8d176 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/create_ml_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/create_ml_alert_type.ts @@ -18,7 +18,7 @@ import { validateImmutable } from '../utils'; export const createMlAlertType = ( createOptions: CreateRuleOptions ): SecurityAlertType => { - const { logger, ml } = createOptions; + const { ml } = createOptions; return { id: ML_RULE_TYPE_ID, name: 'Machine Learning Rule', @@ -63,11 +63,11 @@ export const createMlAlertType = ( async executor(execOptions) { const { runOpts: { - buildRuleMessage, bulkCreate, + completeRule, exceptionItems, listClient, - completeRule, + ruleExecutionLogger, tuple, wrapHits, }, @@ -76,15 +76,14 @@ export const createMlAlertType = ( } = execOptions; const result = await mlExecutor({ - buildRuleMessage, - bulkCreate, - exceptionItems, - listClient, - logger, - ml, completeRule, - services, tuple, + ml, + listClient, + exceptionItems, + services, + ruleExecutionLogger, + bulkCreate, wrapHits, }); return { ...result, state }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/new_terms/create_new_terms_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/new_terms/create_new_terms_alert_type.ts index 9bea2eb495b26..e02bcc6251f40 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/new_terms/create_new_terms_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/new_terms/create_new_terms_alert_type.ts @@ -100,7 +100,7 @@ export const createNewTermsAlertType = ( async executor(execOptions) { const { runOpts: { - buildRuleMessage, + ruleExecutionLogger, bulkCreate, completeRule, exceptionItems, @@ -185,12 +185,11 @@ export const createNewTermsAlertType = ( from: tuple.from.toISOString(), to: tuple.to.toISOString(), services, + ruleExecutionLogger, filter, - logger, pageSize: 0, primaryTimestamp, secondaryTimestamp, - buildRuleMessage, runtimeMappings, }); const searchResultWithAggs = searchResult as RecentTermsAggResult; @@ -239,12 +238,11 @@ export const createNewTermsAlertType = ( from: parsedHistoryWindowSize.toISOString(), to: tuple.to.toISOString(), services, + ruleExecutionLogger, filter, - logger, pageSize: 0, primaryTimestamp, secondaryTimestamp, - buildRuleMessage, }); searchAfterResults.searchDurations.push(pageSearchDuration); searchAfterResults.searchErrors.push(...pageSearchErrors); @@ -285,12 +283,11 @@ export const createNewTermsAlertType = ( from: tuple.from.toISOString(), to: tuple.to.toISOString(), services, + ruleExecutionLogger, filter, - logger, pageSize: 0, primaryTimestamp, secondaryTimestamp, - buildRuleMessage, }); searchAfterResults.searchDurations.push(docFetchSearchDuration); searchAfterResults.searchErrors.push(...docFetchSearchErrors); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.test.ts index 4a6b6b3c9d867..c7328055e1a7d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.test.ts @@ -13,7 +13,7 @@ import { createRuleTypeMocks } from '../__mocks__/rule_type'; import { createSecurityRuleTypeWrapper } from '../create_security_rule_type_wrapper'; import { createMockConfig } from '../../routes/__mocks__'; import { createMockTelemetryEventsSender } from '../../../telemetry/__mocks__'; -import { ruleExecutionLogMock } from '../../rule_execution_log/__mocks__'; +import { ruleExecutionLogMock } from '../../rule_monitoring/mocks'; import { sampleDocNoSortId } from '../../signals/__mocks__/es_results'; import { getQueryRuleParams } from '../../schemas/rule_schemas.mock'; @@ -32,14 +32,13 @@ jest.mock('../utils/get_list_client', () => ({ describe('Custom Query Alerts', () => { const mocks = createRuleTypeMocks(); const { dependencies, executor, services } = mocks; - const { alerting, eventLogService, lists, logger, ruleDataClient } = dependencies; + const { alerting, lists, logger, ruleDataClient } = dependencies; const securityRuleTypeWrapper = createSecurityRuleTypeWrapper({ lists, logger, config: createMockConfig(), ruleDataClient, - eventLogService, - ruleExecutionLoggerFactory: () => ruleExecutionLogMock.forExecutors.create(), + ruleExecutionLoggerFactory: () => Promise.resolve(ruleExecutionLogMock.forExecutors.create()), version: '8.3', }); const eventsTelemetry = createMockTelemetryEventsSender(true); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.ts index b9a6c17bfc261..14e309a83c959 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.ts @@ -17,7 +17,7 @@ import { validateImmutable, validateIndexPatterns } from '../utils'; export const createQueryAlertType = ( createOptions: CreateRuleOptions ): SecurityAlertType => { - const { eventsTelemetry, experimentalFeatures, logger, version } = createOptions; + const { eventsTelemetry, experimentalFeatures, version } = createOptions; return { id: QUERY_RULE_TYPE_ID, name: 'Custom Query Rule', @@ -65,13 +65,13 @@ export const createQueryAlertType = ( runOpts: { inputIndex, runtimeMappings, - buildRuleMessage, - bulkCreate, + completeRule, + tuple, exceptionItems, listClient, - completeRule, + ruleExecutionLogger, searchAfterSize, - tuple, + bulkCreate, wrapHits, primaryTimestamp, secondaryTimestamp, @@ -81,18 +81,17 @@ export const createQueryAlertType = ( } = execOptions; const result = await queryExecutor({ - buildRuleMessage, - bulkCreate, + completeRule, + tuple, exceptionItems, + listClient, experimentalFeatures, + ruleExecutionLogger, eventsTelemetry, - listClient, - logger, - completeRule, - searchAfterSize, services, - tuple, version, + searchAfterSize, + bulkCreate, wrapHits, inputIndex, runtimeMappings, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/saved_query/create_saved_query_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/saved_query/create_saved_query_alert_type.ts index fe9c377aa04f6..f8009220581e1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/saved_query/create_saved_query_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/saved_query/create_saved_query_alert_type.ts @@ -17,7 +17,7 @@ import { validateImmutable, validateIndexPatterns } from '../utils'; export const createSavedQueryAlertType = ( createOptions: CreateRuleOptions ): SecurityAlertType => { - const { experimentalFeatures, logger, version } = createOptions; + const { experimentalFeatures, version } = createOptions; return { id: SAVED_QUERY_RULE_TYPE_ID, name: 'Saved Query Rule', @@ -65,13 +65,13 @@ export const createSavedQueryAlertType = ( runOpts: { inputIndex, runtimeMappings, - buildRuleMessage, - bulkCreate, + completeRule, + tuple, exceptionItems, listClient, - completeRule, + ruleExecutionLogger, searchAfterSize, - tuple, + bulkCreate, wrapHits, primaryTimestamp, secondaryTimestamp, @@ -83,18 +83,17 @@ export const createSavedQueryAlertType = ( const result = await queryExecutor({ inputIndex, runtimeMappings, - buildRuleMessage, - bulkCreate, + completeRule: completeRule as CompleteRule, + tuple, exceptionItems, experimentalFeatures, - eventsTelemetry: undefined, listClient, - logger, - completeRule: completeRule as CompleteRule, - searchAfterSize, + ruleExecutionLogger, + eventsTelemetry: undefined, services, - tuple, version, + searchAfterSize, + bulkCreate, wrapHits, primaryTimestamp, secondaryTimestamp, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/create_threshold_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/create_threshold_alert_type.ts index 43e57057bcbc1..5c8426e194f0d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/create_threshold_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/create_threshold_alert_type.ts @@ -19,7 +19,7 @@ import { validateImmutable, validateIndexPatterns } from '../utils'; export const createThresholdAlertType = ( createOptions: CreateRuleOptions ): SecurityAlertType => { - const { experimentalFeatures, logger, version } = createOptions; + const { version } = createOptions; return { id: THRESHOLD_RULE_TYPE_ID, name: 'Threshold Rule', @@ -65,7 +65,6 @@ export const createThresholdAlertType = ( async executor(execOptions) { const { runOpts: { - buildRuleMessage, bulkCreate, exceptionItems, completeRule, @@ -76,6 +75,7 @@ export const createThresholdAlertType = ( runtimeMappings, primaryTimestamp, secondaryTimestamp, + ruleExecutionLogger, }, services, startedAt, @@ -83,17 +83,15 @@ export const createThresholdAlertType = ( } = execOptions; const result = await thresholdExecutor({ - buildRuleMessage, - bulkCreate, - exceptionItems, - experimentalFeatures, - logger, completeRule, + tuple, + exceptionItems, + ruleExecutionLogger, services, + version, startedAt, state, - tuple, - version, + bulkCreate, wrapHits, ruleDataReader, inputIndex, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts index 3212fee6a1c1d..d2ed6965a547a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts @@ -24,11 +24,10 @@ import type { IRuleDataClient, IRuleDataReader, } from '@kbn/rule-registry-plugin/server'; -import type { IEventLogService } from '@kbn/event-log-plugin/server'; + import type { ConfigType } from '../../../config'; import type { SetupPlugins } from '../../../plugin'; import type { CompleteRule, RuleParams } from '../schemas/rule_schemas'; -import type { BuildRuleMessage } from '../signals/rule_messages'; import type { BulkCreate, SearchAfterAndBulkCreateReturnType, @@ -37,7 +36,7 @@ import type { } from '../signals/types'; import type { ExperimentalFeatures } from '../../../../common/experimental_features'; import type { ITelemetryEventsSender } from '../../telemetry/sender'; -import type { RuleExecutionLogForExecutorsFactory } from '../rule_execution_log'; +import type { IRuleExecutionLogForExecutors, IRuleExecutionLogService } from '../rule_monitoring'; export interface SecurityAlertTypeReturnValue { bulkCreateTimes: string[]; @@ -53,17 +52,17 @@ export interface SecurityAlertTypeReturnValue { } export interface RunOpts { - buildRuleMessage: BuildRuleMessage; - bulkCreate: BulkCreate; - exceptionItems: ExceptionListItemSchema[]; - listClient: ListClient; completeRule: CompleteRule; - searchAfterSize: number; tuple: { to: Moment; from: Moment; maxSignals: number; }; + exceptionItems: ExceptionListItemSchema[]; + ruleExecutionLogger: IRuleExecutionLogForExecutors; + listClient: ListClient; + searchAfterSize: number; + bulkCreate: BulkCreate; wrapHits: WrapHits; wrapSequences: WrapSequences; ruleDataReader: IRuleDataReader; @@ -102,8 +101,7 @@ export interface CreateSecurityRuleTypeWrapperProps { logger: Logger; config: ConfigType; ruleDataClient: IRuleDataClient; - eventLogService: IEventLogService; - ruleExecutionLoggerFactory: RuleExecutionLogForExecutorsFactory; + ruleExecutionLoggerFactory: IRuleExecutionLogService['createClientForExecutors']; version: string; } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/delete_rules.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/delete_rules.test.ts index 7c0daefc6c6c3..0c7edae7022c7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/delete_rules.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/delete_rules.test.ts @@ -6,7 +6,7 @@ */ import { rulesClientMock } from '@kbn/alerting-plugin/server/mocks'; -import { ruleExecutionLogMock } from '../rule_execution_log/__mocks__'; +import { ruleExecutionLogMock } from '../rule_monitoring/mocks'; import { deleteRules } from './delete_rules'; import type { DeleteRuleOptions } from './types'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts index 569b253dd9089..6e3b1a3dea2cb 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts @@ -8,27 +8,28 @@ import type { Readable } from 'stream'; import type { SavedObjectAttributes, SavedObjectsClientContract } from '@kbn/core/server'; +import type { SanitizedRule } from '@kbn/alerting-plugin/common'; +import type { RulesClient, PartialRule } from '@kbn/alerting-plugin/server'; import { ruleTypeMappings } from '@kbn/securitysolution-rules'; -import type { RulesClient, PartialRule } from '@kbn/alerting-plugin/server'; -import type { SanitizedRule } from '@kbn/alerting-plugin/common'; -import type { UpdateRulesSchema } from '../../../../common/detection_engine/schemas/request'; import type { + FieldsOrUndefined, Id, IdOrUndefined, - RuleIdOrUndefined, - PerPageOrUndefined, PageOrUndefined, - SortFieldOrUndefined, + PerPageOrUndefined, QueryFilterOrUndefined, - FieldsOrUndefined, + RuleIdOrUndefined, + SortFieldOrUndefined, SortOrderOrUndefined, } from '../../../../common/detection_engine/schemas/common'; -import type { RuleParams } from '../schemas/rule_schemas'; -import type { IRuleExecutionLogForRoutes } from '../rule_execution_log'; import type { CreateRulesSchema } from '../../../../common/detection_engine/schemas/request/rule_schemas'; import type { PatchRulesSchema } from '../../../../common/detection_engine/schemas/request/patch_rules_schema'; +import type { UpdateRulesSchema } from '../../../../common/detection_engine/schemas/request'; + +import type { RuleParams } from '../schemas/rule_schemas'; +import type { IRuleExecutionLogForRoutes } from '../rule_monitoring'; export type RuleAlertType = SanitizedRule; @@ -96,12 +97,12 @@ export interface DeleteRuleOptions { export interface FindRuleOptions { rulesClient: RulesClient; - perPage: PerPageOrUndefined; - page: PageOrUndefined; - sortField: SortFieldOrUndefined; filter: QueryFilterOrUndefined; fields: FieldsOrUndefined; + sortField: SortFieldOrUndefined; sortOrder: SortOrderOrUndefined; + page: PageOrUndefined; + perPage: PerPageOrUndefined; } export interface LegacyMigrateParams { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.test.ts index dc28f2184323c..d71274c7f1540 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.test.ts @@ -14,7 +14,7 @@ import { getAddPrepackagedRulesSchemaMock, getAddPrepackagedThreatMatchRulesSchemaMock, } from '../../../../common/detection_engine/schemas/request/add_prepackaged_rules_schema.mock'; -import { ruleExecutionLogMock } from '../rule_execution_log/__mocks__'; +import { ruleExecutionLogMock } from '../rule_monitoring/mocks'; import { legacyMigrate } from './utils'; import { getQueryRuleParams, getThreatRuleParams } from '../schemas/rule_schemas.mock'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.ts index b6b528c307b38..1487aa79e4874 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.ts @@ -16,7 +16,7 @@ import type { RuleParams } from '../schemas/rule_schemas'; import { legacyMigrate } from './utils'; import { deleteRules } from './delete_rules'; import { PrepackagedRulesError } from '../routes/rules/add_prepackaged_rules_route'; -import type { IRuleExecutionLogForRoutes } from '../rule_execution_log'; +import type { IRuleExecutionLogForRoutes } from '../rule_monitoring'; import { createRules } from './create_rules'; import { transformAlertToRuleAction } from '../../../../common/detection_engine/transform_actions'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_converters.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_converters.ts index 0056606f17f15..74d55c262d17b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_converters.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_converters.ts @@ -41,9 +41,9 @@ import { assertUnreachable } from '../../../../common/utility_types'; import type { RelatedIntegrationArray, RequiredFieldArray, - RuleExecutionSummary, SetupGuide, } from '../../../../common/detection_engine/schemas/common'; +import type { RuleExecutionSummary } from '../../../../common/detection_engine/rule_monitoring'; import { eqlPatchParams, machineLearningPatchParams, @@ -81,7 +81,7 @@ import { } from '../rules/utils'; // eslint-disable-next-line no-restricted-imports import type { LegacyRuleActions } from '../rule_actions/legacy_types'; -import { mergeRuleExecutionSummary } from '../rule_execution_log'; +import { mergeRuleExecutionSummary } from '../rule_monitoring'; // These functions provide conversions from the request API schema to the internal rule schema and from the internal rule schema // to the response API schema. This provides static type-check assurances that the internal schema is in sync with the API schema for diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_ml_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_ml_signals.ts index ef8662960d0a5..11b7a035d8a2f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_ml_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/bulk_create_ml_signals.ts @@ -8,7 +8,6 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { flow, omit } from 'lodash/fp'; import set from 'set-value'; -import type { Logger } from '@kbn/core/server'; import type { AlertInstanceContext, AlertInstanceState, @@ -16,20 +15,19 @@ import type { } from '@kbn/alerting-plugin/server'; import type { GenericBulkCreateResponse } from '../rule_types/factories'; import type { Anomaly } from '../../machine_learning'; -import type { BuildRuleMessage } from './rule_messages'; import type { BulkCreate, WrapHits } from './types'; import type { CompleteRule, MachineLearningRuleParams } from '../schemas/rule_schemas'; import { buildReasonMessageForMlAlert } from './reason_formatters'; import type { BaseFieldsLatest } from '../../../../common/detection_engine/schemas/alerts'; +import type { IRuleExecutionLogForExecutors } from '../rule_monitoring'; interface BulkCreateMlSignalsParams { anomalyHits: Array>; completeRule: CompleteRule; services: RuleExecutorServices; - logger: Logger; + ruleExecutionLogger: IRuleExecutionLogForExecutors; id: string; signalsIndex: string; - buildRuleMessage: BuildRuleMessage; bulkCreate: BulkCreate; wrapHits: WrapHits; } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.test.ts index 3f52dd1fcd4e8..ab52fedf60e41 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.test.ts @@ -6,24 +6,23 @@ */ import dateMath from '@kbn/datemath'; -import { loggingSystemMock } from '@kbn/core/server/mocks'; import type { RuleExecutorServicesMock } from '@kbn/alerting-plugin/server/mocks'; import { alertsMock } from '@kbn/alerting-plugin/server/mocks'; -import { eqlExecutor } from './eql'; import { getExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_item_schema.mock'; import { getEntryListMock } from '@kbn/lists-plugin/common/schemas/types/entry_list.mock'; -import { getCompleteRuleMock, getEqlRuleParams } from '../../schemas/rule_schemas.mock'; +import { DEFAULT_INDEX_PATTERN } from '../../../../../common/constants'; import { getIndexVersion } from '../../routes/index/get_index_version'; import { SIGNALS_TEMPLATE_VERSION } from '../../routes/index/get_signals_template'; -import { allowedExperimentalValues } from '../../../../../common/experimental_features'; import type { EqlRuleParams } from '../../schemas/rule_schemas'; -import { DEFAULT_INDEX_PATTERN } from '../../../../../common/constants'; +import { getCompleteRuleMock, getEqlRuleParams } from '../../schemas/rule_schemas.mock'; +import { ruleExecutionLogMock } from '../../rule_monitoring/mocks'; +import { eqlExecutor } from './eql'; jest.mock('../../routes/index/get_index_version'); describe('eql_executor', () => { const version = '8.0.0'; - let logger: ReturnType; + const ruleExecutionLogger = ruleExecutionLogMock.forExecutors.create(); let alertServices: RuleExecutorServicesMock; (getIndexVersion as jest.Mock).mockReturnValue(SIGNALS_TEMPLATE_VERSION); const params = getEqlRuleParams(); @@ -35,8 +34,8 @@ describe('eql_executor', () => { }; beforeEach(() => { + jest.clearAllMocks(); alertServices = alertsMock.createRuleExecutorServices(); - logger = loggingSystemMock.createLogger(); alertServices.scopedClusterClient.asCurrentUser.eql.search.mockResolvedValue({ hits: { total: { relation: 'eq', value: 10 }, @@ -54,10 +53,9 @@ describe('eql_executor', () => { completeRule: eqlCompleteRule, tuple, exceptionItems, - experimentalFeatures: allowedExperimentalValues, + ruleExecutionLogger, services: alertServices, version, - logger, bulkCreate: jest.fn(), wrapHits: jest.fn(), wrapSequences: jest.fn(), diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.ts index 5e54515be1056..c1de5fc5f18d9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/eql.ts @@ -7,7 +7,6 @@ import { performance } from 'perf_hooks'; import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; -import type { Logger } from '@kbn/core/server'; import type { AlertInstanceContext, AlertInstanceState, @@ -27,7 +26,6 @@ import type { SignalSource, } from '../types'; import { createSearchAfterReturnType, makeFloatString } from '../utils'; -import type { ExperimentalFeatures } from '../../../../../common/experimental_features'; import { buildReasonMessageForEqlAlert } from '../reason_formatters'; import type { CompleteRule, EqlRuleParams } from '../../schemas/rule_schemas'; import { withSecuritySpan } from '../../../../utils/with_security_span'; @@ -35,6 +33,7 @@ import type { BaseFieldsLatest, WrappedFieldsLatest, } from '../../../../../common/detection_engine/schemas/alerts'; +import type { IRuleExecutionLogForExecutors } from '../../rule_monitoring'; export const eqlExecutor = async ({ inputIndex, @@ -42,10 +41,9 @@ export const eqlExecutor = async ({ completeRule, tuple, exceptionItems, - experimentalFeatures, + ruleExecutionLogger, services, version, - logger, bulkCreate, wrapHits, wrapSequences, @@ -57,10 +55,9 @@ export const eqlExecutor = async ({ completeRule: CompleteRule; tuple: RuleRangeTuple; exceptionItems: ExceptionListItemSchema[]; - experimentalFeatures: ExperimentalFeatures; + ruleExecutionLogger: IRuleExecutionLogForExecutors; services: RuleExecutorServices; version: string; - logger: Logger; bulkCreate: BulkCreate; wrapHits: WrapHits; wrapSequences: WrapSequences; @@ -94,8 +91,9 @@ export const eqlExecutor = async ({ tiebreakerField: ruleParams.tiebreakerField, }); + ruleExecutionLogger.debug(`EQL query request: ${JSON.stringify(request)}`); + const eqlSignalSearchStart = performance.now(); - logger.debug(`EQL query request: ${JSON.stringify(request)}`); const response = await services.scopedClusterClient.asCurrentUser.eql.search( request diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/ml.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/ml.test.ts index c838f3243fc33..58a693c71bc41 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/ml.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/ml.test.ts @@ -6,18 +6,17 @@ */ import dateMath from '@kbn/datemath'; -import { loggingSystemMock } from '@kbn/core/server/mocks'; import type { RuleExecutorServicesMock } from '@kbn/alerting-plugin/server/mocks'; import { alertsMock } from '@kbn/alerting-plugin/server/mocks'; import { mlExecutor } from './ml'; import { getExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_item_schema.mock'; import { getCompleteRuleMock, getMlRuleParams } from '../../schemas/rule_schemas.mock'; -import { buildRuleMessageFactory } from '../rule_messages'; import { getListClientMock } from '@kbn/lists-plugin/server/services/lists/list_client.mock'; import { findMlSignals } from '../find_ml_signals'; import { bulkCreateMlSignals } from '../bulk_create_ml_signals'; import { mlPluginServerMock } from '@kbn/ml-plugin/server/mocks'; import type { MachineLearningRuleParams } from '../../schemas/rule_schemas'; +import { ruleExecutionLogMock } from '../../rule_monitoring/mocks'; jest.mock('../find_ml_signals'); jest.mock('../bulk_create_ml_signals'); @@ -25,32 +24,30 @@ jest.mock('../bulk_create_ml_signals'); describe('ml_executor', () => { let jobsSummaryMock: jest.Mock; let mlMock: ReturnType; - const exceptionItems = [getExceptionListItemSchemaMock()]; - let logger: ReturnType; let alertServices: RuleExecutorServicesMock; + let ruleExecutionLogger: ReturnType; const params = getMlRuleParams(); const mlCompleteRule = getCompleteRuleMock(params); - + const exceptionItems = [getExceptionListItemSchemaMock()]; const tuple = { from: dateMath.parse(params.from)!, to: dateMath.parse(params.to)!, maxSignals: params.maxSignals, }; - const buildRuleMessage = buildRuleMessageFactory({ - id: mlCompleteRule.alertId, - ruleId: mlCompleteRule.ruleParams.ruleId, - name: mlCompleteRule.ruleConfig.name, - index: mlCompleteRule.ruleParams.outputIndex, - }); beforeEach(() => { jobsSummaryMock = jest.fn(); - alertServices = alertsMock.createRuleExecutorServices(); - logger = loggingSystemMock.createLogger(); mlMock = mlPluginServerMock.createSetupContract(); mlMock.jobServiceProvider.mockReturnValue({ jobsSummary: jobsSummaryMock, }); + alertServices = alertsMock.createRuleExecutorServices(); + ruleExecutionLogger = ruleExecutionLogMock.forExecutors.create({ + ruleId: mlCompleteRule.alertId, + ruleUuid: mlCompleteRule.ruleParams.ruleId, + ruleName: mlCompleteRule.ruleConfig.name, + ruleType: mlCompleteRule.ruleConfig.ruleTypeId, + }); (findMlSignals as jest.Mock).mockResolvedValue({ _shards: {}, hits: { @@ -73,8 +70,7 @@ describe('ml_executor', () => { ml: undefined, exceptionItems, services: alertServices, - logger, - buildRuleMessage, + ruleExecutionLogger, listClient: getListClientMock(), bulkCreate: jest.fn(), wrapHits: jest.fn(), @@ -90,14 +86,15 @@ describe('ml_executor', () => { ml: mlMock, exceptionItems, services: alertServices, - logger, - buildRuleMessage, + ruleExecutionLogger, listClient: getListClientMock(), bulkCreate: jest.fn(), wrapHits: jest.fn(), }); - expect(logger.warn).toHaveBeenCalled(); - expect(logger.warn.mock.calls[0][0]).toContain('Machine learning job(s) are not started'); + expect(ruleExecutionLogger.warn).toHaveBeenCalled(); + expect(ruleExecutionLogger.warn.mock.calls[0][0]).toContain( + 'Machine learning job(s) are not started' + ); expect(response.warningMessages.length).toEqual(1); }); @@ -116,14 +113,15 @@ describe('ml_executor', () => { ml: mlMock, exceptionItems, services: alertServices, - logger, - buildRuleMessage, + ruleExecutionLogger, listClient: getListClientMock(), bulkCreate: jest.fn(), wrapHits: jest.fn(), }); - expect(logger.warn).toHaveBeenCalled(); - expect(logger.warn.mock.calls[0][0]).toContain('Machine learning job(s) are not started'); + expect(ruleExecutionLogger.warn).toHaveBeenCalled(); + expect(ruleExecutionLogger.warn.mock.calls[0][0]).toContain( + 'Machine learning job(s) are not started' + ); expect(response.warningMessages.length).toEqual(1); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/ml.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/ml.ts index 4e568128e9f03..0c6c9b8181512 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/ml.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/ml.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { KibanaRequest, Logger } from '@kbn/core/server'; +import type { KibanaRequest } from '@kbn/core/server'; import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; import type { AlertInstanceContext, @@ -18,11 +18,11 @@ import type { CompleteRule, MachineLearningRuleParams } from '../../schemas/rule import { bulkCreateMlSignals } from '../bulk_create_ml_signals'; import { filterEventsAgainstList } from '../filters/filter_events_against_list'; import { findMlSignals } from '../find_ml_signals'; -import type { BuildRuleMessage } from '../rule_messages'; import type { BulkCreate, RuleRangeTuple, WrapHits } from '../types'; import { createErrorsFromShard, createSearchAfterReturnType, mergeReturns } from '../utils'; import type { SetupPlugins } from '../../../../plugin'; import { withSecuritySpan } from '../../../../utils/with_security_span'; +import type { IRuleExecutionLogForExecutors } from '../../rule_monitoring'; export const mlExecutor = async ({ completeRule, @@ -31,8 +31,7 @@ export const mlExecutor = async ({ listClient, exceptionItems, services, - logger, - buildRuleMessage, + ruleExecutionLogger, bulkCreate, wrapHits, }: { @@ -42,8 +41,7 @@ export const mlExecutor = async ({ listClient: ListClient; exceptionItems: ExceptionListItemSchema[]; services: RuleExecutorServices; - logger: Logger; - buildRuleMessage: BuildRuleMessage; + ruleExecutionLogger: IRuleExecutionLogForExecutors; bulkCreate: BulkCreate; wrapHits: WrapHits; }) => { @@ -69,7 +67,7 @@ export const mlExecutor = async ({ jobSummaries.length < 1 || jobSummaries.some((job) => !isJobStarted(job.jobState, job.datafeedState)) ) { - const warningMessage = buildRuleMessage( + const warningMessage = [ 'Machine learning job(s) are not started:', ...jobSummaries.map((job) => [ @@ -77,10 +75,11 @@ export const mlExecutor = async ({ `job status: "${job.jobState}"`, `datafeed status: "${job.datafeedState}"`, ].join(', ') - ) - ); + ), + ].join(' '); + result.warningMessages.push(warningMessage); - logger.warn(warningMessage); + ruleExecutionLogger.warn(warningMessage); result.warning = true; } @@ -100,24 +99,23 @@ export const mlExecutor = async ({ const [filteredAnomalyHits, _] = await filterEventsAgainstList({ listClient, exceptionsList: exceptionItems, - logger, + ruleExecutionLogger, events: anomalyResults.hits.hits, - buildRuleMessage, }); const anomalyCount = filteredAnomalyHits.length; if (anomalyCount) { - logger.debug(buildRuleMessage(`Found ${anomalyCount} signals from ML anomalies.`)); + ruleExecutionLogger.debug(`Found ${anomalyCount} signals from ML anomalies`); } + const { success, errors, bulkCreateDuration, createdItemsCount, createdItems } = await bulkCreateMlSignals({ anomalyHits: filteredAnomalyHits, completeRule, services, - logger, + ruleExecutionLogger, id: completeRule.alertId, signalsIndex: ruleParams.outputIndex, - buildRuleMessage, bulkCreate, wrapHits, }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/query.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/query.ts index fcfa61dcf3624..48e979472c4c7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/query.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/query.ts @@ -5,7 +5,6 @@ * 2.0. */ -import type { Logger } from '@kbn/core/server'; import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; import type { AlertInstanceContext, @@ -19,7 +18,6 @@ import { getFilter } from '../get_filter'; import { searchAfterAndBulkCreate } from '../search_after_bulk_create'; import type { RuleRangeTuple, BulkCreate, WrapHits } from '../types'; import type { ITelemetryEventsSender } from '../../../telemetry/sender'; -import type { BuildRuleMessage } from '../rule_messages'; import type { CompleteRule, SavedQueryRuleParams, @@ -28,21 +26,21 @@ import type { import type { ExperimentalFeatures } from '../../../../../common/experimental_features'; import { buildReasonMessageForQueryAlert } from '../reason_formatters'; import { withSecuritySpan } from '../../../../utils/with_security_span'; +import type { IRuleExecutionLogForExecutors } from '../../rule_monitoring'; export const queryExecutor = async ({ inputIndex, runtimeMappings, completeRule, tuple, - listClient, exceptionItems, + listClient, experimentalFeatures, + ruleExecutionLogger, + eventsTelemetry, services, version, searchAfterSize, - logger, - eventsTelemetry, - buildRuleMessage, bulkCreate, wrapHits, primaryTimestamp, @@ -52,15 +50,14 @@ export const queryExecutor = async ({ runtimeMappings: estypes.MappingRuntimeFields | undefined; completeRule: CompleteRule | CompleteRule; tuple: RuleRangeTuple; - listClient: ListClient; exceptionItems: ExceptionListItemSchema[]; + listClient: ListClient; experimentalFeatures: ExperimentalFeatures; + ruleExecutionLogger: IRuleExecutionLogForExecutors; + eventsTelemetry: ITelemetryEventsSender | undefined; services: RuleExecutorServices; version: string; searchAfterSize: number; - logger: Logger; - eventsTelemetry: ITelemetryEventsSender | undefined; - buildRuleMessage: BuildRuleMessage; bulkCreate: BulkCreate; wrapHits: WrapHits; primaryTimestamp: string; @@ -82,18 +79,16 @@ export const queryExecutor = async ({ return searchAfterAndBulkCreate({ tuple, - listClient, - exceptionsList: exceptionItems, completeRule, services, - logger, + listClient, + exceptionsList: exceptionItems, + ruleExecutionLogger, eventsTelemetry, - id: completeRule.alertId, inputIndexPattern: inputIndex, - filter: esFilter, pageSize: searchAfterSize, + filter: esFilter, buildReasonMessage: buildReasonMessageForQueryAlert, - buildRuleMessage, bulkCreate, wrapHits, runtimeMappings, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threat_match.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threat_match.ts index a25561c52e271..2d3cd8e078242 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threat_match.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threat_match.ts @@ -5,7 +5,6 @@ * 2.0. */ -import type { Logger } from '@kbn/core/server'; import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; @@ -17,27 +16,24 @@ import type { import type { ListClient } from '@kbn/lists-plugin/server'; import type { RuleRangeTuple, BulkCreate, WrapHits } from '../types'; import type { ITelemetryEventsSender } from '../../../telemetry/sender'; -import type { BuildRuleMessage } from '../rule_messages'; import { createThreatSignals } from '../threat_mapping/create_threat_signals'; import type { CompleteRule, ThreatRuleParams } from '../../schemas/rule_schemas'; -import type { ExperimentalFeatures } from '../../../../../common/experimental_features'; import { withSecuritySpan } from '../../../../utils/with_security_span'; import { DEFAULT_INDICATOR_SOURCE_PATH } from '../../../../../common/constants'; +import type { IRuleExecutionLogForExecutors } from '../../rule_monitoring'; export const threatMatchExecutor = async ({ inputIndex, runtimeMappings, completeRule, tuple, - listClient, exceptionItems, + listClient, services, version, searchAfterSize, - logger, + ruleExecutionLogger, eventsTelemetry, - experimentalFeatures, - buildRuleMessage, bulkCreate, wrapHits, primaryTimestamp, @@ -47,15 +43,13 @@ export const threatMatchExecutor = async ({ runtimeMappings: estypes.MappingRuntimeFields | undefined; completeRule: CompleteRule; tuple: RuleRangeTuple; - listClient: ListClient; exceptionItems: ExceptionListItemSchema[]; + listClient: ListClient; services: RuleExecutorServices; version: string; searchAfterSize: number; - logger: Logger; + ruleExecutionLogger: IRuleExecutionLogForExecutors; eventsTelemetry: ITelemetryEventsSender | undefined; - experimentalFeatures: ExperimentalFeatures; - buildRuleMessage: BuildRuleMessage; bulkCreate: BulkCreate; wrapHits: WrapHits; primaryTimestamp: string; @@ -66,7 +60,6 @@ export const threatMatchExecutor = async ({ return withSecuritySpan('threatMatchExecutor', async () => { return createThreatSignals({ alertId: completeRule.alertId, - buildRuleMessage, bulkCreate, completeRule, concurrentSearches: ruleParams.concurrentSearches ?? 1, @@ -77,9 +70,9 @@ export const threatMatchExecutor = async ({ itemsPerSearch: ruleParams.itemsPerSearch ?? 9000, language: ruleParams.language, listClient, - logger, outputIndex: ruleParams.outputIndex, query: ruleParams.query, + ruleExecutionLogger, savedId: ruleParams.savedId, searchAfterSize, services, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.test.ts index a5516cb911193..dd51f7aaef25d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.test.ts @@ -6,7 +6,6 @@ */ import dateMath from '@kbn/datemath'; -import { loggingSystemMock } from '@kbn/core/server/mocks'; import { elasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-mocks'; import type { RuleExecutorServicesMock } from '@kbn/alerting-plugin/server/mocks'; import { alertsMock } from '@kbn/alerting-plugin/server/mocks'; @@ -14,32 +13,26 @@ import { thresholdExecutor } from './threshold'; import { getExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_item_schema.mock'; import { getEntryListMock } from '@kbn/lists-plugin/common/schemas/types/entry_list.mock'; import { getThresholdRuleParams, getCompleteRuleMock } from '../../schemas/rule_schemas.mock'; -import { buildRuleMessageFactory } from '../rule_messages'; import { sampleEmptyAggsSearchResults } from '../__mocks__/es_results'; -import { allowedExperimentalValues } from '../../../../../common/experimental_features'; +import { getThresholdTermsHash } from '../utils'; import type { ThresholdRuleParams } from '../../schemas/rule_schemas'; import { createRuleDataClientMock } from '@kbn/rule-registry-plugin/server/rule_data_client/rule_data_client.mock'; import { TIMESTAMP } from '@kbn/rule-data-utils'; +import type { IRuleExecutionLogForExecutors } from '../../rule_monitoring'; +import { ruleExecutionLogMock } from '../../rule_monitoring/mocks'; describe('threshold_executor', () => { - const version = '8.0.0'; - let logger: ReturnType; let alertServices: RuleExecutorServicesMock; - const params = getThresholdRuleParams(); + let ruleExecutionLogger: IRuleExecutionLogForExecutors; + const version = '8.0.0'; + const params = getThresholdRuleParams(); const thresholdCompleteRule = getCompleteRuleMock(params); - const tuple = { from: dateMath.parse(params.from)!, to: dateMath.parse(params.to)!, maxSignals: params.maxSignals, }; - const buildRuleMessage = buildRuleMessageFactory({ - id: thresholdCompleteRule.alertId, - ruleId: thresholdCompleteRule.ruleParams.ruleId, - name: thresholdCompleteRule.ruleConfig.name, - index: thresholdCompleteRule.ruleParams.outputIndex, - }); beforeEach(() => { alertServices = alertsMock.createRuleExecutorServices(); @@ -51,7 +44,12 @@ describe('threshold_executor', () => { }, }) ); - logger = loggingSystemMock.createLogger(); + ruleExecutionLogger = ruleExecutionLogMock.forExecutors.create({ + ruleId: thresholdCompleteRule.alertId, + ruleUuid: thresholdCompleteRule.ruleParams.ruleId, + ruleName: thresholdCompleteRule.ruleConfig.name, + ruleType: thresholdCompleteRule.ruleConfig.ruleTypeId, + }); }); describe('thresholdExecutor', () => { @@ -62,12 +60,10 @@ describe('threshold_executor', () => { completeRule: thresholdCompleteRule, tuple, exceptionItems, - experimentalFeatures: allowedExperimentalValues, services: alertServices, state: { initialized: true, signalHistory: {} }, version, - logger, - buildRuleMessage, + ruleExecutionLogger, startedAt: new Date(), bulkCreate: jest.fn().mockImplementation((hits) => ({ errors: [], @@ -84,5 +80,64 @@ describe('threshold_executor', () => { }); expect(response.warningMessages.length).toEqual(1); }); + + it('should clean up any signal history that has fallen outside the window when state is initialized', async () => { + const ruleDataClientMock = createRuleDataClientMock(); + const terms1 = [ + { + field: 'host.name', + value: 'elastic-pc-1', + }, + ]; + const signalHistoryRecord1 = { + terms: terms1, + lastSignalTimestamp: tuple.from.valueOf() - 60 * 1000, + }; + const terms2 = [ + { + field: 'host.name', + value: 'elastic-pc-2', + }, + ]; + const signalHistoryRecord2 = { + terms: terms2, + lastSignalTimestamp: tuple.from.valueOf() + 60 * 1000, + }; + const state = { + initialized: true, + signalHistory: { + [`${getThresholdTermsHash(terms1)}`]: signalHistoryRecord1, + [`${getThresholdTermsHash(terms2)}`]: signalHistoryRecord2, + }, + }; + const response = await thresholdExecutor({ + completeRule: thresholdCompleteRule, + tuple, + exceptionItems: [], + services: alertServices, + state, + version, + ruleExecutionLogger, + startedAt: new Date(), + bulkCreate: jest.fn().mockImplementation((hits) => ({ + errors: [], + success: true, + bulkCreateDuration: '0', + createdItemsCount: 0, + createdItems: [], + })), + wrapHits: jest.fn(), + ruleDataReader: ruleDataClientMock.getReader({ namespace: 'default' }), + runtimeMappings: {}, + inputIndex: ['auditbeat-*'], + primaryTimestamp: TIMESTAMP, + }); + expect(response.state).toEqual({ + initialized: true, + signalHistory: { + [`${getThresholdTermsHash(terms2)}`]: signalHistoryRecord2, + }, + }); + }); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.ts index 2cce95a5a9160..b5bf0cdc337a3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.ts @@ -9,8 +9,6 @@ import type { SearchHit } from '@elastic/elasticsearch/lib/api/typesWithBodyKey' import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; -import type { Logger } from '@kbn/core/server'; - import type { AlertInstanceContext, AlertInstanceState, @@ -34,10 +32,9 @@ import type { WrapHits, } from '../types'; import { createSearchAfterReturnType } from '../utils'; -import type { BuildRuleMessage } from '../rule_messages'; -import type { ExperimentalFeatures } from '../../../../../common/experimental_features'; import { withSecuritySpan } from '../../../../utils/with_security_span'; import { buildThresholdSignalHistory } from '../threshold/build_signal_history'; +import type { IRuleExecutionLogForExecutors } from '../../rule_monitoring'; export const thresholdExecutor = async ({ inputIndex, @@ -45,11 +42,9 @@ export const thresholdExecutor = async ({ completeRule, tuple, exceptionItems, - experimentalFeatures, + ruleExecutionLogger, services, version, - logger, - buildRuleMessage, startedAt, state, bulkCreate, @@ -63,11 +58,9 @@ export const thresholdExecutor = async ({ completeRule: CompleteRule; tuple: RuleRangeTuple; exceptionItems: ExceptionListItemSchema[]; - experimentalFeatures: ExperimentalFeatures; services: RuleExecutorServices; + ruleExecutionLogger: IRuleExecutionLogForExecutors; version: string; - logger: Logger; - buildRuleMessage: BuildRuleMessage; startedAt: Date; state: ThresholdAlertState; bulkCreate: BulkCreate; @@ -136,10 +129,9 @@ export const thresholdExecutor = async ({ to: tuple.to.toISOString(), maxSignals: tuple.maxSignals, services, - logger, + ruleExecutionLogger, filter: esFilter, threshold: ruleParams.threshold, - buildRuleMessage, runtimeMappings, primaryTimestamp, secondaryTimestamp, @@ -152,7 +144,6 @@ export const thresholdExecutor = async ({ completeRule, filter: esFilter, services, - logger, inputIndexPattern: inputIndex, signalsIndex: ruleParams.outputIndex, startedAt, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_field_and_set_tuples.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_field_and_set_tuples.test.ts index 47725bbeefcc2..d6b452216be92 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_field_and_set_tuples.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_field_and_set_tuples.test.ts @@ -6,18 +6,19 @@ */ import { createFieldAndSetTuples } from './create_field_and_set_tuples'; -import { mockLogger, sampleDocWithSortId } from '../__mocks__/es_results'; +import { sampleDocWithSortId } from '../__mocks__/es_results'; import { getExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_item_schema.mock'; import { listMock } from '@kbn/lists-plugin/server/mocks'; import { getSearchListItemResponseMock } from '@kbn/lists-plugin/common/schemas/response/search_list_item_schema.mock'; import type { EntryList } from '@kbn/securitysolution-io-ts-list-types'; -import { buildRuleMessageMock as buildRuleMessage } from '../rule_messages.mock'; +import { ruleExecutionLogMock } from '../../rule_monitoring/mocks'; describe('filterEventsAgainstList', () => { let listClient = listMock.getListClient(); let exceptionItem = getExceptionListItemSchemaMock(); let events = [sampleDocWithSortId('123', undefined, '1.1.1.1')]; + const ruleExecutionLogger = ruleExecutionLogMock.forExecutors.create(); beforeEach(() => { jest.clearAllMocks(); @@ -55,10 +56,9 @@ describe('filterEventsAgainstList', () => { exceptionItem.entries = []; const field = await createFieldAndSetTuples({ listClient, - logger: mockLogger, + ruleExecutionLogger, events, exceptionItem, - buildRuleMessage, }); expect(field).toEqual([]); }); @@ -66,10 +66,9 @@ describe('filterEventsAgainstList', () => { test('it returns a single field and set tuple if entries has a single item', async () => { const field = await createFieldAndSetTuples({ listClient, - logger: mockLogger, + ruleExecutionLogger, events, exceptionItem, - buildRuleMessage, }); expect(field.length).toEqual(1); }); @@ -78,10 +77,9 @@ describe('filterEventsAgainstList', () => { (exceptionItem.entries[0] as EntryList).operator = 'included'; const [{ operator }] = await createFieldAndSetTuples({ listClient, - logger: mockLogger, + ruleExecutionLogger, events, exceptionItem, - buildRuleMessage, }); expect(operator).toEqual('included'); }); @@ -90,10 +88,9 @@ describe('filterEventsAgainstList', () => { (exceptionItem.entries[0] as EntryList).operator = 'excluded'; const [{ operator }] = await createFieldAndSetTuples({ listClient, - logger: mockLogger, + ruleExecutionLogger, events, exceptionItem, - buildRuleMessage, }); expect(operator).toEqual('excluded'); }); @@ -102,10 +99,9 @@ describe('filterEventsAgainstList', () => { (exceptionItem.entries[0] as EntryList).field = 'source.ip'; const [{ field }] = await createFieldAndSetTuples({ listClient, - logger: mockLogger, + ruleExecutionLogger, events, exceptionItem, - buildRuleMessage, }); expect(field).toEqual('source.ip'); }); @@ -115,10 +111,9 @@ describe('filterEventsAgainstList', () => { (exceptionItem.entries[0] as EntryList).field = 'source.ip'; const [{ matchedSet }] = await createFieldAndSetTuples({ listClient, - logger: mockLogger, + ruleExecutionLogger, events, exceptionItem, - buildRuleMessage, }); expect([...matchedSet]).toEqual([JSON.stringify(['1.1.1.1'])]); }); @@ -131,10 +126,9 @@ describe('filterEventsAgainstList', () => { (exceptionItem.entries[0] as EntryList).field = 'source.ip'; const [{ matchedSet }] = await createFieldAndSetTuples({ listClient, - logger: mockLogger, + ruleExecutionLogger, events, exceptionItem, - buildRuleMessage, }); expect([...matchedSet]).toEqual([JSON.stringify(['1.1.1.1']), JSON.stringify(['2.2.2.2'])]); }); @@ -144,10 +138,9 @@ describe('filterEventsAgainstList', () => { (exceptionItem.entries[0] as EntryList).field = 'source.ip'; const [{ matchedSet }] = await createFieldAndSetTuples({ listClient, - logger: mockLogger, + ruleExecutionLogger, events, exceptionItem, - buildRuleMessage, }); expect([...matchedSet]).toEqual([JSON.stringify(['1.1.1.1', '2.2.2.2'])]); }); @@ -179,10 +172,9 @@ describe('filterEventsAgainstList', () => { ]; const fields = await createFieldAndSetTuples({ listClient, - logger: mockLogger, + ruleExecutionLogger, events, exceptionItem, - buildRuleMessage, }); expect(fields.length).toEqual(2); }); @@ -214,10 +206,9 @@ describe('filterEventsAgainstList', () => { ]; const [{ operator: operator1 }, { operator: operator2 }] = await createFieldAndSetTuples({ listClient, - logger: mockLogger, + ruleExecutionLogger, events, exceptionItem, - buildRuleMessage, }); expect(operator1).toEqual('included'); expect(operator2).toEqual('excluded'); @@ -250,10 +241,9 @@ describe('filterEventsAgainstList', () => { ]; const [{ field: field1 }, { field: field2 }] = await createFieldAndSetTuples({ listClient, - logger: mockLogger, + ruleExecutionLogger, events, exceptionItem, - buildRuleMessage, }); expect(field1).toEqual('source.ip'); expect(field2).toEqual('destination.ip'); @@ -287,10 +277,9 @@ describe('filterEventsAgainstList', () => { const [{ matchedSet: matchedSet1 }, { matchedSet: matchedSet2 }] = await createFieldAndSetTuples({ listClient, - logger: mockLogger, + ruleExecutionLogger, events, exceptionItem, - buildRuleMessage, }); expect([...matchedSet1]).toEqual([JSON.stringify(['1.1.1.1']), JSON.stringify(['2.2.2.2'])]); expect([...matchedSet2]).toEqual([JSON.stringify(['3.3.3.3']), JSON.stringify(['5.5.5.5'])]); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_field_and_set_tuples.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_field_and_set_tuples.ts index 0d58a4f7be078..6c04d2985f8d6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_field_and_set_tuples.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_field_and_set_tuples.ts @@ -14,8 +14,7 @@ export const createFieldAndSetTuples = async ({ events, exceptionItem, listClient, - logger, - buildRuleMessage, + ruleExecutionLogger, }: CreateFieldAndSetTuplesOptions): Promise => { const typedEntries = exceptionItem.entries.filter((entry): entry is EntryList => entriesList.is(entry) @@ -30,8 +29,7 @@ export const createFieldAndSetTuples = async ({ listId: id, listType: type, listClient, - logger, - buildRuleMessage, + ruleExecutionLogger, }); return { field, operator, matchedSet }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_set_to_filter_against.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_set_to_filter_against.test.ts index c6061d37f4279..d28bc2a39418d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_set_to_filter_against.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_set_to_filter_against.test.ts @@ -5,16 +5,17 @@ * 2.0. */ -import { mockLogger, sampleDocWithSortId } from '../__mocks__/es_results'; +import { sampleDocWithSortId } from '../__mocks__/es_results'; import { listMock } from '@kbn/lists-plugin/server/mocks'; import { getSearchListItemResponseMock } from '@kbn/lists-plugin/common/schemas/response/search_list_item_schema.mock'; import { createSetToFilterAgainst } from './create_set_to_filter_against'; -import { buildRuleMessageMock as buildRuleMessage } from '../rule_messages.mock'; +import { ruleExecutionLogMock } from '../../rule_monitoring/mocks'; describe('createSetToFilterAgainst', () => { let listClient = listMock.getListClient(); let events = [sampleDocWithSortId('123', undefined, '1.1.1.1')]; + const ruleExecutionLogger = ruleExecutionLogMock.forExecutors.create(); beforeEach(() => { jest.clearAllMocks(); @@ -42,8 +43,7 @@ describe('createSetToFilterAgainst', () => { listId: 'list-123', listType: 'ip', listClient, - logger: mockLogger, - buildRuleMessage, + ruleExecutionLogger, }); expect([...field]).toEqual([]); }); @@ -56,8 +56,7 @@ describe('createSetToFilterAgainst', () => { listId: 'list-123', listType: 'ip', listClient, - logger: mockLogger, - buildRuleMessage, + ruleExecutionLogger, }); expect(listClient.searchListItemByValues).toHaveBeenCalledWith({ listId: 'list-123', @@ -78,8 +77,7 @@ describe('createSetToFilterAgainst', () => { listId: 'list-123', listType: 'ip', listClient, - logger: mockLogger, - buildRuleMessage, + ruleExecutionLogger, }); expect(listClient.searchListItemByValues).toHaveBeenCalledWith({ listId: 'list-123', @@ -100,8 +98,7 @@ describe('createSetToFilterAgainst', () => { listId: 'list-123', listType: 'ip', listClient, - logger: mockLogger, - buildRuleMessage, + ruleExecutionLogger, }); expect(listClient.searchListItemByValues).toHaveBeenCalledWith({ listId: 'list-123', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_set_to_filter_against.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_set_to_filter_against.ts index 97369f269a7a0..e419d13589a57 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_set_to_filter_against.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_set_to_filter_against.ts @@ -26,8 +26,7 @@ export const createSetToFilterAgainst = async ({ listId, listType, listClient, - logger, - buildRuleMessage, + ruleExecutionLogger, }: CreateSetToFilterAgainstOptions): Promise> => { const valuesFromSearchResultField = events.reduce((acc, searchResultItem) => { const valueField = searchResultItem.fields ? searchResultItem.fields[field] : undefined; @@ -37,10 +36,8 @@ export const createSetToFilterAgainst = async ({ return acc; }, new Set()); - logger.debug( - buildRuleMessage( - `number of distinct values from ${field}: ${[...valuesFromSearchResultField].length}` - ) + ruleExecutionLogger.debug( + `number of distinct values from ${field}: ${[...valuesFromSearchResultField].length}` ); const matchedListItems = await listClient.searchListItemByValues({ @@ -49,10 +46,8 @@ export const createSetToFilterAgainst = async ({ value: [...valuesFromSearchResultField], }); - logger.debug( - buildRuleMessage( - `number of matched items from list with id ${listId}: ${matchedListItems.length}` - ) + ruleExecutionLogger.debug( + `number of matched items from list with id ${listId}: ${matchedListItems.length}` ); return new Set( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events_against_list.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events_against_list.test.ts index 22dc5136fcded..b702b3ac63acc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events_against_list.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events_against_list.test.ts @@ -6,18 +6,20 @@ */ import uuid from 'uuid'; -import { filterEventsAgainstList } from './filter_events_against_list'; -import { buildRuleMessageMock as buildRuleMessage } from '../rule_messages.mock'; -import { mockLogger, repeatedHitsWithSortId } from '../__mocks__/es_results'; import { getExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_item_schema.mock'; -import { listMock } from '@kbn/lists-plugin/server/mocks'; import { getSearchListItemResponseMock } from '@kbn/lists-plugin/common/schemas/response/search_list_item_schema.mock'; +import { listMock } from '@kbn/lists-plugin/server/mocks'; + +import { filterEventsAgainstList } from './filter_events_against_list'; +import { repeatedHitsWithSortId } from '../__mocks__/es_results'; +import { ruleExecutionLogMock } from '../../rule_monitoring/mocks'; const someGuids = Array.from({ length: 13 }).map((x) => uuid.v4()); describe('filterEventsAgainstList', () => { let listClient = listMock.getListClient(); + const ruleExecutionLogger = ruleExecutionLogMock.forExecutors.create(); beforeEach(() => { jest.clearAllMocks(); @@ -31,7 +33,7 @@ describe('filterEventsAgainstList', () => { it('should respond with eventSearchResult if exceptionList is empty array', async () => { const [included, excluded] = await filterEventsAgainstList({ - logger: mockLogger, + ruleExecutionLogger, listClient, exceptionsList: [], events: repeatedHitsWithSortId(4, someGuids.slice(0, 3), [ @@ -40,7 +42,6 @@ describe('filterEventsAgainstList', () => { '3.3.3.3', '7.7.7.7', ]), - buildRuleMessage, }); expect(included.length).toEqual(4); expect(excluded.length).toEqual(0); @@ -48,7 +49,7 @@ describe('filterEventsAgainstList', () => { it('should respond with eventSearchResult if exceptionList does not contain value list exceptions', async () => { const [included, excluded] = await filterEventsAgainstList({ - logger: mockLogger, + ruleExecutionLogger, listClient, exceptionsList: [getExceptionListItemSchemaMock()], events: repeatedHitsWithSortId(4, someGuids.slice(0, 3), [ @@ -57,11 +58,10 @@ describe('filterEventsAgainstList', () => { '3.3.3.3', '7.7.7.7', ]), - buildRuleMessage, }); expect(included.length).toEqual(4); expect(excluded.length).toEqual(0); - expect((mockLogger.debug as unknown as jest.Mock).mock.calls[0][0]).toContain( + expect(ruleExecutionLogger.debug.mock.calls[0][0]).toContain( 'no exception items of type list found - returning original search result' ); }); @@ -82,11 +82,10 @@ describe('filterEventsAgainstList', () => { ]; const [included, excluded] = await filterEventsAgainstList({ - logger: mockLogger, + ruleExecutionLogger, listClient, exceptionsList: [exceptionItem], events: repeatedHitsWithSortId(4, someGuids.slice(0, 3)), - buildRuleMessage, }); expect(included.length).toEqual(4); expect(excluded.length).toEqual(0); @@ -114,7 +113,7 @@ describe('filterEventsAgainstList', () => { ) ); const [included, excluded] = await filterEventsAgainstList({ - logger: mockLogger, + ruleExecutionLogger, listClient, exceptionsList: [exceptionItem], events: repeatedHitsWithSortId(4, someGuids.slice(0, 3), [ @@ -123,7 +122,6 @@ describe('filterEventsAgainstList', () => { '3.3.3.3', '7.7.7.7', ]), - buildRuleMessage, }); expect((listClient.searchListItemByValues as jest.Mock).mock.calls[0][0].type).toEqual('ip'); expect((listClient.searchListItemByValues as jest.Mock).mock.calls[0][0].listId).toEqual( @@ -175,7 +173,7 @@ describe('filterEventsAgainstList', () => { ]); const [included, excluded] = await filterEventsAgainstList({ - logger: mockLogger, + ruleExecutionLogger, listClient, exceptionsList: [exceptionItem, exceptionItemAgain], events: repeatedHitsWithSortId(9, someGuids.slice(0, 9), [ @@ -189,7 +187,6 @@ describe('filterEventsAgainstList', () => { '8.8.8.8', '9.9.9.9', ]), - buildRuleMessage, }); expect(listClient.searchListItemByValues as jest.Mock).toHaveBeenCalledTimes(2); expect(included.length).toEqual(6); @@ -237,7 +234,7 @@ describe('filterEventsAgainstList', () => { ]); const [included, excluded] = await filterEventsAgainstList({ - logger: mockLogger, + ruleExecutionLogger, listClient, exceptionsList: [exceptionItem, exceptionItemAgain], events: repeatedHitsWithSortId(9, someGuids.slice(0, 9), [ @@ -251,7 +248,6 @@ describe('filterEventsAgainstList', () => { '8.8.8.8', '9.9.9.9', ]), - buildRuleMessage, }); expect(listClient.searchListItemByValues as jest.Mock).toHaveBeenCalledTimes(2); // @ts-expect-error @@ -297,7 +293,7 @@ describe('filterEventsAgainstList', () => { ]); const [included, excluded] = await filterEventsAgainstList({ - logger: mockLogger, + ruleExecutionLogger, listClient, exceptionsList: [exceptionItem], events: repeatedHitsWithSortId( @@ -326,7 +322,6 @@ describe('filterEventsAgainstList', () => { '2.2.2.2', ] ), - buildRuleMessage, }); expect(listClient.searchListItemByValues as jest.Mock).toHaveBeenCalledTimes(2); expect(included.length).toEqual(8); @@ -375,7 +370,7 @@ describe('filterEventsAgainstList', () => { ]); const [included, excluded] = await filterEventsAgainstList({ - logger: mockLogger, + ruleExecutionLogger, listClient, exceptionsList: [exceptionItem], events: repeatedHitsWithSortId(9, someGuids.slice(0, 9), [ @@ -389,7 +384,6 @@ describe('filterEventsAgainstList', () => { '8.8.8.8', '9.9.9.9', ]), - buildRuleMessage, }); expect(listClient.searchListItemByValues as jest.Mock).toHaveBeenCalledTimes(2); expect(included.length).toEqual(9); @@ -443,7 +437,7 @@ describe('filterEventsAgainstList', () => { ]); const [included, excluded] = await filterEventsAgainstList({ - logger: mockLogger, + ruleExecutionLogger, listClient, exceptionsList: [exceptionItem], events: repeatedHitsWithSortId( @@ -460,7 +454,6 @@ describe('filterEventsAgainstList', () => { ['3.3.3.3', '4.4.4.4'], ] ), - buildRuleMessage, }); expect(listClient.searchListItemByValues as jest.Mock).toHaveBeenCalledTimes(2); expect((listClient.searchListItemByValues as jest.Mock).mock.calls[0][0].value).toEqual([ @@ -505,11 +498,10 @@ describe('filterEventsAgainstList', () => { }, ]; const [included, excluded] = await filterEventsAgainstList({ - logger: mockLogger, + ruleExecutionLogger, listClient, exceptionsList: [exceptionItem], events: repeatedHitsWithSortId(4, someGuids.slice(0, 3)), - buildRuleMessage, }); expect(included.length).toEqual(0); expect(excluded.length).toEqual(4); @@ -537,7 +529,7 @@ describe('filterEventsAgainstList', () => { ) ); const [included, excluded] = await filterEventsAgainstList({ - logger: mockLogger, + ruleExecutionLogger, listClient, exceptionsList: [exceptionItem], events: repeatedHitsWithSortId(4, someGuids.slice(0, 3), [ @@ -546,7 +538,6 @@ describe('filterEventsAgainstList', () => { '3.3.3.3', '7.7.7.7', ]), - buildRuleMessage, }); expect((listClient.searchListItemByValues as jest.Mock).mock.calls[0][0].type).toEqual('ip'); expect((listClient.searchListItemByValues as jest.Mock).mock.calls[0][0].listId).toEqual( @@ -592,7 +583,7 @@ describe('filterEventsAgainstList', () => { ]); const [included, excluded] = await filterEventsAgainstList({ - logger: mockLogger, + ruleExecutionLogger, listClient, exceptionsList: [exceptionItem], events: repeatedHitsWithSortId( @@ -609,7 +600,6 @@ describe('filterEventsAgainstList', () => { ['3.3.3.3', '4.4.4.4'], ] ), - buildRuleMessage, }); expect(listClient.searchListItemByValues as jest.Mock).toHaveBeenCalledTimes(2); expect((listClient.searchListItemByValues as jest.Mock).mock.calls[0][0].value).toEqual([ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events_against_list.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events_against_list.ts index 91fa599e9e207..1093742d76d6e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events_against_list.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events_against_list.ts @@ -32,15 +32,14 @@ import { createFieldAndSetTuples } from './create_field_and_set_tuples'; * * @param listClient The list client to use for queries * @param exceptionsList The exception list - * @param logger Logger for messages - * @param eventSearchResult The current events from the search + * @param ruleExecutionLogger Logger for messages + * @param events The current events from the search */ export const filterEventsAgainstList = async ({ listClient, exceptionsList, - logger, + ruleExecutionLogger, events, - buildRuleMessage, }: FilterEventsAgainstListOptions): Promise> => { try { const atLeastOneLargeValueList = exceptionsList.some(({ entries }) => @@ -48,8 +47,8 @@ export const filterEventsAgainstList = async ({ ); if (!atLeastOneLargeValueList) { - logger.debug( - buildRuleMessage('no exception items of type list found - returning original search result') + ruleExecutionLogger.debug( + 'no exception items of type list found - returning original search result' ); return [events, []]; } @@ -70,17 +69,14 @@ export const filterEventsAgainstList = async ({ events: includedEvents, exceptionItem, listClient, - logger, - buildRuleMessage, + ruleExecutionLogger, }); const [nextIncludedEvents, nextExcludedEvents] = partitionEvents({ events: includedEvents, fieldAndSetTuples, }); - logger.debug( - buildRuleMessage( - `Exception with id ${exceptionItem.id} filtered out ${nextExcludedEvents.length} events` - ) + ruleExecutionLogger.debug( + `Exception with id ${exceptionItem.id} filtered out ${nextExcludedEvents.length} events` ); return [nextIncludedEvents, [...excludedEvents, ...nextExcludedEvents]]; }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/types.ts index 114dd5ee43f1c..f4f8aaf91f969 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/types.ts @@ -5,18 +5,15 @@ * 2.0. */ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import type { Logger } from '@kbn/core/server'; - import type { Type, ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; import type { ListClient } from '@kbn/lists-plugin/server'; -import type { BuildRuleMessage } from '../rule_messages'; +import type { IRuleExecutionLogForExecutors } from '../../rule_monitoring'; export interface FilterEventsAgainstListOptions { listClient: ListClient; exceptionsList: ExceptionListItemSchema[]; - logger: Logger; + ruleExecutionLogger: IRuleExecutionLogForExecutors; events: Array>; - buildRuleMessage: BuildRuleMessage; } export type FilterEventsAgainstListReturn = [ @@ -30,8 +27,7 @@ export interface CreateSetToFilterAgainstOptions { listId: string; listType: Type; listClient: ListClient; - logger: Logger; - buildRuleMessage: BuildRuleMessage; + ruleExecutionLogger: IRuleExecutionLogForExecutors; } export interface FilterEventsOptions { @@ -43,8 +39,7 @@ export interface CreateFieldAndSetTuplesOptions { events: Array>; exceptionItem: ExceptionListItemSchema; listClient: ListClient; - logger: Logger; - buildRuleMessage: BuildRuleMessage; + ruleExecutionLogger: IRuleExecutionLogForExecutors; } export interface FieldSet { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_input_output_index.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_input_output_index.test.ts index 60f98a32dad87..c898dff30c867 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_input_output_index.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_input_output_index.test.ts @@ -12,7 +12,7 @@ import { loggerMock } from '@kbn/logging-mocks'; import { DEFAULT_INDEX_KEY, DEFAULT_INDEX_PATTERN } from '../../../../common/constants'; import type { GetInputIndex } from './get_input_output_index'; -import { getInputIndex } from './get_input_output_index'; +import { getInputIndex, DataViewError } from './get_input_output_index'; describe('get_input_output_index', () => { let servicesMock: RuleExecutorServicesMock; @@ -196,5 +196,21 @@ describe('get_input_output_index', () => { `"Saved object [index-pattern/12345] not found"` ); }); + + test('Returns error of DataViewErrorType', async () => { + servicesMock.savedObjectsClient.get.mockRejectedValue( + new Error('Saved object [index-pattern/12345] not found') + ); + await expect( + getInputIndex({ + services: servicesMock, + version: '8.0.0', + index: [], + dataViewId: '12345', + ruleId: 'rule_1', + logger, + }) + ).rejects.toBeInstanceOf(DataViewError); + }); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_input_output_index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_input_output_index.ts index 796c9ff2598a8..b75c351c84f5f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_input_output_index.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_input_output_index.ts @@ -34,6 +34,8 @@ export interface GetInputIndexReturn { warningToWrite?: string; } +export class DataViewError extends Error {} + export const getInputIndex = async ({ index, services, @@ -45,10 +47,15 @@ export const getInputIndex = async ({ // If data views defined, use it if (dataViewId != null && dataViewId !== '') { // Check to see that the selected dataView exists - const dataView = await services.savedObjectsClient.get( - 'index-pattern', - dataViewId - ); + let dataView; + try { + dataView = await services.savedObjectsClient.get( + 'index-pattern', + dataViewId + ); + } catch (exc) { + throw new DataViewError(exc.message); + } const indices = dataView.attributes.title.split(','); const runtimeMappings = dataView.attributes.runtimeFieldMap != null diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/preview/preview_rule_execution_logger.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/preview/preview_rule_execution_logger.ts index d1d5209328344..978a43a29a878 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/preview/preview_rule_execution_logger.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/preview/preview_rule_execution_logger.ts @@ -6,29 +6,36 @@ */ import type { - RuleExecutionLogForExecutorsFactory, + IRuleExecutionLogService, RuleExecutionContext, StatusChangeArgs, -} from '../../rule_execution_log'; +} from '../../rule_monitoring'; + +export interface IPreviewRuleExecutionLogger { + factory: IRuleExecutionLogService['createClientForExecutors']; +} export const createPreviewRuleExecutionLogger = ( loggedStatusChanges: Array -) => { - const factory: RuleExecutionLogForExecutorsFactory = ( - savedObjectsClient, - eventLogService, - logger, - context - ) => { - return { - context, +): IPreviewRuleExecutionLogger => { + return { + factory: ({ context }) => { + const spyLogger = { + context, - logStatusChange(args: StatusChangeArgs): Promise { - loggedStatusChanges.push({ ...context, ...args }); - return Promise.resolve(); - }, - }; - }; + trace: () => {}, + debug: () => {}, + info: () => {}, + warn: () => {}, + error: () => {}, - return { factory }; + logStatusChange: (args: StatusChangeArgs): Promise => { + loggedStatusChanges.push({ ...context, ...args }); + return Promise.resolve(); + }, + }; + + return Promise.resolve(spyLogger); + }, + }; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_messages.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_messages.test.ts deleted file mode 100644 index 35cefcaad8189..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_messages.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { BuildRuleMessageFactoryParams } from './rule_messages'; -import { buildRuleMessageFactory } from './rule_messages'; - -describe('buildRuleMessageFactory', () => { - let factoryParams: BuildRuleMessageFactoryParams; - beforeEach(() => { - factoryParams = { - name: 'name', - id: 'id', - ruleId: 'ruleId', - index: 'index', - }; - }); - - it('appends rule attributes to the provided message', () => { - const buildMessage = buildRuleMessageFactory(factoryParams); - - const message = buildMessage('my message'); - expect(message).toEqual(expect.stringContaining('my message')); - expect(message).toEqual(expect.stringContaining('name: "name"')); - expect(message).toEqual(expect.stringContaining('id: "id"')); - expect(message).toEqual(expect.stringContaining('rule id: "ruleId"')); - expect(message).toEqual(expect.stringContaining('signals index: "index"')); - }); - - it('joins message parts with spaces', () => { - const buildMessage = buildRuleMessageFactory(factoryParams); - - const message = buildMessage('my message'); - expect(message).toEqual(expect.stringContaining('my message ')); - expect(message).toEqual(expect.stringContaining(' name: "name" ')); - expect(message).toEqual(expect.stringContaining(' id: "id" ')); - expect(message).toEqual(expect.stringContaining(' rule id: "ruleId" ')); - expect(message).toEqual(expect.stringContaining(' signals index: "index"')); - }); - - it('joins multiple arguments with spaces', () => { - const buildMessage = buildRuleMessageFactory(factoryParams); - - const message = buildMessage('my message', 'here is more'); - expect(message).toEqual(expect.stringContaining('my message ')); - expect(message).toEqual(expect.stringContaining(' here is more')); - }); - - it('defaults the rule ID if not provided ', () => { - const buildMessage = buildRuleMessageFactory({ - ...factoryParams, - ruleId: undefined, - }); - - const message = buildMessage('my message', 'here is more'); - expect(message).toEqual(expect.stringContaining('rule id: "(unknown rule id)"')); - }); -}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_messages.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_messages.ts deleted file mode 100644 index 5f30220e71402..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_messages.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export type BuildRuleMessage = (...messages: string[]) => string; -export interface BuildRuleMessageFactoryParams { - name: string; - id: string; - ruleId: string | null | undefined; - index: string; -} - -export const buildRuleMessageFactory = - ({ id, ruleId, index, name }: BuildRuleMessageFactoryParams): BuildRuleMessage => - (...messages) => - [ - ...messages, - `name: "${name}"`, - `id: "${id}"`, - `rule id: "${ruleId ?? '(unknown rule id)'}"`, - `signals index: "${index}"`, - ].join(' '); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts index 51e98d7575444..9561d19fe4378 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts @@ -7,8 +7,6 @@ import { sampleEmptyDocSearchResults, - sampleRuleGuid, - mockLogger, repeatedSearchResultsWithSortId, repeatedSearchResultsWithNoSortId, sampleDocSearchResultsNoSortIdNoHits, @@ -28,7 +26,7 @@ import { elasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-m import { getCompleteRuleMock, getQueryRuleParams } from '../schemas/rule_schemas.mock'; import { bulkCreateFactory } from '../rule_types/factories/bulk_create_factory'; import { wrapHitsFactory } from '../rule_types/factories/wrap_hits_factory'; -import { mockBuildRuleMessage } from './__mocks__/build_rule_message.mock'; +import { ruleExecutionLogMock } from '../rule_monitoring/mocks'; import type { BuildReasonMessage } from './reason_formatters'; import type { QueryRuleParams } from '../schemas/rule_schemas'; import { createPersistenceServicesMock } from '@kbn/rule-registry-plugin/server/utils/create_persistence_rule_type_wrapper.mock'; @@ -48,8 +46,6 @@ import { import { SERVER_APP_ID } from '../../../../common/constants'; import type { CommonAlertFieldsLatest } from '@kbn/rule-registry-plugin/common/schemas'; -const buildRuleMessage = mockBuildRuleMessage; - describe('searchAfterAndBulkCreate', () => { let mockService: RuleExecutorServicesMock; let mockPersistenceServices: jest.Mocked; @@ -58,6 +54,7 @@ describe('searchAfterAndBulkCreate', () => { let wrapHits: WrapHits; let inputIndexPattern: string[] = []; let listClient = listMock.getListClient(); + const ruleExecutionLogger = ruleExecutionLogMock.forExecutors.create(); const someGuids = Array.from({ length: 13 }).map(() => uuid.v4()); const sampleParams = getQueryRuleParams(); const queryCompleteRule = getCompleteRuleMock(sampleParams); @@ -78,6 +75,7 @@ describe('searchAfterAndBulkCreate', () => { }; sampleParams.maxSignals = 30; let tuple: RuleRangeTuple; + beforeEach(() => { jest.clearAllMocks(); buildReasonMessage = jest.fn().mockResolvedValue('some alert reason message'); @@ -86,21 +84,19 @@ describe('searchAfterAndBulkCreate', () => { inputIndexPattern = ['auditbeat-*']; mockService = alertsMock.createRuleExecutorServices(); tuple = getRuleRangeTuples({ - logger: mockLogger, previousStartedAt: new Date(), startedAt: new Date(), from: sampleParams.from, to: sampleParams.to, interval: '5m', maxSignals: sampleParams.maxSignals, - buildRuleMessage, + ruleExecutionLogger, }).tuples[0]; mockPersistenceServices = createPersistenceServicesMock(); bulkCreate = bulkCreateFactory( - mockLogger, mockPersistenceServices.alertWithPersistence, - buildRuleMessage, - false + false, + ruleExecutionLogger ); wrapHits = wrapHitsFactory({ completeRule: queryCompleteRule, @@ -209,14 +205,12 @@ describe('searchAfterAndBulkCreate', () => { listClient, exceptionsList: [exceptionItem], services: mockService, - logger: mockLogger, + ruleExecutionLogger, eventsTelemetry: undefined, - id: sampleRuleGuid, inputIndexPattern, pageSize: 1, filter: defaultFilter, buildReasonMessage, - buildRuleMessage, bulkCreate, wrapHits, runtimeMappings: undefined, @@ -306,14 +300,12 @@ describe('searchAfterAndBulkCreate', () => { listClient, exceptionsList: [exceptionItem], services: mockService, - logger: mockLogger, + ruleExecutionLogger, eventsTelemetry: undefined, - id: sampleRuleGuid, inputIndexPattern, pageSize: 1, filter: defaultFilter, buildReasonMessage, - buildRuleMessage, bulkCreate, wrapHits, runtimeMappings: undefined, @@ -383,14 +375,12 @@ describe('searchAfterAndBulkCreate', () => { listClient, exceptionsList: [exceptionItem], services: mockService, - logger: mockLogger, + ruleExecutionLogger, eventsTelemetry: undefined, - id: sampleRuleGuid, inputIndexPattern, pageSize: 1, filter: defaultFilter, buildReasonMessage, - buildRuleMessage, bulkCreate, wrapHits, runtimeMappings: undefined, @@ -444,14 +434,12 @@ describe('searchAfterAndBulkCreate', () => { listClient, exceptionsList: [exceptionItem], services: mockService, - logger: mockLogger, + ruleExecutionLogger, eventsTelemetry: undefined, - id: sampleRuleGuid, inputIndexPattern, pageSize: 1, filter: defaultFilter, buildReasonMessage, - buildRuleMessage, bulkCreate, wrapHits, runtimeMappings: undefined, @@ -515,14 +503,12 @@ describe('searchAfterAndBulkCreate', () => { listClient, exceptionsList: [], services: mockService, - logger: mockLogger, + ruleExecutionLogger, eventsTelemetry: undefined, - id: sampleRuleGuid, inputIndexPattern, pageSize: 1, filter: defaultFilter, buildReasonMessage, - buildRuleMessage, bulkCreate, wrapHits, runtimeMappings: undefined, @@ -572,14 +558,12 @@ describe('searchAfterAndBulkCreate', () => { listClient, exceptionsList: [exceptionItem], services: mockService, - logger: mockLogger, + ruleExecutionLogger, eventsTelemetry: undefined, - id: sampleRuleGuid, inputIndexPattern, pageSize: 1, filter: defaultFilter, buildReasonMessage, - buildRuleMessage, bulkCreate, wrapHits, runtimeMappings: undefined, @@ -643,14 +627,12 @@ describe('searchAfterAndBulkCreate', () => { listClient, exceptionsList: [exceptionItem], services: mockService, - logger: mockLogger, + ruleExecutionLogger, eventsTelemetry: undefined, - id: sampleRuleGuid, inputIndexPattern, pageSize: 1, filter: defaultFilter, buildReasonMessage, - buildRuleMessage, bulkCreate, wrapHits, runtimeMappings: undefined, @@ -716,14 +698,12 @@ describe('searchAfterAndBulkCreate', () => { listClient, exceptionsList: [], services: mockService, - logger: mockLogger, + ruleExecutionLogger, eventsTelemetry: undefined, - id: sampleRuleGuid, inputIndexPattern, pageSize: 1, filter: defaultFilter, buildReasonMessage, - buildRuleMessage, bulkCreate, wrapHits, runtimeMappings: undefined, @@ -765,14 +745,12 @@ describe('searchAfterAndBulkCreate', () => { tuple, completeRule: queryCompleteRule, services: mockService, - logger: mockLogger, + ruleExecutionLogger, eventsTelemetry: undefined, - id: sampleRuleGuid, inputIndexPattern, pageSize: 1, filter: defaultFilter, buildReasonMessage, - buildRuleMessage, bulkCreate, wrapHits, runtimeMappings: undefined, @@ -813,14 +791,12 @@ describe('searchAfterAndBulkCreate', () => { tuple, completeRule: queryCompleteRule, services: mockService, - logger: mockLogger, + ruleExecutionLogger, eventsTelemetry: undefined, - id: sampleRuleGuid, inputIndexPattern, pageSize: 1, filter: defaultFilter, buildReasonMessage, - buildRuleMessage, bulkCreate, wrapHits, runtimeMappings: undefined, @@ -943,14 +919,12 @@ describe('searchAfterAndBulkCreate', () => { listClient, exceptionsList: [], services: mockService, - logger: mockLogger, + ruleExecutionLogger, eventsTelemetry: undefined, - id: sampleRuleGuid, inputIndexPattern, pageSize: 1, filter: defaultFilter, buildReasonMessage, - buildRuleMessage, bulkCreate, wrapHits, runtimeMappings: undefined, @@ -1032,14 +1006,12 @@ describe('searchAfterAndBulkCreate', () => { listClient, exceptionsList: [], services: mockService, - logger: mockLogger, + ruleExecutionLogger, eventsTelemetry: undefined, - id: sampleRuleGuid, inputIndexPattern, pageSize: 1, filter: defaultFilter, buildReasonMessage, - buildRuleMessage, bulkCreate, wrapHits, runtimeMappings: undefined, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts index afd2bb32ed8ae..6b6dedc302c0f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts @@ -25,7 +25,6 @@ import { withSecuritySpan } from '../../../utils/with_security_span'; // search_after through documents and re-index using bulk endpoint. export const searchAfterAndBulkCreate = async ({ buildReasonMessage, - buildRuleMessage, bulkCreate, completeRule, enrichment = identity, @@ -34,8 +33,8 @@ export const searchAfterAndBulkCreate = async ({ filter, inputIndexPattern, listClient, - logger, pageSize, + ruleExecutionLogger, services, sortOrder, trackTotalHits, @@ -57,7 +56,7 @@ export const searchAfterAndBulkCreate = async ({ let signalsCreatedCount = 0; if (tuple == null || tuple.to == null || tuple.from == null) { - logger.error(buildRuleMessage(`[-] malformed date tuple`)); + ruleExecutionLogger.error(`[-] malformed date tuple`); return createSearchAfterReturnType({ success: false, errors: ['malformed date tuple'], @@ -68,17 +67,17 @@ export const searchAfterAndBulkCreate = async ({ while (signalsCreatedCount < tuple.maxSignals) { try { let mergedSearchResults = createSearchResultReturnType(); - logger.debug(buildRuleMessage(`sortIds: ${sortIds}`)); + ruleExecutionLogger.debug(`sortIds: ${sortIds}`); + if (hasSortId) { const { searchResult, searchDuration, searchErrors } = await singleSearchAfter({ - buildRuleMessage, searchAfterSortIds: sortIds, index: inputIndexPattern, runtimeMappings, from: tuple.from.toISOString(), to: tuple.to.toISOString(), services, - logger, + ruleExecutionLogger, filter, pageSize: Math.ceil(Math.min(tuple.maxSignals, pageSize)), primaryTimestamp, @@ -112,18 +111,16 @@ export const searchAfterAndBulkCreate = async ({ // determine if there are any candidate signals to be processed const totalHits = getTotalHitsValue(mergedSearchResults.hits.total); - logger.debug(buildRuleMessage(`totalHits: ${totalHits}`)); - logger.debug( - buildRuleMessage(`searchResult.hit.hits.length: ${mergedSearchResults.hits.hits.length}`) + ruleExecutionLogger.debug(`totalHits: ${totalHits}`); + ruleExecutionLogger.debug( + `searchResult.hit.hits.length: ${mergedSearchResults.hits.hits.length}` ); if (totalHits === 0 || mergedSearchResults.hits.hits.length === 0) { - logger.debug( - buildRuleMessage( - `${ - totalHits === 0 ? 'totalHits' : 'searchResult.hits.hits.length' - } was 0, exiting early` - ) + ruleExecutionLogger.debug( + `${ + totalHits === 0 ? 'totalHits' : 'searchResult.hits.hits.length' + } was 0, exiting early` ); break; } @@ -134,9 +131,8 @@ export const searchAfterAndBulkCreate = async ({ const [includedEvents, _] = await filterEventsAgainstList({ listClient, exceptionsList, - logger, + ruleExecutionLogger, events: mergedSearchResults.hits.hits, - buildRuleMessage, }); // only bulk create if there are filteredEvents leftover @@ -168,25 +164,25 @@ export const searchAfterAndBulkCreate = async ({ }), ]); signalsCreatedCount += createdCount; - logger.debug(buildRuleMessage(`created ${createdCount} signals`)); - logger.debug(buildRuleMessage(`signalsCreatedCount: ${signalsCreatedCount}`)); - logger.debug(buildRuleMessage(`enrichedEvents.hits.hits: ${enrichedEvents.length}`)); + + ruleExecutionLogger.debug(`created ${createdCount} signals`); + ruleExecutionLogger.debug(`signalsCreatedCount: ${signalsCreatedCount}`); + ruleExecutionLogger.debug(`enrichedEvents.hits.hits: ${enrichedEvents.length}`); sendAlertTelemetryEvents( - logger, - eventsTelemetry, enrichedEvents, createdItems, - buildRuleMessage + eventsTelemetry, + ruleExecutionLogger ); } if (!hasSortId) { - logger.debug(buildRuleMessage('ran out of sort ids to sort on')); + ruleExecutionLogger.debug('ran out of sort ids to sort on'); break; } } catch (exc: unknown) { - logger.error(buildRuleMessage(`[-] search_after_bulk_create threw an error ${exc}`)); + ruleExecutionLogger.error(`[-] search_after_bulk_create threw an error ${exc}`); return mergeReturns([ toReturn, createSearchAfterReturnType({ @@ -196,7 +192,7 @@ export const searchAfterAndBulkCreate = async ({ ]); } } - logger.debug(buildRuleMessage(`[+] completed bulk index of ${toReturn.createdSignalsCount}`)); + ruleExecutionLogger.debug(`[+] completed bulk index of ${toReturn.createdSignalsCount}`); return toReturn; }); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/send_telemetry_events.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/send_telemetry_events.ts index a8ded1ea3f063..65742d5145110 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/send_telemetry_events.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/send_telemetry_events.ts @@ -5,10 +5,9 @@ * 2.0. */ -import type { Logger } from '@kbn/core/server'; import type { ITelemetryEventsSender } from '../../telemetry/sender'; import type { TelemetryEvent } from '../../telemetry/types'; -import type { BuildRuleMessage } from './rule_messages'; +import type { IRuleExecutionLogForExecutors } from '../rule_monitoring'; import type { SignalSource, SignalSourceHit } from './types'; interface SearchResultSource { @@ -44,11 +43,10 @@ export function enrichEndpointAlertsSignalID( } export function sendAlertTelemetryEvents( - logger: Logger, - eventsTelemetry: ITelemetryEventsSender | undefined, filteredEvents: SignalSourceHit[], createdEvents: SignalSource[], - buildRuleMessage: BuildRuleMessage + eventsTelemetry: ITelemetryEventsSender | undefined, + ruleExecutionLogger: IRuleExecutionLogForExecutors ) { if (eventsTelemetry === undefined) { return; @@ -74,6 +72,6 @@ export function sendAlertTelemetryEvents( try { eventsTelemetry.queueTelemetryEvents(selectedEvents); } catch (exc) { - logger.error(buildRuleMessage(`[-] queing telemetry events failed ${exc}`)); + ruleExecutionLogger.error(`[-] queing telemetry events failed ${exc}`); } } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.test.ts index b75eb81eb4163..28b00e45dd5a3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.test.ts @@ -7,23 +7,17 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { sampleDocSearchResultsNoSortId, - mockLogger, sampleDocSearchResultsWithSortId, } from './__mocks__/es_results'; import { singleSearchAfter } from './single_search_after'; import type { RuleExecutorServicesMock } from '@kbn/alerting-plugin/server/mocks'; import { alertsMock } from '@kbn/alerting-plugin/server/mocks'; -import { buildRuleMessageFactory } from './rule_messages'; import { elasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-mocks'; +import { ruleExecutionLogMock } from '../rule_monitoring/mocks'; -const buildRuleMessage = buildRuleMessageFactory({ - id: 'fake id', - ruleId: 'fake rule id', - index: 'fakeindex', - name: 'fake name', -}); describe('singleSearchAfter', () => { const mockService: RuleExecutorServicesMock = alertsMock.createRuleExecutorServices(); + const ruleExecutionLogger = ruleExecutionLogMock.forExecutors.create(); beforeEach(() => { jest.clearAllMocks(); @@ -39,12 +33,11 @@ describe('singleSearchAfter', () => { from: 'now-360s', to: 'now', services: mockService, - logger: mockLogger, + ruleExecutionLogger, pageSize: 1, filter: {}, primaryTimestamp: '@timestamp', secondaryTimestamp: undefined, - buildRuleMessage, runtimeMappings: undefined, }); expect(searchResult).toEqual(sampleDocSearchResultsNoSortId()); @@ -59,12 +52,11 @@ describe('singleSearchAfter', () => { from: 'now-360s', to: 'now', services: mockService, - logger: mockLogger, + ruleExecutionLogger, pageSize: 1, filter: {}, primaryTimestamp: '@timestamp', secondaryTimestamp: undefined, - buildRuleMessage, runtimeMappings: undefined, }); expect(searchErrors).toEqual([]); @@ -111,12 +103,11 @@ describe('singleSearchAfter', () => { from: 'now-360s', to: 'now', services: mockService, - logger: mockLogger, + ruleExecutionLogger, pageSize: 1, filter: {}, primaryTimestamp: '@timestamp', secondaryTimestamp: undefined, - buildRuleMessage, runtimeMappings: undefined, }); expect(searchErrors).toEqual([ @@ -136,12 +127,11 @@ describe('singleSearchAfter', () => { from: 'now-360s', to: 'now', services: mockService, - logger: mockLogger, + ruleExecutionLogger, pageSize: 1, filter: {}, primaryTimestamp: '@timestamp', secondaryTimestamp: undefined, - buildRuleMessage, runtimeMappings: undefined, }); expect(searchResult).toEqual(sampleDocSearchResultsWithSortId()); @@ -158,12 +148,11 @@ describe('singleSearchAfter', () => { from: 'now-360s', to: 'now', services: mockService, - logger: mockLogger, + ruleExecutionLogger, pageSize: 1, filter: {}, primaryTimestamp: '@timestamp', secondaryTimestamp: undefined, - buildRuleMessage, runtimeMappings: undefined, }) ).rejects.toThrow('Fake Error'); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts index ae5b4824d282e..0a0534e887c5e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts @@ -11,9 +11,7 @@ import type { AlertInstanceState, RuleExecutorServices, } from '@kbn/alerting-plugin/server'; -import type { Logger } from '@kbn/core/server'; import type { SignalSearchResponse, SignalSource } from './types'; -import type { BuildRuleMessage } from './rule_messages'; import { buildEventsSearchQuery } from './build_events_query'; import { createErrorsFromShard, makeFloatString } from './utils'; import type { @@ -21,6 +19,7 @@ import type { TimestampOverrideOrUndefined, } from '../../../../common/detection_engine/schemas/common/schemas'; import { withSecuritySpan } from '../../../utils/with_security_span'; +import type { IRuleExecutionLogForExecutors } from '../rule_monitoring'; interface SingleSearchAfterParams { aggregations?: Record; @@ -29,13 +28,12 @@ interface SingleSearchAfterParams { from: string; to: string; services: RuleExecutorServices; - logger: Logger; + ruleExecutionLogger: IRuleExecutionLogForExecutors; pageSize: number; sortOrder?: estypes.SortOrder; filter: estypes.QueryDslQueryContainer; primaryTimestamp: TimestampOverride; secondaryTimestamp: TimestampOverrideOrUndefined; - buildRuleMessage: BuildRuleMessage; trackTotalHits?: boolean; runtimeMappings: estypes.MappingRuntimeFields | undefined; } @@ -52,12 +50,11 @@ export const singleSearchAfter = async < to, services, filter, - logger, + ruleExecutionLogger, pageSize, sortOrder, primaryTimestamp, secondaryTimestamp, - buildRuleMessage, trackTotalHits, }: SingleSearchAfterParams): Promise<{ searchResult: SignalSearchResponse; @@ -99,13 +96,13 @@ export const singleSearchAfter = async < searchErrors, }; } catch (exc) { - logger.error(buildRuleMessage(`[-] nextSearchAfter threw an error ${exc}`)); + ruleExecutionLogger.error(`[-] nextSearchAfter threw an error ${exc}`); if ( exc.message.includes(`No mapping found for [${primaryTimestamp}] in order to sort on`) || (secondaryTimestamp && exc.message.includes(`No mapping found for [${secondaryTimestamp}] in order to sort on`)) ) { - logger.error(buildRuleMessage(`[-] failure reason: ${exc.message}`)); + ruleExecutionLogger.error(`[-] failure reason: ${exc.message}`); const searchRes: SignalSearchResponse = { took: 0, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_enrichment.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_enrichment.ts index ec431a23e0e54..fc5ec50c6bc6f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_enrichment.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/build_threat_enrichment.ts @@ -11,9 +11,8 @@ import type { BuildThreatEnrichmentOptions, GetMatchedThreats } from './types'; import { getThreatList } from './get_threat_list'; export const buildThreatEnrichment = ({ - buildRuleMessage, exceptionItems, - logger, + ruleExecutionLogger, services, threatFilters, threatIndex, @@ -37,14 +36,13 @@ export const buildThreatEnrichment = ({ const threatResponse = await getThreatList({ esClient: services.scopedClusterClient.asCurrentUser, exceptionItems, - threatFilters: [...threatFilters, matchedThreatsFilter], - query: threatQuery, - language: threatLanguage, index: threatIndex, - searchAfter: undefined, - logger, - buildRuleMessage, + language: threatLanguage, perPage: undefined, + query: threatQuery, + ruleExecutionLogger, + searchAfter: undefined, + threatFilters: [...threatFilters, matchedThreatsFilter], threatListConfig: { _source: [`${threatIndicatorPath}.*`, 'threat.feed.*'], fields: undefined, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_event_signal.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_event_signal.ts index 67e39e71421df..a1a63b2e2493c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_event_signal.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_event_signal.ts @@ -19,7 +19,6 @@ import { export const createEventSignal = async ({ alertId, - buildRuleMessage, bulkCreate, completeRule, currentResult, @@ -30,9 +29,9 @@ export const createEventSignal = async ({ inputIndex, language, listClient, - logger, outputIndex, query, + ruleExecutionLogger, savedId, searchAfterSize, services, @@ -60,10 +59,8 @@ export const createEventSignal = async ({ if (!threatFilter.query || threatFilter.query?.bool.should.length === 0) { // empty event list and we do not want to return everything as being // a hit so opt to return the existing result. - logger.debug( - buildRuleMessage( - 'Indicator items are empty after filtering for missing data, returning without attempting a match' - ) + ruleExecutionLogger.debug( + 'Indicator items are empty after filtering for missing data, returning without attempting a match' ); return currentResult; } else { @@ -74,8 +71,7 @@ export const createEventSignal = async ({ query: threatQuery, language: threatLanguage, index: threatIndex, - logger, - buildRuleMessage, + ruleExecutionLogger, threatListConfig: { _source: [`${threatIndicatorPath}.*`, 'threat.feed.*'], fields: undefined, @@ -111,10 +107,8 @@ export const createEventSignal = async ({ lists: exceptionItems, }); - logger.debug( - buildRuleMessage( - `${ids?.length} matched signals found from ${threatListHits.length} indicators` - ) + ruleExecutionLogger.debug( + `${ids?.length} matched signals found from ${threatListHits.length} indicators` ); const threatEnrichment = (signals: SignalSourceHit[]): Promise => @@ -127,18 +121,16 @@ export const createEventSignal = async ({ const result = await searchAfterAndBulkCreate({ buildReasonMessage: buildReasonMessageForThreatMatchAlert, - buildRuleMessage, bulkCreate, completeRule, enrichment: threatEnrichment, eventsTelemetry, exceptionsList: exceptionItems, filter: esFilter, - id: alertId, inputIndexPattern: inputIndex, listClient, - logger, pageSize: searchAfterSize, + ruleExecutionLogger, services, sortOrder: 'desc', trackTotalHits: false, @@ -149,14 +141,12 @@ export const createEventSignal = async ({ secondaryTimestamp, }); - logger.debug( - buildRuleMessage( - `${ - threatFilter.query?.bool.should.length - } items have completed match checks and the total times to search were ${ - result.searchAfterTimes.length !== 0 ? result.searchAfterTimes : '(unknown) ' - }ms` - ) + ruleExecutionLogger.debug( + `${ + threatFilter.query?.bool.should.length + } items have completed match checks and the total times to search were ${ + result.searchAfterTimes.length !== 0 ? result.searchAfterTimes : '(unknown) ' + }ms` ); return result; } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts index faf61c09ca79b..b063ef87761bc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts @@ -15,7 +15,6 @@ import type { SearchAfterAndBulkCreateReturnType } from '../types'; export const createThreatSignal = async ({ alertId, - buildRuleMessage, bulkCreate, completeRule, currentResult, @@ -26,9 +25,9 @@ export const createThreatSignal = async ({ inputIndex, language, listClient, - logger, outputIndex, query, + ruleExecutionLogger, savedId, searchAfterSize, services, @@ -50,10 +49,8 @@ export const createThreatSignal = async ({ if (!threatFilter.query || threatFilter.query?.bool.should.length === 0) { // empty threat list and we do not want to return everything as being // a hit so opt to return the existing result. - logger.debug( - buildRuleMessage( - 'Indicator items are empty after filtering for missing data, returning without attempting a match' - ) + ruleExecutionLogger.debug( + 'Indicator items are empty after filtering for missing data, returning without attempting a match' ); return currentResult; } else { @@ -68,26 +65,22 @@ export const createThreatSignal = async ({ lists: exceptionItems, }); - logger.debug( - buildRuleMessage( - `${threatFilter.query?.bool.should.length} indicator items are being checked for existence of matches` - ) + ruleExecutionLogger.debug( + `${threatFilter.query?.bool.should.length} indicator items are being checked for existence of matches` ); const result = await searchAfterAndBulkCreate({ buildReasonMessage: buildReasonMessageForThreatMatchAlert, - buildRuleMessage, bulkCreate, completeRule, enrichment: threatEnrichment, eventsTelemetry, exceptionsList: exceptionItems, filter: esFilter, - id: alertId, inputIndexPattern: inputIndex, listClient, - logger, pageSize: searchAfterSize, + ruleExecutionLogger, services, sortOrder: 'desc', trackTotalHits: false, @@ -98,14 +91,12 @@ export const createThreatSignal = async ({ secondaryTimestamp, }); - logger.debug( - buildRuleMessage( - `${ - threatFilter.query?.bool.should.length - } items have completed match checks and the total times to search were ${ - result.searchAfterTimes.length !== 0 ? result.searchAfterTimes : '(unknown) ' - }ms` - ) + ruleExecutionLogger.debug( + `${ + threatFilter.query?.bool.should.length + } items have completed match checks and the total times to search were ${ + result.searchAfterTimes.length !== 0 ? result.searchAfterTimes : '(unknown) ' + }ms` ); return result; } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts index 579bf1ca8859c..f0f303e7c837f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts @@ -25,7 +25,6 @@ import { THREAT_PIT_KEEP_ALIVE } from '../../../../../common/cti/constants'; export const createThreatSignals = async ({ alertId, - buildRuleMessage, bulkCreate, completeRule, concurrentSearches, @@ -36,9 +35,9 @@ export const createThreatSignals = async ({ itemsPerSearch, language, listClient, - logger, outputIndex, query, + ruleExecutionLogger, savedId, searchAfterSize, services, @@ -56,7 +55,7 @@ export const createThreatSignals = async ({ secondaryTimestamp, }: CreateThreatSignalsOptions): Promise => { const params = completeRule.ruleParams; - logger.debug(buildRuleMessage('Indicator matching rule starting')); + ruleExecutionLogger.debug('Indicator matching rule starting'); const perPage = concurrentSearches * itemsPerSearch; const verifyExecutionCanProceed = buildExecutionIntervalValidator( completeRule.ruleConfig.schedule.interval @@ -90,10 +89,10 @@ export const createThreatSignals = async ({ secondaryTimestamp, }); - logger.debug(`Total event count: ${eventCount}`); + ruleExecutionLogger.debug(`Total event count: ${eventCount}`); // if (eventCount === 0) { - // logger.debug(buildRuleMessage('Indicator matching rule has completed')); + // ruleExecutionLogger.debug('Indicator matching rule has completed'); // return results; // } @@ -116,7 +115,7 @@ export const createThreatSignals = async ({ index: threatIndex, }); - logger.debug(buildRuleMessage(`Total indicator items: ${threatListCount}`)); + ruleExecutionLogger.debug(`Total indicator items: ${threatListCount}`); const threatListConfig = { fields: threatMapping.map((mapping) => mapping.entries.map((item) => item.value)).flat(), @@ -124,9 +123,8 @@ export const createThreatSignals = async ({ }; const threatEnrichment = buildThreatEnrichment({ - buildRuleMessage, exceptionItems, - logger, + ruleExecutionLogger, services, threatFilters: allThreatFilters, threatIndex, @@ -153,31 +151,25 @@ export const createThreatSignals = async ({ while (list.hits.hits.length !== 0) { verifyExecutionCanProceed(); const chunks = chunk(itemsPerSearch, list.hits.hits); - logger.debug( - buildRuleMessage(`${chunks.length} concurrent indicator searches are starting.`) - ); + ruleExecutionLogger.debug(`${chunks.length} concurrent indicator searches are starting.`); const concurrentSearchesPerformed = chunks.map>(createSignal); const searchesPerformed = await Promise.all(concurrentSearchesPerformed); results = combineConcurrentResults(results, searchesPerformed); documentCount -= list.hits.hits.length; - logger.debug( - buildRuleMessage( - `Concurrent indicator match searches completed with ${results.createdSignalsCount} signals found`, - `search times of ${results.searchAfterTimes}ms,`, - `bulk create times ${results.bulkCreateTimes}ms,`, - `all successes are ${results.success}` - ) + ruleExecutionLogger.debug( + `Concurrent indicator match searches completed with ${results.createdSignalsCount} signals found`, + `search times of ${results.searchAfterTimes}ms,`, + `bulk create times ${results.bulkCreateTimes}ms,`, + `all successes are ${results.success}` ); if (results.createdSignalsCount >= params.maxSignals) { - logger.debug( - buildRuleMessage( - `Indicator match has reached its max signals count ${params.maxSignals}. Additional documents not checked are ${documentCount}` - ) + ruleExecutionLogger.debug( + `Indicator match has reached its max signals count ${params.maxSignals}. Additional documents not checked are ${documentCount}` ); break; } - logger.debug(buildRuleMessage(`Documents items left to check are ${documentCount}`)); + ruleExecutionLogger.debug(`Documents items left to check are ${documentCount}`); list = await getDocumentList({ searchAfter: list.hits.hits[list.hits.hits.length - 1].sort, @@ -191,14 +183,13 @@ export const createThreatSignals = async ({ getDocumentList: async ({ searchAfter }) => getEventList({ services, + ruleExecutionLogger, exceptionItems, filters: allEventFilters, query, language, index: inputIndex, searchAfter, - logger, - buildRuleMessage, perPage, tuple, runtimeMappings, @@ -209,7 +200,6 @@ export const createThreatSignals = async ({ createSignal: (slicedChunk) => createEventSignal({ alertId, - buildRuleMessage, bulkCreate, completeRule, currentEventList: slicedChunk, @@ -220,10 +210,10 @@ export const createThreatSignals = async ({ inputIndex, language, listClient, - logger, outputIndex, query, reassignThreatPitId, + ruleExecutionLogger, savedId, searchAfterSize, services, @@ -255,8 +245,7 @@ export const createThreatSignals = async ({ language: threatLanguage, index: threatIndex, searchAfter, - logger, - buildRuleMessage, + ruleExecutionLogger, perPage, threatListConfig, pitId: threatPitId, @@ -268,7 +257,6 @@ export const createThreatSignals = async ({ createSignal: (slicedChunk) => createThreatSignal({ alertId, - buildRuleMessage, bulkCreate, completeRule, currentResult: results, @@ -279,9 +267,9 @@ export const createThreatSignals = async ({ inputIndex, language, listClient, - logger, outputIndex, query, + ruleExecutionLogger, savedId, searchAfterSize, services, @@ -301,11 +289,11 @@ export const createThreatSignals = async ({ await services.scopedClusterClient.asCurrentUser.closePointInTime({ id: threatPitId }); } catch (error) { // Don't fail due to a bad point in time closure. We have seen failures in e2e tests during nominal operations. - logger.warn( + ruleExecutionLogger.warn( `Error trying to close point in time: "${threatPitId}", it will expire within "${THREAT_PIT_KEEP_ALIVE}". Error is: "${error}"` ); } - logger.debug(buildRuleMessage('Indicator matching rule has completed')); + ruleExecutionLogger.debug('Indicator matching rule has completed'); return results; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_event_count.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_event_count.ts index f01722612559a..347b8fa4c5d63 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_event_count.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_event_count.ts @@ -15,6 +15,7 @@ export const MAX_PER_PAGE = 9000; export const getEventList = async ({ services, + ruleExecutionLogger, query, language, index, @@ -22,8 +23,6 @@ export const getEventList = async ({ searchAfter, exceptionItems, filters, - buildRuleMessage, - logger, tuple, primaryTimestamp, secondaryTimestamp, @@ -34,22 +33,19 @@ export const getEventList = async ({ throw new TypeError('perPage cannot exceed the size of 10000'); } - logger.debug( - buildRuleMessage( - `Querying the events items from the index: "${index}" with searchAfter: "${searchAfter}" for up to ${calculatedPerPage} indicator items` - ) + ruleExecutionLogger.debug( + `Querying the events items from the index: "${index}" with searchAfter: "${searchAfter}" for up to ${calculatedPerPage} indicator items` ); const filter = getQueryFilter(query, language ?? 'kuery', filters, index, exceptionItems); const { searchResult } = await singleSearchAfter({ - buildRuleMessage, searchAfterSortIds: searchAfter, index, from: tuple.from.toISOString(), to: tuple.to.toISOString(), services, - logger, + ruleExecutionLogger, filter, pageSize: Math.ceil(Math.min(tuple.maxSignals, calculatedPerPage)), primaryTimestamp, @@ -59,9 +55,7 @@ export const getEventList = async ({ runtimeMappings, }); - logger.debug( - buildRuleMessage(`Retrieved events items of size: ${searchResult.hits.hits.length}`) - ); + ruleExecutionLogger.debug(`Retrieved events items of size: ${searchResult.hits.hits.length}`); return searchResult; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_threat_list.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_threat_list.ts index 7ca91748fa567..27d8359453d71 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_threat_list.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/get_threat_list.ts @@ -22,18 +22,17 @@ export const INDICATOR_PER_PAGE = 1000; export const getThreatList = async ({ esClient, - query, - language, + exceptionItems, index, + language, + perPage, + query, + ruleExecutionLogger, searchAfter, - exceptionItems, threatFilters, - buildRuleMessage, - logger, threatListConfig, pitId, reassignPitId, - perPage, runtimeMappings, listClient, }: GetThreatListOptions): Promise> => { @@ -49,10 +48,8 @@ export const getThreatList = async ({ exceptionItems ); - logger.debug( - buildRuleMessage( - `Querying the indicator items from the index: "${index}" with searchAfter: "${searchAfter}" for up to ${calculatedPerPage} indicator items` - ) + ruleExecutionLogger.debug( + `Querying the indicator items from the index: "${index}" with searchAfter: "${searchAfter}" for up to ${calculatedPerPage} indicator items` ); const response = await esClient.search< @@ -74,7 +71,7 @@ export const getThreatList = async ({ pit: { id: pitId }, }); - logger.debug(buildRuleMessage(`Retrieved indicator items of size: ${response.hits.hits.length}`)); + ruleExecutionLogger.debug(`Retrieved indicator items of size: ${response.hits.hits.length}`); reassignPitId(response.pit_id); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts index 62f3e9fc7e69e..b4c5d217a3a61 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts @@ -25,9 +25,8 @@ import type { AlertInstanceState, RuleExecutorServices, } from '@kbn/alerting-plugin/server'; -import type { ElasticsearchClient, Logger } from '@kbn/core/server'; +import type { ElasticsearchClient } from '@kbn/core/server'; import type { ITelemetryEventsSender } from '../../../telemetry/sender'; -import type { BuildRuleMessage } from '../rule_messages'; import type { BulkCreate, RuleRangeTuple, @@ -36,12 +35,12 @@ import type { WrapHits, } from '../types'; import type { CompleteRule, ThreatRuleParams } from '../../schemas/rule_schemas'; +import type { IRuleExecutionLogForExecutors } from '../../rule_monitoring'; export type SortOrderOrUndefined = 'asc' | 'desc' | undefined; export interface CreateThreatSignalsOptions { alertId: string; - buildRuleMessage: BuildRuleMessage; bulkCreate: BulkCreate; completeRule: CompleteRule; concurrentSearches: ConcurrentSearches; @@ -52,9 +51,9 @@ export interface CreateThreatSignalsOptions { itemsPerSearch: ItemsPerSearch; language: LanguageOrUndefined; listClient: ListClient; - logger: Logger; outputIndex: string; query: string; + ruleExecutionLogger: IRuleExecutionLogForExecutors; savedId: string | undefined; searchAfterSize: number; services: RuleExecutorServices; @@ -74,7 +73,6 @@ export interface CreateThreatSignalsOptions { export interface CreateThreatSignalOptions { alertId: string; - buildRuleMessage: BuildRuleMessage; bulkCreate: BulkCreate; completeRule: CompleteRule; currentResult: SearchAfterAndBulkCreateReturnType; @@ -85,9 +83,9 @@ export interface CreateThreatSignalOptions { inputIndex: string[]; language: LanguageOrUndefined; listClient: ListClient; - logger: Logger; outputIndex: string; query: string; + ruleExecutionLogger: IRuleExecutionLogForExecutors; savedId: string | undefined; searchAfterSize: number; services: RuleExecutorServices; @@ -103,7 +101,6 @@ export interface CreateThreatSignalOptions { export interface CreateEventSignalOptions { alertId: string; - buildRuleMessage: BuildRuleMessage; bulkCreate: BulkCreate; completeRule: CompleteRule; currentResult: SearchAfterAndBulkCreateReturnType; @@ -114,9 +111,9 @@ export interface CreateEventSignalOptions { inputIndex: string[]; language: LanguageOrUndefined; listClient: ListClient; - logger: Logger; outputIndex: string; query: string; + ruleExecutionLogger: IRuleExecutionLogForExecutors; savedId: string | undefined; searchAfterSize: number; services: RuleExecutorServices; @@ -186,14 +183,13 @@ interface ThreatListConfig { } export interface GetThreatListOptions { - buildRuleMessage: BuildRuleMessage; esClient: ElasticsearchClient; exceptionItems: ExceptionListItemSchema[]; index: string[]; language: ThreatLanguageOrUndefined; - logger: Logger; perPage?: number; query: string; + ruleExecutionLogger: IRuleExecutionLogForExecutors; searchAfter: estypes.SortResults | undefined; threatFilters: unknown[]; threatListConfig: ThreatListConfig; @@ -238,9 +234,8 @@ export interface ThreatMatchNamedQuery { export type GetMatchedThreats = (ids: string[]) => Promise; export interface BuildThreatEnrichmentOptions { - buildRuleMessage: BuildRuleMessage; exceptionItems: ExceptionListItemSchema[]; - logger: Logger; + ruleExecutionLogger: IRuleExecutionLogForExecutors; services: RuleExecutorServices; threatFilters: unknown[]; threatIndex: ThreatIndex; @@ -254,14 +249,13 @@ export interface BuildThreatEnrichmentOptions { export interface EventsOptions { services: RuleExecutorServices; + ruleExecutionLogger: IRuleExecutionLogForExecutors; query: string; - buildRuleMessage: BuildRuleMessage; language: ThreatLanguageOrUndefined; exceptionItems: ExceptionListItemSchema[]; index: string[]; searchAfter: estypes.SortResults | undefined; perPage?: number; - logger: Logger; filters: unknown[]; primaryTimestamp: string; secondaryTimestamp?: string; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts index b02aa91adf90c..a4b7eb9cdcedb 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/bulk_create_threshold_signals.ts @@ -7,7 +7,6 @@ import { TIMESTAMP } from '@kbn/rule-data-utils'; -import type { Logger } from '@kbn/core/server'; import type { AlertInstanceContext, AlertInstanceState, @@ -27,7 +26,6 @@ interface BulkCreateThresholdSignalsParams { completeRule: CompleteRule; services: RuleExecutorServices; inputIndexPattern: string[]; - logger: Logger; filter: unknown; signalsIndex: string; startedAt: Date; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/find_threshold_signals.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/find_threshold_signals.test.ts index 0668afad39431..1d17d1ed63966 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/find_threshold_signals.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/find_threshold_signals.test.ts @@ -8,18 +8,11 @@ import type { RuleExecutorServicesMock } from '@kbn/alerting-plugin/server/mocks'; import { alertsMock } from '@kbn/alerting-plugin/server/mocks'; import { getQueryFilter } from '../../../../../common/detection_engine/get_query_filter'; -import { mockLogger, sampleEmptyDocSearchResults } from '../__mocks__/es_results'; -import { buildRuleMessageFactory } from '../rule_messages'; +import { sampleEmptyDocSearchResults } from '../__mocks__/es_results'; import * as single_search_after from '../single_search_after'; import { findThresholdSignals } from './find_threshold_signals'; import { TIMESTAMP } from '@kbn/rule-data-utils'; - -const buildRuleMessage = buildRuleMessageFactory({ - id: 'fake id', - ruleId: 'fake rule id', - index: 'fakeindex', - name: 'fake name', -}); +import { ruleExecutionLogMock } from '../../rule_monitoring/mocks'; const queryFilter = getQueryFilter('', 'kuery', [], ['*'], []); const mockSingleSearchAfter = jest.fn(async () => ({ @@ -37,6 +30,7 @@ const mockSingleSearchAfter = jest.fn(async () => ({ describe('findThresholdSignals', () => { let mockService: RuleExecutorServicesMock; + const ruleExecutionLogger = ruleExecutionLogMock.forExecutors.create(); beforeEach(() => { jest.clearAllMocks(); @@ -51,13 +45,12 @@ describe('findThresholdSignals', () => { maxSignals: 100, inputIndexPattern: ['*'], services: mockService, - logger: mockLogger, + ruleExecutionLogger, filter: queryFilter, threshold: { field: [], value: 100, }, - buildRuleMessage, runtimeMappings: undefined, primaryTimestamp: TIMESTAMP, secondaryTimestamp: undefined, @@ -87,13 +80,12 @@ describe('findThresholdSignals', () => { maxSignals: 100, inputIndexPattern: ['*'], services: mockService, - logger: mockLogger, + ruleExecutionLogger, filter: queryFilter, threshold: { field: ['host.name'], value: 100, }, - buildRuleMessage, runtimeMappings: undefined, primaryTimestamp: TIMESTAMP, secondaryTimestamp: undefined, @@ -148,14 +140,13 @@ describe('findThresholdSignals', () => { maxSignals: 100, inputIndexPattern: ['*'], services: mockService, - logger: mockLogger, + ruleExecutionLogger, filter: queryFilter, threshold: { field: ['host.name', 'user.name'], value: 100, cardinality: [], }, - buildRuleMessage, runtimeMappings: undefined, primaryTimestamp: TIMESTAMP, secondaryTimestamp: undefined, @@ -217,7 +208,7 @@ describe('findThresholdSignals', () => { maxSignals: 100, inputIndexPattern: ['*'], services: mockService, - logger: mockLogger, + ruleExecutionLogger, filter: queryFilter, threshold: { field: ['host.name', 'user.name'], @@ -229,7 +220,6 @@ describe('findThresholdSignals', () => { }, ], }, - buildRuleMessage, runtimeMappings: undefined, primaryTimestamp: TIMESTAMP, secondaryTimestamp: undefined, @@ -304,7 +294,7 @@ describe('findThresholdSignals', () => { maxSignals: 100, inputIndexPattern: ['*'], services: mockService, - logger: mockLogger, + ruleExecutionLogger, filter: queryFilter, threshold: { cardinality: [ @@ -316,7 +306,6 @@ describe('findThresholdSignals', () => { field: [], value: 200, }, - buildRuleMessage, runtimeMappings: undefined, primaryTimestamp: TIMESTAMP, secondaryTimestamp: undefined, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/find_threshold_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/find_threshold_signals.ts index bd87df8b2a4c8..6f13f495027e3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/find_threshold_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threshold/find_threshold_signals.ts @@ -12,7 +12,6 @@ import type { AlertInstanceState, RuleExecutorServices, } from '@kbn/alerting-plugin/server'; -import type { Logger } from '@kbn/core/server'; import type { ESBoolQuery } from '../../../../../common/typed_json'; import type { @@ -20,7 +19,6 @@ import type { TimestampOverride, TimestampOverrideOrUndefined, } from '../../../../../common/detection_engine/schemas/common/schemas'; -import type { BuildRuleMessage } from '../rule_messages'; import { singleSearchAfter } from '../single_search_after'; import { buildThresholdMultiBucketAggregation, @@ -32,6 +30,7 @@ import type { ThresholdSingleBucketAggregationResult, } from './types'; import { shouldFilterByCardinality } from './utils'; +import type { IRuleExecutionLogForExecutors } from '../../rule_monitoring'; interface FindThresholdSignalsParams { from: string; @@ -39,10 +38,9 @@ interface FindThresholdSignalsParams { maxSignals: number; inputIndexPattern: string[]; services: RuleExecutorServices; - logger: Logger; + ruleExecutionLogger: IRuleExecutionLogForExecutors; filter: ESBoolQuery; threshold: ThresholdNormalized; - buildRuleMessage: BuildRuleMessage; runtimeMappings: estypes.MappingRuntimeFields | undefined; primaryTimestamp: TimestampOverride; secondaryTimestamp: TimestampOverrideOrUndefined; @@ -61,10 +59,9 @@ export const findThresholdSignals = async ({ maxSignals, inputIndexPattern, services, - logger, + ruleExecutionLogger, filter, threshold, - buildRuleMessage, runtimeMappings, primaryTimestamp, secondaryTimestamp, @@ -96,11 +93,10 @@ export const findThresholdSignals = async ({ from, to, services, - logger, + ruleExecutionLogger, filter, pageSize: 0, sortOrder: 'desc', - buildRuleMessage, runtimeMappings, primaryTimestamp, secondaryTimestamp, @@ -132,11 +128,10 @@ export const findThresholdSignals = async ({ from, to, services, - logger, + ruleExecutionLogger, filter, pageSize: 0, sortOrder: 'desc', - buildRuleMessage, trackTotalHits: true, runtimeMappings, primaryTimestamp, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts index a8c26ad14dd33..db88284bc8881 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts @@ -16,7 +16,6 @@ import type { RuleExecutorServices, } from '@kbn/alerting-plugin/server'; import type { ListClient } from '@kbn/lists-plugin/server'; -import type { Logger } from '@kbn/core/server'; import type { EcsFieldMap } from '@kbn/rule-registry-plugin/common/assets/field_maps/ecs_field_map'; import type { TypeOfFieldMap } from '@kbn/rule-registry-plugin/common/field_map'; import type { Status } from '../../../../common/detection_engine/schemas/common/schemas'; @@ -27,7 +26,6 @@ import type { SearchTypes, EqlSequence, } from '../../../../common/detection_engine/types'; -import type { BuildRuleMessage } from './rule_messages'; import type { ITelemetryEventsSender } from '../../telemetry/sender'; import type { CompleteRule, @@ -43,6 +41,7 @@ import type { DetectionAlert, WrappedFieldsLatest, } from '../../../../common/detection_engine/schemas/alerts'; +import type { IRuleExecutionLogForExecutors } from '../rule_monitoring'; export interface ThresholdResult { terms?: Array<{ @@ -270,13 +269,11 @@ export interface SearchAfterAndBulkCreateParams { services: RuleExecutorServices; listClient: ListClient; exceptionsList: ExceptionListItemSchema[]; - logger: Logger; + ruleExecutionLogger: IRuleExecutionLogForExecutors; eventsTelemetry: ITelemetryEventsSender | undefined; - id: string; inputIndexPattern: string[]; pageSize: number; filter: estypes.QueryDslQueryContainer; - buildRuleMessage: BuildRuleMessage; buildReasonMessage: BuildReasonMessage; enrichment?: SignalsEnrichment; bulkCreate: BulkCreate; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts index 0f56cc0238d0b..8bb1673fb4d41 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts @@ -13,9 +13,8 @@ import { ALERT_REASON, ALERT_RULE_PARAMETERS, ALERT_UUID, TIMESTAMP } from '@kbn import type { RuleExecutorServicesMock } from '@kbn/alerting-plugin/server/mocks'; import { alertsMock } from '@kbn/alerting-plugin/server/mocks'; import { listMock } from '@kbn/lists-plugin/server/mocks'; -import { buildRuleMessageFactory } from './rule_messages'; import type { ExceptionListClient } from '@kbn/lists-plugin/server'; -import { RuleExecutionStatus } from '../../../../common/detection_engine/schemas/common'; +import { RuleExecutionStatus } from '../../../../common/detection_engine/rule_monitoring'; import { getListArrayMock } from '../../../../common/detection_engine/schemas/types/lists.mock'; import { getExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_item_schema.mock'; @@ -52,7 +51,6 @@ import { sampleEmptyBulkResponse, sampleBulkError, sampleBulkErrorItem, - mockLogger, sampleSignalHit, sampleDocSearchResultsWithSortId, sampleEmptyDocSearchResults, @@ -63,24 +61,19 @@ import { sampleAlertDocAADNoSortIdWithTimestamp, } from './__mocks__/es_results'; import type { ShardError } from '../../types'; -import { ruleExecutionLogMock } from '../rule_execution_log/__mocks__'; - -const buildRuleMessage = buildRuleMessageFactory({ - id: 'fake id', - ruleId: 'fake rule id', - index: 'fakeindex', - name: 'fake name', -}); +import { ruleExecutionLogMock } from '../rule_monitoring/mocks'; describe('utils', () => { const anchor = '2020-01-01T06:06:06.666Z'; const unix = moment(anchor).valueOf(); let nowDate = moment('2020-01-01T00:00:00.000Z'); let clock: sinon.SinonFakeTimers; + let ruleExecutionLogger: ReturnType; beforeEach(() => { nowDate = moment('2020-01-01T00:00:00.000Z'); clock = sinon.useFakeTimers(unix); + ruleExecutionLogger = ruleExecutionLogMock.forExecutors.create(); }); afterEach(() => { @@ -449,14 +442,13 @@ describe('utils', () => { describe('getRuleRangeTuples', () => { test('should return a single tuple if no gap', () => { const { tuples, remainingGap } = getRuleRangeTuples({ - logger: mockLogger, previousStartedAt: moment().subtract(30, 's').toDate(), startedAt: moment().subtract(30, 's').toDate(), interval: '30s', from: 'now-30s', to: 'now', maxSignals: 20, - buildRuleMessage, + ruleExecutionLogger, }); const someTuple = tuples[0]; expect(moment(someTuple.to).diff(moment(someTuple.from), 's')).toEqual(30); @@ -466,14 +458,13 @@ describe('utils', () => { test('should return a single tuple if malformed interval prevents gap calculation', () => { const { tuples, remainingGap } = getRuleRangeTuples({ - logger: mockLogger, previousStartedAt: moment().subtract(30, 's').toDate(), startedAt: moment().subtract(30, 's').toDate(), interval: 'invalid', from: 'now-30s', to: 'now', maxSignals: 20, - buildRuleMessage, + ruleExecutionLogger, }); const someTuple = tuples[0]; expect(moment(someTuple.to).diff(moment(someTuple.from), 's')).toEqual(30); @@ -483,14 +474,13 @@ describe('utils', () => { test('should return two tuples if gap and previouslyStartedAt', () => { const { tuples, remainingGap } = getRuleRangeTuples({ - logger: mockLogger, previousStartedAt: moment().subtract(65, 's').toDate(), startedAt: moment().toDate(), interval: '50s', from: 'now-55s', to: 'now', maxSignals: 20, - buildRuleMessage, + ruleExecutionLogger, }); const someTuple = tuples[1]; expect(moment(someTuple.to).diff(moment(someTuple.from), 's')).toEqual(55); @@ -499,14 +489,13 @@ describe('utils', () => { test('should return five tuples when give long gap', () => { const { tuples, remainingGap } = getRuleRangeTuples({ - logger: mockLogger, previousStartedAt: moment().subtract(65, 's').toDate(), // 64 is 5 times the interval + lookback, which will trigger max lookback startedAt: moment().toDate(), interval: '10s', from: 'now-13s', to: 'now', maxSignals: 20, - buildRuleMessage, + ruleExecutionLogger, }); expect(tuples.length).toEqual(5); tuples.forEach((item, index) => { @@ -522,14 +511,13 @@ describe('utils', () => { test('should return a single tuple when give a negative gap (rule ran sooner than expected)', () => { const { tuples, remainingGap } = getRuleRangeTuples({ - logger: mockLogger, previousStartedAt: moment().subtract(-15, 's').toDate(), startedAt: moment().subtract(-15, 's').toDate(), interval: '10s', from: 'now-13s', to: 'now', maxSignals: 20, - buildRuleMessage, + ruleExecutionLogger, }); expect(tuples.length).toEqual(1); const someTuple = tuples[0]; @@ -651,8 +639,6 @@ describe('utils', () => { }, }, }; - const ruleExecutionLogger = ruleExecutionLogMock.forExecutors.create(); - mockLogger.warn.mockClear(); const res = await hasTimestampFields({ timestampField, @@ -662,14 +648,9 @@ describe('utils', () => { >, inputIndices: ['myfa*'], ruleExecutionLogger, - logger: mockLogger, - buildRuleMessage, }); expect(res).toBeTruthy(); - expect(mockLogger.warn).toHaveBeenCalledWith( - 'The following indices are missing the timestamp override field "event.ingested": ["myfakeindex-1","myfakeindex-2"] name: "fake name" id: "fake id" rule id: "fake rule id" signals index: "fakeindex"' - ); expect(ruleExecutionLogger.logStatusChange).toHaveBeenCalledWith({ newStatus: RuleExecutionStatus['partial failure'], message: @@ -702,9 +683,6 @@ describe('utils', () => { }, }; - const ruleExecutionLogger = ruleExecutionLogMock.forExecutors.create(); - mockLogger.warn.mockClear(); - const res = await hasTimestampFields({ timestampField, timestampFieldCapsResponse: timestampFieldCapsResponse as TransportResult< @@ -713,14 +691,9 @@ describe('utils', () => { >, inputIndices: ['myfa*'], ruleExecutionLogger, - logger: mockLogger, - buildRuleMessage, }); expect(res).toBeTruthy(); - expect(mockLogger.warn).toHaveBeenCalledWith( - 'The following indices are missing the timestamp field "@timestamp": ["myfakeindex-1","myfakeindex-2"] name: "fake name" id: "fake id" rule id: "fake rule id" signals index: "fakeindex"' - ); expect(ruleExecutionLogger.logStatusChange).toHaveBeenCalledWith({ newStatus: RuleExecutionStatus['partial failure'], message: @@ -738,10 +711,9 @@ describe('utils', () => { }, }; - const ruleExecutionLogger = ruleExecutionLogMock.forExecutors.create({ + ruleExecutionLogger = ruleExecutionLogMock.forExecutors.create({ ruleName: 'Endpoint Security', }); - mockLogger.warn.mockClear(); const res = await hasTimestampFields({ timestampField, @@ -751,14 +723,9 @@ describe('utils', () => { >, inputIndices: ['logs-endpoint.alerts-*'], ruleExecutionLogger, - logger: mockLogger, - buildRuleMessage, }); expect(res).toBeTruthy(); - expect(mockLogger.warn).toHaveBeenCalledWith( - 'This rule is attempting to query data from Elasticsearch indices listed in the "Index pattern" section of the rule definition, however no index matching: ["logs-endpoint.alerts-*"] was found. This warning will continue to appear until a matching index is created or this rule is disabled. If you have recently enrolled agents enabled with Endpoint Security through Fleet, this warning should stop once an alert is sent from an agent. name: "fake name" id: "fake id" rule id: "fake rule id" signals index: "fakeindex"' - ); expect(ruleExecutionLogger.logStatusChange).toHaveBeenCalledWith({ newStatus: RuleExecutionStatus['partial failure'], message: @@ -777,12 +744,10 @@ describe('utils', () => { }; // SUT uses rule execution logger's context to check the rule name - const ruleExecutionLogger = ruleExecutionLogMock.forExecutors.create({ + ruleExecutionLogger = ruleExecutionLogMock.forExecutors.create({ ruleName: 'NOT Endpoint Security', }); - mockLogger.warn.mockClear(); - const res = await hasTimestampFields({ timestampField, timestampFieldCapsResponse: timestampFieldCapsResponse as TransportResult< @@ -791,14 +756,9 @@ describe('utils', () => { >, inputIndices: ['logs-endpoint.alerts-*'], ruleExecutionLogger, - logger: mockLogger, - buildRuleMessage, }); expect(res).toBeTruthy(); - expect(mockLogger.warn).toHaveBeenCalledWith( - 'This rule is attempting to query data from Elasticsearch indices listed in the "Index pattern" section of the rule definition, however no index matching: ["logs-endpoint.alerts-*"] was found. This warning will continue to appear until a matching index is created or this rule is disabled. name: "fake name" id: "fake id" rule id: "fake rule id" signals index: "fakeindex"' - ); expect(ruleExecutionLogger.logStatusChange).toHaveBeenCalledWith({ newStatus: RuleExecutionStatus['partial failure'], message: diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts index c75c8f1a1c125..6fc3708db6b98 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts @@ -22,7 +22,6 @@ import type { import type { ElasticsearchClient, IUiSettingsClient, - Logger, SavedObjectsClientContract, } from '@kbn/core/server'; import type { @@ -36,7 +35,7 @@ import type { TimestampOverride, Privilege, } from '../../../../common/detection_engine/schemas/common'; -import { RuleExecutionStatus } from '../../../../common/detection_engine/schemas/common'; +import { RuleExecutionStatus } from '../../../../common/detection_engine/rule_monitoring'; import type { BulkResponseErrorAggregation, SignalHit, @@ -50,7 +49,6 @@ import type { SimpleHit, WrappedEventHit, } from './types'; -import type { BuildRuleMessage } from './rule_messages'; import type { ShardError } from '../../types'; import type { EqlRuleParams, @@ -61,7 +59,7 @@ import type { ThresholdRuleParams, } from '../schemas/rule_schemas'; import type { BaseHit, SearchTypes } from '../../../../common/detection_engine/types'; -import type { IRuleExecutionLogForExecutors } from '../rule_execution_log'; +import type { IRuleExecutionLogForExecutors } from '../rule_monitoring'; import { withSecuritySpan } from '../../../utils/with_security_span'; import type { DetectionAlert } from '../../../../common/detection_engine/schemas/alerts'; import { ENABLE_CCS_READ_WARNING_SETTING } from '../../../../common/constants'; @@ -70,12 +68,10 @@ export const MAX_RULE_GAP_RATIO = 4; export const hasReadIndexPrivileges = async (args: { privileges: Privilege; - logger: Logger; - buildRuleMessage: BuildRuleMessage; ruleExecutionLogger: IRuleExecutionLogForExecutors; uiSettingsClient: IUiSettingsClient; }): Promise => { - const { privileges, logger, buildRuleMessage, ruleExecutionLogger, uiSettingsClient } = args; + const { privileges, ruleExecutionLogger, uiSettingsClient } = args; const isCcsPermissionWarningEnabled = await uiSettingsClient.get(ENABLE_CCS_READ_WARNING_SETTING); @@ -89,19 +85,16 @@ export const hasReadIndexPrivileges = async (args: { (indexName) => privileges.index[indexName].read ); + // Some indices have read privileges others do not. if (indexesWithNoReadPrivileges.length > 0) { - // some indices have read privileges others do not. - // set a warning status - const errorString = `This rule may not have the required read privileges to the following indices/index patterns: ${JSON.stringify( - indexesWithNoReadPrivileges - )}`; - logger.warn(buildRuleMessage(errorString)); + const indexesString = JSON.stringify(indexesWithNoReadPrivileges); await ruleExecutionLogger.logStatusChange({ newStatus: RuleExecutionStatus['partial failure'], - message: errorString, + message: `This rule may not have the required read privileges to the following indices/index patterns: ${indexesString}`, }); return true; } + return false; }; @@ -113,18 +106,8 @@ export const hasTimestampFields = async (args: { timestampFieldCapsResponse: TransportResult, unknown>; inputIndices: string[]; ruleExecutionLogger: IRuleExecutionLogForExecutors; - logger: Logger; - buildRuleMessage: BuildRuleMessage; }): Promise => { - const { - timestampField, - timestampFieldCapsResponse, - inputIndices, - ruleExecutionLogger, - logger, - buildRuleMessage, - } = args; - + const { timestampField, timestampFieldCapsResponse, inputIndices, ruleExecutionLogger } = args; const { ruleName } = ruleExecutionLogger.context; if (isEmpty(timestampFieldCapsResponse.body.indices)) { @@ -135,11 +118,12 @@ export const hasTimestampFields = async (args: { ? 'If you have recently enrolled agents enabled with Endpoint Security through Fleet, this warning should stop once an alert is sent from an agent.' : '' }`; - logger.warn(buildRuleMessage(errorString.trimEnd())); + await ruleExecutionLogger.logStatusChange({ newStatus: RuleExecutionStatus['partial failure'], message: errorString.trimEnd(), }); + return true; } else if ( isEmpty(timestampFieldCapsResponse.body.fields) || @@ -159,7 +143,6 @@ export const hasTimestampFields = async (args: { : timestampFieldCapsResponse.body.fields[timestampField]?.unmapped?.indices )}`; - logger.warn(buildRuleMessage(errorString)); await ruleExecutionLogger.logStatusChange({ newStatus: RuleExecutionStatus['partial failure'], message: errorString, @@ -167,6 +150,7 @@ export const hasTimestampFields = async (args: { return true; } + return false; }; @@ -413,29 +397,28 @@ export const errorAggregator = ( }; export const getRuleRangeTuples = ({ - logger, + startedAt, previousStartedAt, from, to, interval, maxSignals, - buildRuleMessage, - startedAt, + ruleExecutionLogger, }: { - logger: Logger; + startedAt: Date; previousStartedAt: Date | null | undefined; from: string; to: string; interval: string; maxSignals: number; - buildRuleMessage: BuildRuleMessage; - startedAt: Date; + ruleExecutionLogger: IRuleExecutionLogForExecutors; }) => { - const originalTo = dateMath.parse(to, { forceNow: startedAt }); const originalFrom = dateMath.parse(from, { forceNow: startedAt }); - if (originalTo == null || originalFrom == null) { - throw new Error(buildRuleMessage('dateMath parse failed')); + const originalTo = dateMath.parse(to, { forceNow: startedAt }); + if (originalFrom == null || originalTo == null) { + throw new Error('Failed to parse date math of rule.from or rule.to'); } + const tuples = [ { to: originalTo, @@ -443,11 +426,15 @@ export const getRuleRangeTuples = ({ maxSignals, }, ]; + const intervalDuration = parseInterval(interval); if (intervalDuration == null) { - logger.error(`Failed to compute gap between rule runs: could not parse rule interval`); + ruleExecutionLogger.error( + 'Failed to compute gap between rule runs: could not parse rule interval' + ); return { tuples, remainingGap: moment.duration(0) }; } + const gap = getGapBetweenRuns({ previousStartedAt, originalTo, @@ -465,13 +452,19 @@ export const getRuleRangeTuples = ({ catchup, intervalDuration, }); + tuples.push(...catchupTuples); + // Each extra tuple adds one extra intervalDuration to the time range this rule will cover. const remainingGapMilliseconds = Math.max( gap.asMilliseconds() - catchup * intervalDuration.asMilliseconds(), 0 ); - return { tuples: tuples.reverse(), remainingGap: moment.duration(remainingGapMilliseconds) }; + + return { + tuples: tuples.reverse(), + remainingGap: moment.duration(remainingGapMilliseconds), + }; }; /** diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/__mocks__/index.ts b/x-pack/plugins/security_solution/server/lib/telemetry/__mocks__/index.ts index eba224f477db4..3c881133ff873 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/__mocks__/index.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/__mocks__/index.ts @@ -14,6 +14,7 @@ import type { SecurityTelemetryTaskConfig } from '../task'; import type { PackagePolicy } from '@kbn/fleet-plugin/common/types/models/package_policy'; import { stubEndpointAlertResponse, stubProcessTree, stubFetchTimelineEvents } from './timeline'; import { stubEndpointMetricsResponse } from './metrics'; +import { prebuiltRuleAlertsResponse } from './prebuilt_rule_alerts'; export const createMockTelemetryEventsSender = ( enableTelemetry?: boolean, @@ -85,6 +86,7 @@ export const createMockTelemetryReceiver = ( fetchDiagnosticAlerts: jest.fn().mockReturnValue(diagnosticsAlert ?? jest.fn()), fetchEndpointMetrics: jest.fn().mockReturnValue(stubEndpointMetricsResponse), fetchEndpointPolicyResponses: jest.fn(), + fetchPrebuiltRuleAlerts: jest.fn().mockReturnValue(prebuiltRuleAlertsResponse), fetchTrustedApplications: jest.fn(), fetchEndpointList: jest.fn(), fetchDetectionRules: jest.fn().mockReturnValue({ body: null }), diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/__mocks__/prebuilt_rule_alerts.ts b/x-pack/plugins/security_solution/server/lib/telemetry/__mocks__/prebuilt_rule_alerts.ts new file mode 100644 index 0000000000000..c8a44a68ce7c1 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/telemetry/__mocks__/prebuilt_rule_alerts.ts @@ -0,0 +1,3377 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const prebuiltRuleAlertsResponse = { + events: [ + { + 'kibana.alert.severity': 'medium', + agent: { + id: '465cb546-3483-4d60-976a-6df687597fab', + type: 'endpoint', + version: '7.12.15', + }, + 'kibana.alert.rule.references': [], + 'kibana.alert.rule.updated_by': 'elastic2', + 'event.category': 'malware', + 'kibana.alert.rule.threat': [], + 'kibana.alert.rule.rule_name_override': 'message', + 'kibana.alert.original_event.code': 'memory_signature', + 'kibana.alert.rule.description': + 'Generates a detection alert each time an Elastic Endpoint Security alert is received. Enabling this rule allows you to immediately begin investigating your Endpoint alerts.', + 'kibana.alert.rule.tags': ['Elastic', 'Endpoint Security'], + 'kibana.alert.rule.producer': 'siem', + 'kibana.alert.rule.to': 'now', + 'kibana.alert.rule.created_by': 'elastic2', + 'kibana.alert.rule.timestamp_override': 'event.ingested', + 'kibana.alert.original_event.ingested': '2022-07-18T15:05:03Z', + Endpoint: { + capabilities: [], + configuration: { + isolation: false, + }, + state: { + isolation: false, + }, + status: 'enrolled', + policy: { + applied: { + name: 'Default', + id: '00000000-0000-0000-0000-000000000000', + endpoint_policy_version: 1, + version: 3, + status: 'warning', + }, + }, + }, + ecs: { + version: '1.6.0', + }, + 'kibana.alert.risk_score': 47, + host: { + hostname: 'Host-v3mqhhvt7z', + os: { + Ext: { + variant: 'Windows Server', + }, + name: 'Windows', + family: 'windows', + version: '10.0', + platform: 'Windows', + full: 'Windows Server 2016', + }, + ip: ['10.54.27.117'], + name: 'Host-v3mqhhvt7z', + id: 'c97faec9-1f07-4052-9900-977946b22816', + mac: ['c9-e1-b9-3f-fc-5c'], + architecture: 'n467gryzu6', + }, + 'kibana.alert.rule.name': 'Endpoint Security', + 'event.agent_id_status': 'auth_metadata_missing', + 'event.kind': 'signal', + 'kibana.alert.original_event.id': 'b9623ec1-bbab-4d2c-9e4e-3a523f1b7b9e', + 'kibana.alert.workflow_status': 'open', + 'kibana.alert.rule.uuid': '1a6dbdc0-06aa-11ed-b2d1-8f870d1699ab', + 'kibana.alert.original_event.category': 'malware', + 'kibana.alert.rule.risk_score_mapping': [ + { + field: 'event.risk_score', + value: '', + operator: 'equals', + }, + ], + 'kibana.alert.original_event.module': 'endpoint', + 'kibana.alert.rule.interval': '5m', + 'kibana.alert.reason': + 'malware event with process iexlorer.exe, on Host-v3mqhhvt7z created medium alert Endpoint Security.', + 'kibana.alert.rule.type': 'query', + 'kibana.alert.rule.immutable': true, + 'event.code': 'memory_signature', + 'kibana.alert.original_event.type': 'info', + 'kibana.alert.depth': 1, + 'kibana.alert.rule.enabled': true, + 'kibana.alert.rule.version': 3, + 'kibana.alert.rule.from': 'now-10m', + 'kibana.alert.rule.parameters': { + severity_mapping: [ + { + severity: 'low', + field: 'event.severity', + value: '21', + operator: 'equals', + }, + { + severity: 'medium', + field: 'event.severity', + value: '47', + operator: 'equals', + }, + { + severity: 'high', + field: 'event.severity', + value: '73', + operator: 'equals', + }, + { + severity: 'critical', + field: 'event.severity', + value: '99', + operator: 'equals', + }, + ], + references: [], + description: + 'Generates a detection alert each time an Elastic Endpoint Security alert is received. Enabling this rule allows you to immediately begin investigating your Endpoint alerts.', + language: 'kuery', + type: 'query', + rule_name_override: 'message', + exceptions_list: [ + { + list_id: 'endpoint_list', + namespace_type: 'agnostic', + id: 'endpoint_list', + type: 'endpoint', + }, + ], + timestamp_override: 'event.ingested', + from: 'now-10m', + severity: 'medium', + max_signals: 10000, + risk_score: 47, + risk_score_mapping: [ + { + field: 'event.risk_score', + value: '', + operator: 'equals', + }, + ], + author: ['Elastic'], + query: 'event.kind:alert and event.module:(endpoint and not endgame)\n', + index: ['logs-endpoint.alerts-*'], + version: 3, + rule_id: '9a1a2dae-0b5f-4c3d-8305-a268d404c306', + license: 'Elastic License v2', + required_fields: [], + immutable: true, + related_integrations: [], + setup: '', + false_positives: [], + threat: [], + to: 'now', + }, + 'kibana.alert.status': 'active', + 'kibana.alert.ancestors': [ + { + depth: 0, + index: '.ds-logs-endpoint.alerts-default-2022.07.18-000001', + id: 'WnXXEYIBRKQBoH3JXndW', + type: 'event', + }, + ], + 'kibana.alert.original_event.dataset': 'endpoint', + 'kibana.alert.rule.exceptions_list': [ + { + list_id: 'endpoint_list', + namespace_type: 'agnostic', + id: 'endpoint_list', + type: 'endpoint', + }, + ], + 'kibana.alert.rule.actions': [], + 'event.sequence': 610, + 'kibana.alert.rule.rule_type_id': 'siem.queryRule', + file: {}, + 'event.module': 'endpoint', + 'kibana.alert.rule.license': 'Elastic License v2', + 'kibana.alert.original_event.kind': 'alert', + 'kibana.alert.rule.severity_mapping': [ + { + severity: 'low', + field: 'event.severity', + value: '21', + operator: 'equals', + }, + { + severity: 'medium', + field: 'event.severity', + value: '47', + operator: 'equals', + }, + { + severity: 'high', + field: 'event.severity', + value: '73', + operator: 'equals', + }, + { + severity: 'critical', + field: 'event.severity', + value: '99', + operator: 'equals', + }, + ], + process: { + Ext: { + malware_signature: { + identifier: 'diagnostic-malware-signature-v1-fake', + all_names: 'Windows.Trojan.FakeAgent', + }, + ancestry: ['1jx51k3ycb', '3ckuzyk9ei'], + code_signature: [ + { + trusted: false, + subject_name: 'bad signer', + }, + ], + user: 'SYSTEM', + token: { + integrity_level_name: 'high', + }, + }, + parent: { + pid: 1, + entity_id: '1jx51k3ycb', + }, + group_leader: { + name: 'fake leader', + pid: 849, + entity_id: 'ah4nfw1r7k', + }, + session_leader: { + name: 'fake session', + pid: 369, + entity_id: 'ah4nfw1r7k', + }, + entry_leader: { + name: 'fake entry', + pid: 125, + entity_id: 'ah4nfw1r7k', + }, + name: 'iexlorer.exe', + start: 1658158926265, + pid: 2, + entity_id: 'fur1qcerz4', + executable: 'C:/fake/iexlorer.exe', + hash: { + sha1: 'fake sha1', + sha256: 'fake sha256', + md5: 'fake md5', + }, + uptime: 0, + }, + 'kibana.alert.rule.max_signals': 10000, + dll: [ + { + Ext: { + compile_time: 1534424710, + malware_classification: { + identifier: 'Whitelisted', + score: 0, + threshold: 0, + version: '3.0.0', + }, + mapped_address: 5362483200, + mapped_size: 0, + }, + path: 'C:\\Program Files\\Cybereason ActiveProbe\\AmSvc.exe', + code_signature: { + trusted: true, + subject_name: 'Cybereason Inc', + }, + pe: { + architecture: 'x64', + }, + hash: { + sha1: 'ca85243c0af6a6471bdaa560685c51eefd6dbc0d', + sha256: '8ad40c90a611d36eb8f9eb24fa04f7dbca713db383ff55a03aa0f382e92061a2', + md5: '1f2d082566b0fc5f2c238a5180db7451', + }, + }, + ], + 'kibana.alert.rule.updated_at': '2022-07-18T14:58:38.295Z', + 'kibana.alert.rule.risk_score': 47, + 'kibana.alert.rule.author': ['Elastic'], + 'kibana.alert.rule.false_positives': [], + 'kibana.alert.rule.consumer': 'siem', + 'kibana.alert.rule.indices': ['logs-endpoint.alerts-*'], + 'kibana.alert.rule.category': 'Custom Query Rule', + 'kibana.alert.original_event.sequence': 610, + 'event.ingested': '2022-07-18T15:05:03Z', + 'event.action': 'start', + '@timestamp': '2022-07-18T15:08:48.063Z', + 'kibana.alert.rule.created_at': '2022-07-18T14:58:38.295Z', + 'kibana.alert.original_event.action': 'start', + 'kibana.alert.rule.severity': 'medium', + data_stream: { + namespace: 'default', + type: 'logs', + dataset: 'endpoint.alerts', + }, + 'event.type': 'info', + 'kibana.alert.original_event.agent_id_status': 'auth_metadata_missing', + elastic: { + agent: { + id: '465cb546-3483-4d60-976a-6df687597fab', + }, + }, + 'kibana.alert.rule.execution.uuid': '455933fc-319b-444b-a6c1-b7df2532634b', + 'kibana.space_ids': ['default'], + 'kibana.alert.uuid': 'ef7fcd81ddfa218aa5e5cd1d4e80fb07fed5806489f26084d759fb42e4e2f25c', + 'kibana.version': '8.4.0', + 'event.id': 'b9623ec1-bbab-4d2c-9e4e-3a523f1b7b9e', + 'event.dataset': 'endpoint', + 'kibana.alert.original_time': '2022-07-18T15:42:06.265Z', + 'kibana.alert.rule.rule_id': '9a1a2dae-0b5f-4c3d-8305-a268d404c306', + }, + { + 'kibana.alert.severity': 'medium', + agent: { + id: '465cb546-3483-4d60-976a-6df687597fab', + type: 'endpoint', + version: '7.12.15', + }, + 'kibana.alert.rule.references': [], + 'kibana.alert.rule.updated_by': 'elastic2', + 'event.category': 'behavior', + 'kibana.alert.rule.threat': [], + 'kibana.alert.rule.rule_name_override': 'message', + 'kibana.alert.original_event.code': 'behavior', + 'kibana.alert.rule.description': + 'Generates a detection alert each time an Elastic Endpoint Security alert is received. Enabling this rule allows you to immediately begin investigating your Endpoint alerts.', + source: { + port: 59406, + ip: '10.13.177.247', + }, + 'kibana.alert.rule.tags': ['Elastic', 'Endpoint Security'], + 'kibana.alert.rule.producer': 'siem', + 'kibana.alert.rule.to': 'now', + 'kibana.alert.rule.created_by': 'elastic2', + 'kibana.alert.rule.timestamp_override': 'event.ingested', + 'kibana.alert.original_event.ingested': '2022-07-18T15:05:03Z', + Endpoint: { + capabilities: [], + configuration: { + isolation: false, + }, + state: { + isolation: false, + }, + status: 'enrolled', + policy: { + applied: { + name: 'Default', + id: '00000000-0000-0000-0000-000000000000', + endpoint_policy_version: 1, + version: 3, + status: 'warning', + }, + }, + }, + ecs: { + version: '1.6.0', + }, + 'kibana.alert.risk_score': 47, + host: { + hostname: 'Host-v3mqhhvt7z', + os: { + Ext: { + variant: 'Windows Server', + }, + name: 'Windows', + family: 'windows', + version: '10.0', + platform: 'Windows', + full: 'Windows Server 2016', + }, + ip: ['10.54.27.117'], + name: 'Host-v3mqhhvt7z', + id: 'c97faec9-1f07-4052-9900-977946b22816', + mac: ['c9-e1-b9-3f-fc-5c'], + architecture: 'n467gryzu6', + }, + 'kibana.alert.rule.name': 'Endpoint Security', + 'event.agent_id_status': 'auth_metadata_missing', + 'event.kind': 'signal', + 'kibana.alert.original_event.id': '0ea9b324-7bb6-4512-85c7-ac4ab7cd8080', + 'kibana.alert.workflow_status': 'open', + 'kibana.alert.rule.uuid': '1a6dbdc0-06aa-11ed-b2d1-8f870d1699ab', + 'kibana.alert.original_event.category': 'behavior', + 'kibana.alert.rule.risk_score_mapping': [ + { + field: 'event.risk_score', + value: '', + operator: 'equals', + }, + ], + 'kibana.alert.original_event.module': 'endpoint', + 'kibana.alert.rule.interval': '5m', + 'kibana.alert.reason': + 'behavior event with process notepad.exe, file fake_behavior.exe,:59406,:443, on Host-v3mqhhvt7z created medium alert Endpoint Security.', + 'kibana.alert.rule.type': 'query', + 'kibana.alert.rule.immutable': true, + 'event.code': 'behavior', + 'kibana.alert.original_event.type': 'info', + 'kibana.alert.depth': 1, + 'kibana.alert.rule.enabled': true, + 'kibana.alert.rule.version': 3, + 'kibana.alert.rule.from': 'now-10m', + 'kibana.alert.rule.parameters': { + severity_mapping: [ + { + severity: 'low', + field: 'event.severity', + value: '21', + operator: 'equals', + }, + { + severity: 'medium', + field: 'event.severity', + value: '47', + operator: 'equals', + }, + { + severity: 'high', + field: 'event.severity', + value: '73', + operator: 'equals', + }, + { + severity: 'critical', + field: 'event.severity', + value: '99', + operator: 'equals', + }, + ], + references: [], + description: + 'Generates a detection alert each time an Elastic Endpoint Security alert is received. Enabling this rule allows you to immediately begin investigating your Endpoint alerts.', + language: 'kuery', + type: 'query', + rule_name_override: 'message', + exceptions_list: [ + { + list_id: 'endpoint_list', + namespace_type: 'agnostic', + id: 'endpoint_list', + type: 'endpoint', + }, + ], + timestamp_override: 'event.ingested', + from: 'now-10m', + severity: 'medium', + max_signals: 10000, + risk_score: 47, + risk_score_mapping: [ + { + field: 'event.risk_score', + value: '', + operator: 'equals', + }, + ], + author: ['Elastic'], + query: 'event.kind:alert and event.module:(endpoint and not endgame)\n', + index: ['logs-endpoint.alerts-*'], + version: 3, + rule_id: '9a1a2dae-0b5f-4c3d-8305-a268d404c306', + license: 'Elastic License v2', + required_fields: [], + immutable: true, + related_integrations: [], + setup: '', + false_positives: [], + threat: [], + to: 'now', + }, + 'kibana.alert.status': 'active', + destination: { + port: 443, + ip: '10.232.197.71', + }, + 'kibana.alert.ancestors': [ + { + depth: 0, + index: '.ds-logs-endpoint.alerts-default-2022.07.18-000001', + id: 'hHXXEYIBRKQBoH3JXnVV', + type: 'event', + }, + ], + rule: { + description: 'Behavior rule description', + id: '252955d3-a777-431d-a015-d4a70e2b68bc', + }, + 'kibana.alert.original_event.dataset': 'endpoint.diagnostic.collection', + 'kibana.alert.rule.exceptions_list': [ + { + list_id: 'endpoint_list', + namespace_type: 'agnostic', + id: 'endpoint_list', + type: 'endpoint', + }, + ], + 'kibana.alert.rule.actions': [], + network: { + transport: 'tcp', + type: 'ipv4', + direction: 'outgoing', + }, + 'event.sequence': 140, + 'kibana.alert.rule.rule_type_id': 'siem.queryRule', + file: { + path: 'C:/fake_behavior.exe', + name: 'fake_behavior.exe', + }, + 'event.module': 'endpoint', + 'kibana.alert.rule.license': 'Elastic License v2', + 'kibana.alert.original_event.kind': 'alert', + registry: { + path: 'HKEY_USERS\\S-1-5-21-2460036010-3910878774-3458087990-1001\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run\\chrome', + data: { + strings: 'C:/fake_behavior/notepad.exe', + }, + value: 'notepad.exe', + }, + 'kibana.alert.rule.severity_mapping': [ + { + severity: 'low', + field: 'event.severity', + value: '21', + operator: 'equals', + }, + { + severity: 'medium', + field: 'event.severity', + value: '47', + operator: 'equals', + }, + { + severity: 'high', + field: 'event.severity', + value: '73', + operator: 'equals', + }, + { + severity: 'critical', + field: 'event.severity', + value: '99', + operator: 'equals', + }, + ], + process: { + Ext: { + ancestry: ['kdtntqj9es', 'yno4ha0v7c'], + code_signature: [ + { + trusted: false, + subject_name: 'bad signer', + }, + ], + user: 'SYSTEM', + token: { + integrity_level_name: 'high', + elevation_level: 'full', + }, + }, + parent: { + pid: 1, + entity_id: 'kdtntqj9es', + }, + group_leader: { + name: 'fake leader', + pid: 5, + entity_id: '76xj16xt4f', + }, + session_leader: { + name: 'fake session', + pid: 291, + entity_id: '76xj16xt4f', + }, + code_signature: { + subject_name: 'Microsoft Windows', + status: 'trusted', + }, + entry_leader: { + name: 'fake entry', + pid: 202, + entity_id: '76xj16xt4f', + }, + name: 'notepad.exe', + pid: 2, + entity_id: 'ebdnts2ebu', + executable: 'C:/fake_behavior/notepad.exe', + }, + 'kibana.alert.rule.max_signals': 10000, + dll: [ + { + Ext: { + compile_time: 1534424710, + malware_classification: { + identifier: 'Whitelisted', + score: 0, + threshold: 0, + version: '3.0.0', + }, + mapped_address: 5362483200, + mapped_size: 0, + }, + path: 'C:\\Program Files\\Cybereason ActiveProbe\\AmSvc.exe', + code_signature: { + trusted: true, + subject_name: 'Cybereason Inc', + }, + pe: { + architecture: 'x64', + }, + hash: { + sha1: 'ca85243c0af6a6471bdaa560685c51eefd6dbc0d', + sha256: '8ad40c90a611d36eb8f9eb24fa04f7dbca713db383ff55a03aa0f382e92061a2', + md5: '1f2d082566b0fc5f2c238a5180db7451', + }, + }, + ], + 'kibana.alert.rule.updated_at': '2022-07-18T14:58:38.295Z', + 'kibana.alert.rule.risk_score': 47, + 'kibana.alert.rule.author': ['Elastic'], + 'kibana.alert.rule.false_positives': [], + 'kibana.alert.rule.consumer': 'siem', + 'kibana.alert.rule.indices': ['logs-endpoint.alerts-*'], + 'kibana.alert.rule.category': 'Custom Query Rule', + 'kibana.alert.original_event.sequence': 140, + 'event.ingested': '2022-07-18T15:05:03Z', + 'event.action': 'rule_detection', + '@timestamp': '2022-07-18T15:08:48.063Z', + 'kibana.alert.rule.created_at': '2022-07-18T14:58:38.295Z', + 'kibana.alert.original_event.action': 'rule_detection', + 'kibana.alert.rule.severity': 'medium', + data_stream: { + namespace: 'default', + type: 'logs', + dataset: 'endpoint.alerts', + }, + 'event.type': 'info', + 'kibana.alert.original_event.agent_id_status': 'auth_metadata_missing', + elastic: { + agent: { + id: '465cb546-3483-4d60-976a-6df687597fab', + }, + }, + 'kibana.alert.rule.execution.uuid': '455933fc-319b-444b-a6c1-b7df2532634b', + 'kibana.space_ids': ['default'], + 'kibana.alert.uuid': '5f0b38ac1b3652867f73b688b8186b4e7803a9dd2fcf916744afb7381e58d015', + 'kibana.version': '8.4.0', + 'event.id': '0ea9b324-7bb6-4512-85c7-ac4ab7cd8080', + 'event.dataset': 'endpoint.diagnostic.collection', + 'kibana.alert.original_time': '2022-07-18T15:47:37.258Z', + 'kibana.alert.rule.rule_id': '9a1a2dae-0b5f-4c3d-8305-a268d404c306', + }, + { + 'kibana.alert.severity': 'medium', + agent: { + id: '465cb546-3483-4d60-976a-6df687597fab', + type: 'endpoint', + version: '7.12.15', + }, + 'kibana.alert.rule.references': [], + 'kibana.alert.rule.updated_by': 'elastic2', + 'event.category': 'malware', + 'kibana.alert.rule.threat': [], + 'kibana.alert.rule.rule_name_override': 'message', + 'kibana.alert.original_event.code': 'shellcode_thread', + 'kibana.alert.rule.description': + 'Generates a detection alert each time an Elastic Endpoint Security alert is received. Enabling this rule allows you to immediately begin investigating your Endpoint alerts.', + 'kibana.alert.rule.tags': ['Elastic', 'Endpoint Security'], + 'kibana.alert.rule.producer': 'siem', + 'kibana.alert.rule.to': 'now', + 'kibana.alert.rule.created_by': 'elastic2', + 'kibana.alert.rule.timestamp_override': 'event.ingested', + 'kibana.alert.original_event.ingested': '2022-07-18T15:05:03Z', + Endpoint: { + capabilities: [], + configuration: { + isolation: false, + }, + state: { + isolation: false, + }, + status: 'enrolled', + policy: { + applied: { + name: 'Default', + id: '00000000-0000-0000-0000-000000000000', + endpoint_policy_version: 1, + version: 3, + status: 'warning', + }, + }, + }, + ecs: { + version: '1.6.0', + }, + 'kibana.alert.risk_score': 47, + host: { + hostname: 'Host-v3mqhhvt7z', + os: { + Ext: { + variant: 'Windows Server', + }, + name: 'Windows', + family: 'windows', + version: '10.0', + platform: 'Windows', + full: 'Windows Server 2016', + }, + ip: ['10.54.27.117'], + name: 'Host-v3mqhhvt7z', + id: 'c97faec9-1f07-4052-9900-977946b22816', + mac: ['c9-e1-b9-3f-fc-5c'], + architecture: 'n467gryzu6', + }, + 'kibana.alert.rule.name': 'Endpoint Security', + 'event.agent_id_status': 'auth_metadata_missing', + 'event.kind': 'signal', + 'kibana.alert.original_event.id': 'd0bf61e0-94b6-4cdd-8a17-11ea0d440961', + 'kibana.alert.workflow_status': 'open', + 'kibana.alert.rule.uuid': '1a6dbdc0-06aa-11ed-b2d1-8f870d1699ab', + 'kibana.alert.original_event.category': 'malware', + 'kibana.alert.rule.risk_score_mapping': [ + { + field: 'event.risk_score', + value: '', + operator: 'equals', + }, + ], + 'kibana.alert.original_event.module': 'endpoint', + 'kibana.alert.rule.interval': '5m', + 'kibana.alert.reason': + 'malware event with process explorer.exe, on Host-v3mqhhvt7z created medium alert Endpoint Security.', + 'kibana.alert.rule.type': 'query', + 'kibana.alert.rule.immutable': true, + 'event.code': 'shellcode_thread', + 'kibana.alert.original_event.type': 'info', + 'kibana.alert.depth': 1, + 'kibana.alert.rule.enabled': true, + 'kibana.alert.rule.version': 3, + 'kibana.alert.rule.from': 'now-10m', + 'kibana.alert.rule.parameters': { + severity_mapping: [ + { + severity: 'low', + field: 'event.severity', + value: '21', + operator: 'equals', + }, + { + severity: 'medium', + field: 'event.severity', + value: '47', + operator: 'equals', + }, + { + severity: 'high', + field: 'event.severity', + value: '73', + operator: 'equals', + }, + { + severity: 'critical', + field: 'event.severity', + value: '99', + operator: 'equals', + }, + ], + references: [], + description: + 'Generates a detection alert each time an Elastic Endpoint Security alert is received. Enabling this rule allows you to immediately begin investigating your Endpoint alerts.', + language: 'kuery', + type: 'query', + rule_name_override: 'message', + exceptions_list: [ + { + list_id: 'endpoint_list', + namespace_type: 'agnostic', + id: 'endpoint_list', + type: 'endpoint', + }, + ], + timestamp_override: 'event.ingested', + from: 'now-10m', + severity: 'medium', + max_signals: 10000, + risk_score: 47, + risk_score_mapping: [ + { + field: 'event.risk_score', + value: '', + operator: 'equals', + }, + ], + author: ['Elastic'], + query: 'event.kind:alert and event.module:(endpoint and not endgame)\n', + index: ['logs-endpoint.alerts-*'], + version: 3, + rule_id: '9a1a2dae-0b5f-4c3d-8305-a268d404c306', + license: 'Elastic License v2', + required_fields: [], + immutable: true, + related_integrations: [], + setup: '', + false_positives: [], + threat: [], + to: 'now', + }, + 'kibana.alert.status': 'active', + 'kibana.alert.ancestors': [ + { + depth: 0, + index: '.ds-logs-endpoint.alerts-default-2022.07.18-000001', + id: '8nXXEYIBRKQBoH3JXnVV', + type: 'event', + }, + ], + 'kibana.alert.original_event.dataset': 'endpoint', + 'kibana.alert.rule.exceptions_list': [ + { + list_id: 'endpoint_list', + namespace_type: 'agnostic', + id: 'endpoint_list', + type: 'endpoint', + }, + ], + 'kibana.alert.rule.actions': [], + 'event.sequence': 250, + 'kibana.alert.rule.rule_type_id': 'siem.queryRule', + Target: { + process: { + thread: { + Ext: { + start_address_details: { + region_size: 4000, + region_protection: 'RWX', + allocation_type: 'PRIVATE', + memory_pe: { + imphash: 'a hash', + }, + allocation_size: 4000, + }, + start_address_bytes_disasm_hash: 'a disam hash', + start_address_allocation_offset: 0, + }, + }, + }, + }, + file: {}, + 'event.module': 'endpoint', + 'kibana.alert.rule.license': 'Elastic License v2', + 'kibana.alert.original_event.kind': 'alert', + 'kibana.alert.rule.severity_mapping': [ + { + severity: 'low', + field: 'event.severity', + value: '21', + operator: 'equals', + }, + { + severity: 'medium', + field: 'event.severity', + value: '47', + operator: 'equals', + }, + { + severity: 'high', + field: 'event.severity', + value: '73', + operator: 'equals', + }, + { + severity: 'critical', + field: 'event.severity', + value: '99', + operator: 'equals', + }, + ], + process: { + Ext: { + malware_signature: { + identifier: 'diagnostic-malware-signature-v1-fake', + all_names: 'Windows.Trojan.FakeAgent', + }, + ancestry: ['9mdj855xum', '1kmjaboxkt'], + code_signature: [ + { + trusted: false, + subject_name: 'bad signer', + }, + ], + user: 'SYSTEM', + token: { + integrity_level_name: 'high', + }, + }, + parent: { + pid: 1, + entity_id: '9mdj855xum', + }, + group_leader: { + name: 'fake leader', + pid: 64, + entity_id: '7vrnkjm397', + }, + session_leader: { + name: 'fake session', + pid: 30, + entity_id: '7vrnkjm397', + }, + entry_leader: { + name: 'fake entry', + pid: 727, + entity_id: '7vrnkjm397', + }, + name: 'explorer.exe', + start: 1658159275261, + pid: 2, + entity_id: 'urrnlvmn1n', + executable: 'C:/fake/explorer.exe', + hash: { + sha1: 'fake sha1', + sha256: 'fake sha256', + md5: 'fake md5', + }, + uptime: 0, + }, + 'kibana.alert.rule.max_signals': 10000, + dll: [ + { + Ext: { + compile_time: 1534424710, + malware_classification: { + identifier: 'Whitelisted', + score: 0, + threshold: 0, + version: '3.0.0', + }, + mapped_address: 5362483200, + mapped_size: 0, + }, + path: 'C:\\Program Files\\Cybereason ActiveProbe\\AmSvc.exe', + code_signature: { + trusted: true, + subject_name: 'Cybereason Inc', + }, + pe: { + architecture: 'x64', + }, + hash: { + sha1: 'ca85243c0af6a6471bdaa560685c51eefd6dbc0d', + sha256: '8ad40c90a611d36eb8f9eb24fa04f7dbca713db383ff55a03aa0f382e92061a2', + md5: '1f2d082566b0fc5f2c238a5180db7451', + }, + }, + ], + 'kibana.alert.rule.updated_at': '2022-07-18T14:58:38.295Z', + 'kibana.alert.rule.risk_score': 47, + 'kibana.alert.rule.author': ['Elastic'], + 'kibana.alert.rule.false_positives': [], + 'kibana.alert.rule.consumer': 'siem', + 'kibana.alert.rule.indices': ['logs-endpoint.alerts-*'], + 'kibana.alert.rule.category': 'Custom Query Rule', + 'kibana.alert.original_event.sequence': 250, + 'event.ingested': '2022-07-18T15:05:03Z', + 'event.action': 'start', + '@timestamp': '2022-07-18T15:08:48.063Z', + 'kibana.alert.rule.created_at': '2022-07-18T14:58:38.295Z', + 'kibana.alert.original_event.action': 'start', + 'kibana.alert.rule.severity': 'medium', + data_stream: { + namespace: 'default', + type: 'logs', + dataset: 'endpoint.alerts', + }, + 'event.type': 'info', + 'kibana.alert.original_event.agent_id_status': 'auth_metadata_missing', + elastic: { + agent: { + id: '465cb546-3483-4d60-976a-6df687597fab', + }, + }, + 'kibana.alert.rule.execution.uuid': '455933fc-319b-444b-a6c1-b7df2532634b', + 'kibana.space_ids': ['default'], + 'kibana.alert.uuid': '0b37e801e62dea3f145ff9308ea56a34c4a0a3c2398d5cf52911629597514e96', + 'kibana.version': '8.4.0', + 'event.id': 'd0bf61e0-94b6-4cdd-8a17-11ea0d440961', + 'event.dataset': 'endpoint', + 'kibana.alert.original_time': '2022-07-18T15:47:55.261Z', + 'kibana.alert.rule.rule_id': '9a1a2dae-0b5f-4c3d-8305-a268d404c306', + }, + { + 'kibana.alert.severity': 'medium', + agent: { + id: '465cb546-3483-4d60-976a-6df687597fab', + type: 'endpoint', + version: '7.12.15', + }, + 'kibana.alert.rule.references': [], + 'kibana.alert.rule.updated_by': 'elastic2', + 'event.category': 'malware', + 'kibana.alert.rule.threat': [], + 'kibana.alert.rule.rule_name_override': 'message', + 'kibana.alert.original_event.code': 'malicious_file', + 'kibana.alert.rule.description': + 'Generates a detection alert each time an Elastic Endpoint Security alert is received. Enabling this rule allows you to immediately begin investigating your Endpoint alerts.', + 'kibana.alert.rule.tags': ['Elastic', 'Endpoint Security'], + 'kibana.alert.rule.producer': 'siem', + 'kibana.alert.rule.to': 'now', + 'kibana.alert.rule.created_by': 'elastic2', + 'kibana.alert.rule.timestamp_override': 'event.ingested', + 'kibana.alert.original_event.ingested': '2022-07-18T15:05:03Z', + Endpoint: { + capabilities: [], + configuration: { + isolation: false, + }, + state: { + isolation: false, + }, + status: 'enrolled', + policy: { + applied: { + name: 'Default', + id: '00000000-0000-0000-0000-000000000000', + endpoint_policy_version: 1, + version: 3, + status: 'warning', + }, + }, + }, + ecs: { + version: '1.4.0', + }, + 'kibana.alert.risk_score': 47, + host: { + hostname: 'Host-v3mqhhvt7z', + os: { + Ext: { + variant: 'Windows Server', + }, + name: 'Windows', + family: 'windows', + version: '10.0', + platform: 'Windows', + full: 'Windows Server 2016', + }, + ip: ['10.54.27.117'], + name: 'Host-v3mqhhvt7z', + id: 'c97faec9-1f07-4052-9900-977946b22816', + mac: ['c9-e1-b9-3f-fc-5c'], + architecture: 'n467gryzu6', + }, + 'kibana.alert.rule.name': 'Endpoint Security', + 'event.agent_id_status': 'auth_metadata_missing', + 'event.kind': 'signal', + 'kibana.alert.original_event.id': '1b63fd23-74ec-4f27-88a4-c3da8411958d', + 'kibana.alert.workflow_status': 'open', + 'kibana.alert.rule.uuid': '1a6dbdc0-06aa-11ed-b2d1-8f870d1699ab', + 'kibana.alert.original_event.category': 'malware', + 'kibana.alert.rule.risk_score_mapping': [ + { + field: 'event.risk_score', + value: '', + operator: 'equals', + }, + ], + 'kibana.alert.original_event.module': 'endpoint', + 'kibana.alert.rule.interval': '5m', + 'kibana.alert.reason': + 'malware event with process malware writer, file fake_malware.exe, on Host-v3mqhhvt7z created medium alert Endpoint Security.', + 'kibana.alert.rule.type': 'query', + 'kibana.alert.rule.immutable': true, + 'event.code': 'malicious_file', + 'kibana.alert.original_event.type': 'creation', + 'kibana.alert.depth': 1, + 'kibana.alert.rule.enabled': true, + 'kibana.alert.rule.version': 3, + 'kibana.alert.rule.from': 'now-10m', + 'kibana.alert.rule.parameters': { + severity_mapping: [ + { + severity: 'low', + field: 'event.severity', + value: '21', + operator: 'equals', + }, + { + severity: 'medium', + field: 'event.severity', + value: '47', + operator: 'equals', + }, + { + severity: 'high', + field: 'event.severity', + value: '73', + operator: 'equals', + }, + { + severity: 'critical', + field: 'event.severity', + value: '99', + operator: 'equals', + }, + ], + references: [], + description: + 'Generates a detection alert each time an Elastic Endpoint Security alert is received. Enabling this rule allows you to immediately begin investigating your Endpoint alerts.', + language: 'kuery', + type: 'query', + rule_name_override: 'message', + exceptions_list: [ + { + list_id: 'endpoint_list', + namespace_type: 'agnostic', + id: 'endpoint_list', + type: 'endpoint', + }, + ], + timestamp_override: 'event.ingested', + from: 'now-10m', + severity: 'medium', + max_signals: 10000, + risk_score: 47, + risk_score_mapping: [ + { + field: 'event.risk_score', + value: '', + operator: 'equals', + }, + ], + author: ['Elastic'], + query: 'event.kind:alert and event.module:(endpoint and not endgame)\n', + index: ['logs-endpoint.alerts-*'], + version: 3, + rule_id: '9a1a2dae-0b5f-4c3d-8305-a268d404c306', + license: 'Elastic License v2', + required_fields: [], + immutable: true, + related_integrations: [], + setup: '', + false_positives: [], + threat: [], + to: 'now', + }, + 'kibana.alert.status': 'active', + 'kibana.alert.ancestors': [ + { + depth: 0, + index: '.ds-logs-endpoint.alerts-default-2022.07.18-000001', + id: 'DnXXEYIBRKQBoH3JXndW', + type: 'event', + }, + ], + 'kibana.alert.original_event.dataset': 'endpoint', + 'kibana.alert.rule.exceptions_list': [ + { + list_id: 'endpoint_list', + namespace_type: 'agnostic', + id: 'endpoint_list', + type: 'endpoint', + }, + ], + 'kibana.alert.rule.actions': [], + 'event.sequence': 534, + 'kibana.alert.rule.rule_type_id': 'siem.queryRule', + file: { + owner: 'SYSTEM', + Ext: { + temp_file_path: 'C:/temp/fake_malware.exe', + code_signature: [ + { + trusted: false, + subject_name: 'bad signer', + }, + ], + quarantine_message: 'fake quarantine message', + quarantine_result: true, + malware_classification: { + identifier: 'endpointpe', + score: 1, + threshold: 0.66, + version: '3.0.33', + }, + }, + path: 'C:/fake_malware.exe', + size: 3456, + created: 1658159281265, + name: 'fake_malware.exe', + accessed: 1658159281265, + mtime: 1658159281265, + hash: { + sha1: 'fake file sha1', + sha256: 'fake file sha256', + md5: 'fake file md5', + }, + }, + 'event.module': 'endpoint', + 'kibana.alert.rule.license': 'Elastic License v2', + 'kibana.alert.original_event.kind': 'alert', + 'kibana.alert.rule.severity_mapping': [ + { + severity: 'low', + field: 'event.severity', + value: '21', + operator: 'equals', + }, + { + severity: 'medium', + field: 'event.severity', + value: '47', + operator: 'equals', + }, + { + severity: 'high', + field: 'event.severity', + value: '73', + operator: 'equals', + }, + { + severity: 'critical', + field: 'event.severity', + value: '99', + operator: 'equals', + }, + ], + process: { + Ext: { + ancestry: ['oap1xvrjpd', 's4f3nnx6d1'], + code_signature: [ + { + trusted: false, + subject_name: 'bad signer', + }, + ], + user: 'SYSTEM', + token: { + integrity_level: 16384, + privileges: [ + { + name: 'SeAssignPrimaryTokenPrivilege', + description: 'Replace a process level token', + enabled: false, + }, + ], + integrity_level_name: 'system', + domain: 'NT AUTHORITY', + type: 'tokenPrimary', + user: 'SYSTEM', + sid: 'S-1-5-18', + }, + }, + parent: { + pid: 1, + entity_id: 'oap1xvrjpd', + }, + group_leader: { + name: 'fake leader', + pid: 532, + entity_id: 'ah4nfw1r7k', + }, + session_leader: { + name: 'fake session', + pid: 935, + entity_id: 'ah4nfw1r7k', + }, + entry_leader: { + name: 'fake entry', + pid: 510, + entity_id: 'ah4nfw1r7k', + }, + name: 'malware writer', + start: 1658159281265, + pid: 2, + entity_id: '1cl70y16ka', + executable: 'C:/malware.exe', + hash: { + sha1: 'fake sha1', + sha256: 'fake sha256', + md5: 'fake md5', + }, + uptime: 0, + }, + 'kibana.alert.rule.max_signals': 10000, + dll: [ + { + Ext: { + compile_time: 1534424710, + malware_classification: { + identifier: 'Whitelisted', + score: 0, + threshold: 0, + version: '3.0.0', + }, + mapped_address: 5362483200, + mapped_size: 0, + }, + path: 'C:\\Program Files\\Cybereason ActiveProbe\\AmSvc.exe', + code_signature: { + trusted: true, + subject_name: 'Cybereason Inc', + }, + pe: { + architecture: 'x64', + }, + hash: { + sha1: 'ca85243c0af6a6471bdaa560685c51eefd6dbc0d', + sha256: '8ad40c90a611d36eb8f9eb24fa04f7dbca713db383ff55a03aa0f382e92061a2', + md5: '1f2d082566b0fc5f2c238a5180db7451', + }, + }, + ], + 'kibana.alert.rule.updated_at': '2022-07-18T14:58:38.295Z', + 'kibana.alert.rule.risk_score': 47, + 'kibana.alert.rule.author': ['Elastic'], + 'kibana.alert.rule.false_positives': [], + 'kibana.alert.rule.consumer': 'siem', + 'kibana.alert.rule.indices': ['logs-endpoint.alerts-*'], + 'kibana.alert.rule.category': 'Custom Query Rule', + 'kibana.alert.original_event.sequence': 534, + 'event.ingested': '2022-07-18T15:05:03Z', + 'event.action': 'deletion', + '@timestamp': '2022-07-18T15:08:48.064Z', + 'kibana.alert.rule.created_at': '2022-07-18T14:58:38.295Z', + 'kibana.alert.original_event.action': 'deletion', + 'kibana.alert.rule.severity': 'medium', + data_stream: { + namespace: 'default', + type: 'logs', + dataset: 'endpoint.alerts', + }, + 'event.type': 'creation', + 'kibana.alert.original_event.agent_id_status': 'auth_metadata_missing', + elastic: { + agent: { + id: '465cb546-3483-4d60-976a-6df687597fab', + }, + }, + 'kibana.alert.rule.execution.uuid': '455933fc-319b-444b-a6c1-b7df2532634b', + 'kibana.space_ids': ['default'], + 'kibana.alert.uuid': '245b782b0be20d2e0bad3b230c692e44a4454b3bc6cde7cca5216af8d2b76c24', + 'kibana.version': '8.4.0', + 'event.id': '1b63fd23-74ec-4f27-88a4-c3da8411958d', + 'event.dataset': 'endpoint', + 'kibana.alert.original_time': '2022-07-18T15:48:01.265Z', + 'kibana.alert.rule.rule_id': '9a1a2dae-0b5f-4c3d-8305-a268d404c306', + }, + { + 'kibana.alert.severity': 'medium', + agent: { + id: '465cb546-3483-4d60-976a-6df687597fab', + type: 'endpoint', + version: '7.12.15', + }, + 'kibana.alert.rule.references': [], + 'kibana.alert.rule.updated_by': 'elastic2', + 'event.category': 'malware', + 'kibana.alert.rule.threat': [], + 'kibana.alert.rule.rule_name_override': 'message', + 'kibana.alert.original_event.code': 'malicious_file', + 'kibana.alert.rule.description': + 'Generates a detection alert each time an Elastic Endpoint Security alert is received. Enabling this rule allows you to immediately begin investigating your Endpoint alerts.', + 'kibana.alert.rule.tags': ['Elastic', 'Endpoint Security'], + 'kibana.alert.rule.producer': 'siem', + 'kibana.alert.rule.to': 'now', + 'kibana.alert.rule.created_by': 'elastic2', + 'kibana.alert.rule.timestamp_override': 'event.ingested', + 'kibana.alert.original_event.ingested': '2022-07-18T15:05:03Z', + Endpoint: { + capabilities: [], + configuration: { + isolation: false, + }, + state: { + isolation: false, + }, + status: 'enrolled', + policy: { + applied: { + name: 'Default', + id: '00000000-0000-0000-0000-000000000000', + endpoint_policy_version: 1, + version: 3, + status: 'warning', + }, + }, + }, + ecs: { + version: '1.4.0', + }, + 'kibana.alert.risk_score': 47, + host: { + hostname: 'Host-v3mqhhvt7z', + os: { + Ext: { + variant: 'Windows Server', + }, + name: 'Windows', + family: 'windows', + version: '10.0', + platform: 'Windows', + full: 'Windows Server 2016', + }, + ip: ['10.54.27.117'], + name: 'Host-v3mqhhvt7z', + id: 'c97faec9-1f07-4052-9900-977946b22816', + mac: ['c9-e1-b9-3f-fc-5c'], + architecture: 'n467gryzu6', + }, + 'kibana.alert.rule.name': 'Endpoint Security', + 'event.agent_id_status': 'auth_metadata_missing', + 'event.kind': 'signal', + 'kibana.alert.original_event.id': 'acd7d638-778f-4e63-9af8-ae919ccb5dbc', + 'kibana.alert.workflow_status': 'open', + 'kibana.alert.rule.uuid': '1a6dbdc0-06aa-11ed-b2d1-8f870d1699ab', + 'kibana.alert.original_event.category': 'malware', + 'kibana.alert.rule.risk_score_mapping': [ + { + field: 'event.risk_score', + value: '', + operator: 'equals', + }, + ], + 'kibana.alert.original_event.module': 'endpoint', + 'kibana.alert.rule.interval': '5m', + 'kibana.alert.reason': + 'malware event with process malware writer, file fake_malware.exe, on Host-v3mqhhvt7z created medium alert Endpoint Security.', + 'kibana.alert.rule.type': 'query', + 'kibana.alert.rule.immutable': true, + 'event.code': 'malicious_file', + 'kibana.alert.original_event.type': 'creation', + 'kibana.alert.depth': 1, + 'kibana.alert.rule.enabled': true, + 'kibana.alert.rule.version': 3, + 'kibana.alert.rule.from': 'now-10m', + 'kibana.alert.rule.parameters': { + severity_mapping: [ + { + severity: 'low', + field: 'event.severity', + value: '21', + operator: 'equals', + }, + { + severity: 'medium', + field: 'event.severity', + value: '47', + operator: 'equals', + }, + { + severity: 'high', + field: 'event.severity', + value: '73', + operator: 'equals', + }, + { + severity: 'critical', + field: 'event.severity', + value: '99', + operator: 'equals', + }, + ], + references: [], + description: + 'Generates a detection alert each time an Elastic Endpoint Security alert is received. Enabling this rule allows you to immediately begin investigating your Endpoint alerts.', + language: 'kuery', + type: 'query', + rule_name_override: 'message', + exceptions_list: [ + { + list_id: 'endpoint_list', + namespace_type: 'agnostic', + id: 'endpoint_list', + type: 'endpoint', + }, + ], + timestamp_override: 'event.ingested', + from: 'now-10m', + severity: 'medium', + max_signals: 10000, + risk_score: 47, + risk_score_mapping: [ + { + field: 'event.risk_score', + value: '', + operator: 'equals', + }, + ], + author: ['Elastic'], + query: 'event.kind:alert and event.module:(endpoint and not endgame)\n', + index: ['logs-endpoint.alerts-*'], + version: 3, + rule_id: '9a1a2dae-0b5f-4c3d-8305-a268d404c306', + license: 'Elastic License v2', + required_fields: [], + immutable: true, + related_integrations: [], + setup: '', + false_positives: [], + threat: [], + to: 'now', + }, + 'kibana.alert.status': 'active', + 'kibana.alert.ancestors': [ + { + depth: 0, + index: '.ds-logs-endpoint.alerts-default-2022.07.18-000001', + id: 'qXXXEYIBRKQBoH3JXnZW', + type: 'event', + }, + ], + 'kibana.alert.original_event.dataset': 'endpoint', + 'kibana.alert.rule.exceptions_list': [ + { + list_id: 'endpoint_list', + namespace_type: 'agnostic', + id: 'endpoint_list', + type: 'endpoint', + }, + ], + 'kibana.alert.rule.actions': [], + 'event.sequence': 433, + 'kibana.alert.rule.rule_type_id': 'siem.queryRule', + file: { + owner: 'SYSTEM', + Ext: { + temp_file_path: 'C:/temp/fake_malware.exe', + code_signature: [ + { + trusted: false, + subject_name: 'bad signer', + }, + ], + quarantine_message: 'fake quarantine message', + quarantine_result: true, + malware_classification: { + identifier: 'endpointpe', + score: 1, + threshold: 0.66, + version: '3.0.33', + }, + }, + path: 'C:/fake_malware.exe', + size: 3456, + created: 1658159293264, + name: 'fake_malware.exe', + accessed: 1658159293264, + mtime: 1658159293264, + hash: { + sha1: 'fake file sha1', + sha256: 'fake file sha256', + md5: 'fake file md5', + }, + }, + 'event.module': 'endpoint', + 'kibana.alert.rule.license': 'Elastic License v2', + 'kibana.alert.original_event.kind': 'alert', + 'kibana.alert.rule.severity_mapping': [ + { + severity: 'low', + field: 'event.severity', + value: '21', + operator: 'equals', + }, + { + severity: 'medium', + field: 'event.severity', + value: '47', + operator: 'equals', + }, + { + severity: 'high', + field: 'event.severity', + value: '73', + operator: 'equals', + }, + { + severity: 'critical', + field: 'event.severity', + value: '99', + operator: 'equals', + }, + ], + process: { + Ext: { + ancestry: ['1xcca9dpu5', 'q2e9bxeoex'], + code_signature: [ + { + trusted: false, + subject_name: 'bad signer', + }, + ], + user: 'SYSTEM', + token: { + integrity_level: 16384, + privileges: [ + { + name: 'SeAssignPrimaryTokenPrivilege', + description: 'Replace a process level token', + enabled: false, + }, + ], + integrity_level_name: 'system', + domain: 'NT AUTHORITY', + type: 'tokenPrimary', + user: 'SYSTEM', + sid: 'S-1-5-18', + }, + }, + parent: { + pid: 1, + entity_id: '1xcca9dpu5', + }, + group_leader: { + name: 'fake leader', + pid: 369, + entity_id: 'xffdpn017z', + }, + session_leader: { + name: 'fake session', + pid: 63, + entity_id: 'xffdpn017z', + }, + entry_leader: { + name: 'fake entry', + pid: 392, + entity_id: 'xffdpn017z', + }, + name: 'malware writer', + start: 1658159293264, + pid: 2, + entity_id: '5si3vtl7f5', + executable: 'C:/malware.exe', + hash: { + sha1: 'fake sha1', + sha256: 'fake sha256', + md5: 'fake md5', + }, + uptime: 0, + }, + 'kibana.alert.rule.max_signals': 10000, + dll: [ + { + Ext: { + compile_time: 1534424710, + malware_classification: { + identifier: 'Whitelisted', + score: 0, + threshold: 0, + version: '3.0.0', + }, + mapped_address: 5362483200, + mapped_size: 0, + }, + path: 'C:\\Program Files\\Cybereason ActiveProbe\\AmSvc.exe', + code_signature: { + trusted: true, + subject_name: 'Cybereason Inc', + }, + pe: { + architecture: 'x64', + }, + hash: { + sha1: 'ca85243c0af6a6471bdaa560685c51eefd6dbc0d', + sha256: '8ad40c90a611d36eb8f9eb24fa04f7dbca713db383ff55a03aa0f382e92061a2', + md5: '1f2d082566b0fc5f2c238a5180db7451', + }, + }, + ], + 'kibana.alert.rule.updated_at': '2022-07-18T14:58:38.295Z', + 'kibana.alert.rule.risk_score': 47, + 'kibana.alert.rule.author': ['Elastic'], + 'kibana.alert.rule.false_positives': [], + 'kibana.alert.rule.consumer': 'siem', + 'kibana.alert.rule.indices': ['logs-endpoint.alerts-*'], + 'kibana.alert.rule.category': 'Custom Query Rule', + 'kibana.alert.original_event.sequence': 433, + 'event.ingested': '2022-07-18T15:05:03Z', + 'event.action': 'creation', + '@timestamp': '2022-07-18T15:08:48.064Z', + 'kibana.alert.rule.created_at': '2022-07-18T14:58:38.295Z', + 'kibana.alert.original_event.action': 'creation', + 'kibana.alert.rule.severity': 'medium', + data_stream: { + namespace: 'default', + type: 'logs', + dataset: 'endpoint.alerts', + }, + 'event.type': 'creation', + 'kibana.alert.original_event.agent_id_status': 'auth_metadata_missing', + elastic: { + agent: { + id: '465cb546-3483-4d60-976a-6df687597fab', + }, + }, + 'kibana.alert.rule.execution.uuid': '455933fc-319b-444b-a6c1-b7df2532634b', + 'kibana.space_ids': ['default'], + 'kibana.alert.uuid': 'bdb9e76269b9d541ef83724974313a41b2ccc9e057c54ddbcdf56c3a3ec9b076', + 'kibana.version': '8.4.0', + 'event.id': 'acd7d638-778f-4e63-9af8-ae919ccb5dbc', + 'event.dataset': 'endpoint', + 'kibana.alert.original_time': '2022-07-18T15:48:13.264Z', + 'kibana.alert.rule.rule_id': '9a1a2dae-0b5f-4c3d-8305-a268d404c306', + }, + { + 'kibana.alert.severity': 'medium', + agent: { + id: '465cb546-3483-4d60-976a-6df687597fab', + type: 'endpoint', + version: '7.12.15', + }, + 'kibana.alert.rule.references': [], + 'kibana.alert.rule.updated_by': 'elastic2', + 'event.category': 'behavior', + 'kibana.alert.rule.threat': [], + 'kibana.alert.rule.rule_name_override': 'message', + 'kibana.alert.original_event.code': 'behavior', + 'kibana.alert.rule.description': + 'Generates a detection alert each time an Elastic Endpoint Security alert is received. Enabling this rule allows you to immediately begin investigating your Endpoint alerts.', + source: { + port: 59406, + ip: '10.196.88.41', + }, + 'kibana.alert.rule.tags': ['Elastic', 'Endpoint Security'], + 'kibana.alert.rule.producer': 'siem', + 'kibana.alert.rule.to': 'now', + 'kibana.alert.rule.created_by': 'elastic2', + 'kibana.alert.rule.timestamp_override': 'event.ingested', + 'kibana.alert.original_event.ingested': '2022-07-18T15:05:03Z', + Endpoint: { + capabilities: [], + configuration: { + isolation: false, + }, + state: { + isolation: false, + }, + status: 'enrolled', + policy: { + applied: { + name: 'Default', + id: '00000000-0000-0000-0000-000000000000', + endpoint_policy_version: 1, + version: 3, + status: 'warning', + }, + }, + }, + ecs: { + version: '1.6.0', + }, + 'kibana.alert.risk_score': 47, + host: { + hostname: 'Host-v3mqhhvt7z', + os: { + Ext: { + variant: 'Windows Server', + }, + name: 'Windows', + family: 'windows', + version: '10.0', + platform: 'Windows', + full: 'Windows Server 2016', + }, + ip: ['10.54.27.117'], + name: 'Host-v3mqhhvt7z', + id: 'c97faec9-1f07-4052-9900-977946b22816', + mac: ['c9-e1-b9-3f-fc-5c'], + architecture: 'n467gryzu6', + }, + 'kibana.alert.rule.name': 'Endpoint Security', + 'event.agent_id_status': 'auth_metadata_missing', + 'event.kind': 'signal', + 'kibana.alert.original_event.id': '30d9cd79-a388-4bec-bc4e-edd529d7f8c7', + 'kibana.alert.workflow_status': 'open', + 'kibana.alert.rule.uuid': '1a6dbdc0-06aa-11ed-b2d1-8f870d1699ab', + 'kibana.alert.original_event.category': 'behavior', + 'kibana.alert.rule.risk_score_mapping': [ + { + field: 'event.risk_score', + value: '', + operator: 'equals', + }, + ], + 'kibana.alert.original_event.module': 'endpoint', + 'kibana.alert.rule.interval': '5m', + 'kibana.alert.reason': + 'behavior event with process mimikatz.exe, file fake_behavior.exe,:59406,:443, on Host-v3mqhhvt7z created medium alert Endpoint Security.', + 'kibana.alert.rule.type': 'query', + 'kibana.alert.rule.immutable': true, + 'event.code': 'behavior', + 'kibana.alert.original_event.type': 'info', + 'kibana.alert.depth': 1, + 'kibana.alert.rule.enabled': true, + 'kibana.alert.rule.version': 3, + 'kibana.alert.rule.from': 'now-10m', + 'kibana.alert.rule.parameters': { + severity_mapping: [ + { + severity: 'low', + field: 'event.severity', + value: '21', + operator: 'equals', + }, + { + severity: 'medium', + field: 'event.severity', + value: '47', + operator: 'equals', + }, + { + severity: 'high', + field: 'event.severity', + value: '73', + operator: 'equals', + }, + { + severity: 'critical', + field: 'event.severity', + value: '99', + operator: 'equals', + }, + ], + references: [], + description: + 'Generates a detection alert each time an Elastic Endpoint Security alert is received. Enabling this rule allows you to immediately begin investigating your Endpoint alerts.', + language: 'kuery', + type: 'query', + rule_name_override: 'message', + exceptions_list: [ + { + list_id: 'endpoint_list', + namespace_type: 'agnostic', + id: 'endpoint_list', + type: 'endpoint', + }, + ], + timestamp_override: 'event.ingested', + from: 'now-10m', + severity: 'medium', + max_signals: 10000, + risk_score: 47, + risk_score_mapping: [ + { + field: 'event.risk_score', + value: '', + operator: 'equals', + }, + ], + author: ['Elastic'], + query: 'event.kind:alert and event.module:(endpoint and not endgame)\n', + index: ['logs-endpoint.alerts-*'], + version: 3, + rule_id: '9a1a2dae-0b5f-4c3d-8305-a268d404c306', + license: 'Elastic License v2', + required_fields: [], + immutable: true, + related_integrations: [], + setup: '', + false_positives: [], + threat: [], + to: 'now', + }, + 'kibana.alert.status': 'active', + destination: { + port: 443, + ip: '10.35.239.147', + }, + 'kibana.alert.ancestors': [ + { + depth: 0, + index: '.ds-logs-endpoint.alerts-default-2022.07.18-000001', + id: 'RHXXEYIBRKQBoH3JXnZV', + type: 'event', + }, + ], + rule: { + description: 'Behavior rule description', + id: '2a90fbfc-8fba-4d29-baf8-d92d71b74c06', + }, + 'kibana.alert.original_event.dataset': 'endpoint.diagnostic.collection', + 'kibana.alert.rule.exceptions_list': [ + { + list_id: 'endpoint_list', + namespace_type: 'agnostic', + id: 'endpoint_list', + type: 'endpoint', + }, + ], + 'kibana.alert.rule.actions': [], + network: { + transport: 'tcp', + type: 'ipv4', + direction: 'outgoing', + }, + 'event.sequence': 332, + 'kibana.alert.rule.rule_type_id': 'siem.queryRule', + file: { + path: 'C:/fake_behavior.exe', + name: 'fake_behavior.exe', + }, + 'event.module': 'endpoint', + 'kibana.alert.rule.license': 'Elastic License v2', + 'kibana.alert.original_event.kind': 'alert', + registry: { + path: 'HKEY_USERS\\S-1-5-21-2460036010-3910878774-3458087990-1001\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run\\chrome', + data: { + strings: 'C:/fake_behavior/mimikatz.exe', + }, + value: 'mimikatz.exe', + }, + 'kibana.alert.rule.severity_mapping': [ + { + severity: 'low', + field: 'event.severity', + value: '21', + operator: 'equals', + }, + { + severity: 'medium', + field: 'event.severity', + value: '47', + operator: 'equals', + }, + { + severity: 'high', + field: 'event.severity', + value: '73', + operator: 'equals', + }, + { + severity: 'critical', + field: 'event.severity', + value: '99', + operator: 'equals', + }, + ], + process: { + Ext: { + ancestry: ['tjgw33zj1b'], + code_signature: [ + { + trusted: false, + subject_name: 'bad signer', + }, + ], + user: 'SYSTEM', + token: { + integrity_level_name: 'high', + elevation_level: 'full', + }, + }, + parent: { + pid: 1, + entity_id: 'tjgw33zj1b', + }, + group_leader: { + name: 'fake leader', + pid: 641, + entity_id: 'tjgw33zj1b', + }, + session_leader: { + name: 'fake session', + pid: 927, + entity_id: 'tjgw33zj1b', + }, + code_signature: { + subject_name: 'Microsoft Windows', + status: 'trusted', + }, + entry_leader: { + name: 'fake entry', + pid: 948, + entity_id: 'tjgw33zj1b', + }, + name: 'mimikatz.exe', + pid: 2, + entity_id: 'rnvpx8wrfo', + executable: 'C:/fake_behavior/mimikatz.exe', + }, + 'kibana.alert.rule.max_signals': 10000, + dll: [ + { + Ext: { + compile_time: 1534424710, + malware_classification: { + identifier: 'Whitelisted', + score: 0, + threshold: 0, + version: '3.0.0', + }, + mapped_address: 5362483200, + mapped_size: 0, + }, + path: 'C:\\Program Files\\Cybereason ActiveProbe\\AmSvc.exe', + code_signature: { + trusted: true, + subject_name: 'Cybereason Inc', + }, + pe: { + architecture: 'x64', + }, + hash: { + sha1: 'ca85243c0af6a6471bdaa560685c51eefd6dbc0d', + sha256: '8ad40c90a611d36eb8f9eb24fa04f7dbca713db383ff55a03aa0f382e92061a2', + md5: '1f2d082566b0fc5f2c238a5180db7451', + }, + }, + ], + 'kibana.alert.rule.updated_at': '2022-07-18T14:58:38.295Z', + 'kibana.alert.rule.risk_score': 47, + 'kibana.alert.rule.author': ['Elastic'], + 'kibana.alert.rule.false_positives': [], + 'kibana.alert.rule.consumer': 'siem', + 'kibana.alert.rule.indices': ['logs-endpoint.alerts-*'], + 'kibana.alert.rule.category': 'Custom Query Rule', + 'kibana.alert.original_event.sequence': 332, + 'event.ingested': '2022-07-18T15:05:03Z', + 'event.action': 'rule_detection', + '@timestamp': '2022-07-18T15:08:48.064Z', + 'kibana.alert.rule.created_at': '2022-07-18T14:58:38.295Z', + 'kibana.alert.original_event.action': 'rule_detection', + 'kibana.alert.rule.severity': 'medium', + data_stream: { + namespace: 'default', + type: 'logs', + dataset: 'endpoint.alerts', + }, + 'event.type': 'info', + 'kibana.alert.original_event.agent_id_status': 'auth_metadata_missing', + elastic: { + agent: { + id: '465cb546-3483-4d60-976a-6df687597fab', + }, + }, + 'kibana.alert.rule.execution.uuid': '455933fc-319b-444b-a6c1-b7df2532634b', + 'kibana.space_ids': ['default'], + 'kibana.alert.uuid': 'ccfbc0d32551137e86c816025670ce52b5bc48efde7f619b48bba02da884fac8', + 'kibana.version': '8.4.0', + 'event.id': '30d9cd79-a388-4bec-bc4e-edd529d7f8c7', + 'event.dataset': 'endpoint.diagnostic.collection', + 'kibana.alert.original_time': '2022-07-18T15:51:13.263Z', + 'kibana.alert.rule.rule_id': '9a1a2dae-0b5f-4c3d-8305-a268d404c306', + }, + { + 'kibana.alert.severity': 'medium', + agent: { + id: '465cb546-3483-4d60-976a-6df687597fab', + type: 'endpoint', + version: '7.12.15', + }, + 'kibana.alert.rule.references': [], + 'kibana.alert.rule.updated_by': 'elastic2', + 'event.category': 'malware', + 'kibana.alert.rule.threat': [], + 'kibana.alert.rule.rule_name_override': 'message', + 'kibana.alert.original_event.code': 'memory_signature', + 'kibana.alert.rule.description': + 'Generates a detection alert each time an Elastic Endpoint Security alert is received. Enabling this rule allows you to immediately begin investigating your Endpoint alerts.', + 'kibana.alert.rule.tags': ['Elastic', 'Endpoint Security'], + 'kibana.alert.rule.producer': 'siem', + 'kibana.alert.rule.to': 'now', + 'kibana.alert.rule.created_by': 'elastic2', + 'kibana.alert.rule.timestamp_override': 'event.ingested', + 'kibana.alert.original_event.ingested': '2022-07-18T15:05:03Z', + Endpoint: { + capabilities: [], + configuration: { + isolation: false, + }, + state: { + isolation: false, + }, + status: 'enrolled', + policy: { + applied: { + name: 'Default', + id: '00000000-0000-0000-0000-000000000000', + endpoint_policy_version: 1, + version: 3, + status: 'warning', + }, + }, + }, + ecs: { + version: '1.6.0', + }, + 'kibana.alert.risk_score': 47, + host: { + hostname: 'Host-v3mqhhvt7z', + os: { + Ext: { + variant: 'Windows Server', + }, + name: 'Windows', + family: 'windows', + version: '10.0', + platform: 'Windows', + full: 'Windows Server 2016', + }, + ip: ['10.54.27.117'], + name: 'Host-v3mqhhvt7z', + id: 'c97faec9-1f07-4052-9900-977946b22816', + mac: ['c9-e1-b9-3f-fc-5c'], + architecture: 'n467gryzu6', + }, + 'kibana.alert.rule.name': 'Endpoint Security', + 'event.agent_id_status': 'auth_metadata_missing', + 'event.kind': 'signal', + 'kibana.alert.original_event.id': 'edab4def-d175-474f-bac6-df422eeeeaa0', + 'kibana.alert.workflow_status': 'open', + 'kibana.alert.rule.uuid': '1a6dbdc0-06aa-11ed-b2d1-8f870d1699ab', + 'kibana.alert.original_event.category': 'malware', + 'kibana.alert.rule.risk_score_mapping': [ + { + field: 'event.risk_score', + value: '', + operator: 'equals', + }, + ], + 'kibana.alert.original_event.module': 'endpoint', + 'kibana.alert.rule.interval': '5m', + 'kibana.alert.reason': + 'malware event with process iexlorer.exe, on Host-v3mqhhvt7z created medium alert Endpoint Security.', + 'kibana.alert.rule.type': 'query', + 'kibana.alert.rule.immutable': true, + 'event.code': 'memory_signature', + 'kibana.alert.original_event.type': 'info', + 'kibana.alert.depth': 1, + 'kibana.alert.rule.enabled': true, + 'kibana.alert.rule.version': 3, + 'kibana.alert.rule.from': 'now-10m', + 'kibana.alert.rule.parameters': { + severity_mapping: [ + { + severity: 'low', + field: 'event.severity', + value: '21', + operator: 'equals', + }, + { + severity: 'medium', + field: 'event.severity', + value: '47', + operator: 'equals', + }, + { + severity: 'high', + field: 'event.severity', + value: '73', + operator: 'equals', + }, + { + severity: 'critical', + field: 'event.severity', + value: '99', + operator: 'equals', + }, + ], + references: [], + description: + 'Generates a detection alert each time an Elastic Endpoint Security alert is received. Enabling this rule allows you to immediately begin investigating your Endpoint alerts.', + language: 'kuery', + type: 'query', + rule_name_override: 'message', + exceptions_list: [ + { + list_id: 'endpoint_list', + namespace_type: 'agnostic', + id: 'endpoint_list', + type: 'endpoint', + }, + ], + timestamp_override: 'event.ingested', + from: 'now-10m', + severity: 'medium', + max_signals: 10000, + risk_score: 47, + risk_score_mapping: [ + { + field: 'event.risk_score', + value: '', + operator: 'equals', + }, + ], + author: ['Elastic'], + query: 'event.kind:alert and event.module:(endpoint and not endgame)\n', + index: ['logs-endpoint.alerts-*'], + version: 3, + rule_id: '9a1a2dae-0b5f-4c3d-8305-a268d404c306', + license: 'Elastic License v2', + required_fields: [], + immutable: true, + related_integrations: [], + setup: '', + false_positives: [], + threat: [], + to: 'now', + }, + 'kibana.alert.status': 'active', + 'kibana.alert.ancestors': [ + { + depth: 0, + index: '.ds-logs-endpoint.alerts-default-2022.07.18-000001', + id: 'AXXXEYIBRKQBoH3JXnVV', + type: 'event', + }, + ], + 'kibana.alert.original_event.dataset': 'endpoint', + 'kibana.alert.rule.exceptions_list': [ + { + list_id: 'endpoint_list', + namespace_type: 'agnostic', + id: 'endpoint_list', + type: 'endpoint', + }, + ], + 'kibana.alert.rule.actions': [], + 'event.sequence': 9, + 'kibana.alert.rule.rule_type_id': 'siem.queryRule', + file: {}, + 'event.module': 'endpoint', + 'kibana.alert.rule.license': 'Elastic License v2', + 'kibana.alert.original_event.kind': 'alert', + 'kibana.alert.rule.severity_mapping': [ + { + severity: 'low', + field: 'event.severity', + value: '21', + operator: 'equals', + }, + { + severity: 'medium', + field: 'event.severity', + value: '47', + operator: 'equals', + }, + { + severity: 'high', + field: 'event.severity', + value: '73', + operator: 'equals', + }, + { + severity: 'critical', + field: 'event.severity', + value: '99', + operator: 'equals', + }, + ], + process: { + Ext: { + malware_signature: { + identifier: 'diagnostic-malware-signature-v1-fake', + all_names: 'Windows.Trojan.FakeAgent', + }, + ancestry: [], + code_signature: [ + { + trusted: false, + subject_name: 'bad signer', + }, + ], + user: 'SYSTEM', + token: { + integrity_level_name: 'high', + }, + }, + group_leader: { + name: 'fake leader', + pid: 204, + entity_id: 'rcjx6qalkt', + }, + session_leader: { + name: 'fake session', + pid: 898, + entity_id: 'rcjx6qalkt', + }, + entry_leader: { + name: 'fake entry', + pid: 247, + entity_id: 'rcjx6qalkt', + }, + name: 'iexlorer.exe', + start: 1658159689251, + pid: 2, + entity_id: 'rcjx6qalkt', + executable: 'C:/fake/iexlorer.exe', + hash: { + sha1: 'fake sha1', + sha256: 'fake sha256', + md5: 'fake md5', + }, + uptime: 0, + }, + 'kibana.alert.rule.max_signals': 10000, + dll: [ + { + Ext: { + compile_time: 1534424710, + malware_classification: { + identifier: 'Whitelisted', + score: 0, + threshold: 0, + version: '3.0.0', + }, + mapped_address: 5362483200, + mapped_size: 0, + }, + path: 'C:\\Program Files\\Cybereason ActiveProbe\\AmSvc.exe', + code_signature: { + trusted: true, + subject_name: 'Cybereason Inc', + }, + pe: { + architecture: 'x64', + }, + hash: { + sha1: 'ca85243c0af6a6471bdaa560685c51eefd6dbc0d', + sha256: '8ad40c90a611d36eb8f9eb24fa04f7dbca713db383ff55a03aa0f382e92061a2', + md5: '1f2d082566b0fc5f2c238a5180db7451', + }, + }, + ], + 'kibana.alert.rule.updated_at': '2022-07-18T14:58:38.295Z', + 'kibana.alert.rule.risk_score': 47, + 'kibana.alert.rule.author': ['Elastic'], + 'kibana.alert.rule.false_positives': [], + 'kibana.alert.rule.consumer': 'siem', + 'kibana.alert.rule.indices': ['logs-endpoint.alerts-*'], + 'kibana.alert.rule.category': 'Custom Query Rule', + 'kibana.alert.original_event.sequence': 9, + 'event.ingested': '2022-07-18T15:05:03Z', + 'event.action': 'start', + '@timestamp': '2022-07-18T15:08:48.064Z', + 'kibana.alert.rule.created_at': '2022-07-18T14:58:38.295Z', + 'kibana.alert.original_event.action': 'start', + 'kibana.alert.rule.severity': 'medium', + data_stream: { + namespace: 'default', + type: 'logs', + dataset: 'endpoint.alerts', + }, + 'event.type': 'info', + 'kibana.alert.original_event.agent_id_status': 'auth_metadata_missing', + elastic: { + agent: { + id: '465cb546-3483-4d60-976a-6df687597fab', + }, + }, + 'kibana.alert.rule.execution.uuid': '455933fc-319b-444b-a6c1-b7df2532634b', + 'kibana.space_ids': ['default'], + 'kibana.alert.uuid': '3b748dac2b98c0c0ae7a849d036d78207173fa5c514e717f0a6fda6290689f75', + 'kibana.version': '8.4.0', + 'event.id': 'edab4def-d175-474f-bac6-df422eeeeaa0', + 'event.dataset': 'endpoint', + 'kibana.alert.original_time': '2022-07-18T15:54:49.251Z', + 'kibana.alert.rule.rule_id': '9a1a2dae-0b5f-4c3d-8305-a268d404c306', + }, + { + 'kibana.alert.severity': 'medium', + agent: { + id: '465cb546-3483-4d60-976a-6df687597fab', + type: 'endpoint', + version: '7.12.15', + }, + 'kibana.alert.rule.references': [], + 'kibana.alert.rule.updated_by': 'elastic2', + 'event.category': 'malware', + 'kibana.alert.rule.threat': [], + 'kibana.alert.rule.rule_name_override': 'message', + 'kibana.alert.original_event.code': 'memory_signature', + 'kibana.alert.rule.description': + 'Generates a detection alert each time an Elastic Endpoint Security alert is received. Enabling this rule allows you to immediately begin investigating your Endpoint alerts.', + 'kibana.alert.rule.tags': ['Elastic', 'Endpoint Security'], + 'kibana.alert.rule.producer': 'siem', + 'kibana.alert.rule.to': 'now', + 'kibana.alert.rule.created_by': 'elastic2', + 'kibana.alert.rule.timestamp_override': 'event.ingested', + 'kibana.alert.original_event.ingested': '2022-07-18T15:05:03Z', + Endpoint: { + capabilities: [], + configuration: { + isolation: false, + }, + state: { + isolation: false, + }, + status: 'enrolled', + policy: { + applied: { + name: 'Default', + id: '00000000-0000-0000-0000-000000000000', + endpoint_policy_version: 1, + version: 3, + status: 'warning', + }, + }, + }, + ecs: { + version: '1.6.0', + }, + 'kibana.alert.risk_score': 47, + host: { + hostname: 'Host-v3mqhhvt7z', + os: { + Ext: { + variant: 'Windows Server', + }, + name: 'Windows', + family: 'windows', + version: '10.0', + platform: 'Windows', + full: 'Windows Server 2016', + }, + ip: ['10.54.27.117'], + name: 'Host-v3mqhhvt7z', + id: 'c97faec9-1f07-4052-9900-977946b22816', + mac: ['c9-e1-b9-3f-fc-5c'], + architecture: 'n467gryzu6', + }, + 'kibana.alert.rule.name': 'Endpoint Security', + 'event.agent_id_status': 'auth_metadata_missing', + 'event.kind': 'signal', + 'kibana.alert.original_event.id': 'eea8511d-7dcd-4e18-aede-9850ca8873c1', + 'kibana.alert.workflow_status': 'open', + 'kibana.alert.rule.uuid': '1a6dbdc0-06aa-11ed-b2d1-8f870d1699ab', + 'kibana.alert.original_event.category': 'malware', + 'kibana.alert.rule.risk_score_mapping': [ + { + field: 'event.risk_score', + value: '', + operator: 'equals', + }, + ], + 'kibana.alert.original_event.module': 'endpoint', + 'kibana.alert.rule.interval': '5m', + 'kibana.alert.reason': + 'malware event with process notepad.exe, on Host-v3mqhhvt7z created medium alert Endpoint Security.', + 'kibana.alert.rule.type': 'query', + 'kibana.alert.rule.immutable': true, + 'event.code': 'memory_signature', + 'kibana.alert.original_event.type': 'info', + 'kibana.alert.depth': 1, + 'kibana.alert.rule.enabled': true, + 'kibana.alert.rule.version': 3, + 'kibana.alert.rule.from': 'now-10m', + 'kibana.alert.rule.parameters': { + severity_mapping: [ + { + severity: 'low', + field: 'event.severity', + value: '21', + operator: 'equals', + }, + { + severity: 'medium', + field: 'event.severity', + value: '47', + operator: 'equals', + }, + { + severity: 'high', + field: 'event.severity', + value: '73', + operator: 'equals', + }, + { + severity: 'critical', + field: 'event.severity', + value: '99', + operator: 'equals', + }, + ], + references: [], + description: + 'Generates a detection alert each time an Elastic Endpoint Security alert is received. Enabling this rule allows you to immediately begin investigating your Endpoint alerts.', + language: 'kuery', + type: 'query', + rule_name_override: 'message', + exceptions_list: [ + { + list_id: 'endpoint_list', + namespace_type: 'agnostic', + id: 'endpoint_list', + type: 'endpoint', + }, + ], + timestamp_override: 'event.ingested', + from: 'now-10m', + severity: 'medium', + max_signals: 10000, + risk_score: 47, + risk_score_mapping: [ + { + field: 'event.risk_score', + value: '', + operator: 'equals', + }, + ], + author: ['Elastic'], + query: 'event.kind:alert and event.module:(endpoint and not endgame)\n', + index: ['logs-endpoint.alerts-*'], + version: 3, + rule_id: '9a1a2dae-0b5f-4c3d-8305-a268d404c306', + license: 'Elastic License v2', + required_fields: [], + immutable: true, + related_integrations: [], + setup: '', + false_positives: [], + threat: [], + to: 'now', + }, + 'kibana.alert.status': 'active', + 'kibana.alert.ancestors': [ + { + depth: 0, + index: '.ds-logs-endpoint.alerts-default-2022.07.18-000001', + id: 'NXXXEYIBRKQBoH3JXnVV', + type: 'event', + }, + ], + 'kibana.alert.original_event.dataset': 'endpoint', + 'kibana.alert.rule.exceptions_list': [ + { + list_id: 'endpoint_list', + namespace_type: 'agnostic', + id: 'endpoint_list', + type: 'endpoint', + }, + ], + 'kibana.alert.rule.actions': [], + 'event.sequence': 61, + 'kibana.alert.rule.rule_type_id': 'siem.queryRule', + file: {}, + 'event.module': 'endpoint', + 'kibana.alert.rule.license': 'Elastic License v2', + 'kibana.alert.original_event.kind': 'alert', + 'kibana.alert.rule.severity_mapping': [ + { + severity: 'low', + field: 'event.severity', + value: '21', + operator: 'equals', + }, + { + severity: 'medium', + field: 'event.severity', + value: '47', + operator: 'equals', + }, + { + severity: 'high', + field: 'event.severity', + value: '73', + operator: 'equals', + }, + { + severity: 'critical', + field: 'event.severity', + value: '99', + operator: 'equals', + }, + ], + process: { + Ext: { + malware_signature: { + identifier: 'diagnostic-malware-signature-v1-fake', + all_names: 'Windows.Trojan.FakeAgent', + }, + ancestry: ['9pn3h229yo', 'p4vhozscnl'], + code_signature: [ + { + trusted: false, + subject_name: 'bad signer', + }, + ], + user: 'SYSTEM', + token: { + integrity_level_name: 'high', + }, + }, + parent: { + pid: 1, + entity_id: '9pn3h229yo', + }, + group_leader: { + name: 'fake leader', + pid: 180, + entity_id: 'rcjx6qalkt', + }, + session_leader: { + name: 'fake session', + pid: 731, + entity_id: 'rcjx6qalkt', + }, + entry_leader: { + name: 'fake entry', + pid: 538, + entity_id: 'rcjx6qalkt', + }, + name: 'notepad.exe', + start: 1658160062251, + pid: 2, + entity_id: 'vm1uiebh2c', + executable: 'C:/fake/notepad.exe', + hash: { + sha1: 'fake sha1', + sha256: 'fake sha256', + md5: 'fake md5', + }, + uptime: 0, + }, + 'kibana.alert.rule.max_signals': 10000, + dll: [ + { + Ext: { + compile_time: 1534424710, + malware_classification: { + identifier: 'Whitelisted', + score: 0, + threshold: 0, + version: '3.0.0', + }, + mapped_address: 5362483200, + mapped_size: 0, + }, + path: 'C:\\Program Files\\Cybereason ActiveProbe\\AmSvc.exe', + code_signature: { + trusted: true, + subject_name: 'Cybereason Inc', + }, + pe: { + architecture: 'x64', + }, + hash: { + sha1: 'ca85243c0af6a6471bdaa560685c51eefd6dbc0d', + sha256: '8ad40c90a611d36eb8f9eb24fa04f7dbca713db383ff55a03aa0f382e92061a2', + md5: '1f2d082566b0fc5f2c238a5180db7451', + }, + }, + ], + 'kibana.alert.rule.updated_at': '2022-07-18T14:58:38.295Z', + 'kibana.alert.rule.risk_score': 47, + 'kibana.alert.rule.author': ['Elastic'], + 'kibana.alert.rule.false_positives': [], + 'kibana.alert.rule.consumer': 'siem', + 'kibana.alert.rule.indices': ['logs-endpoint.alerts-*'], + 'kibana.alert.rule.category': 'Custom Query Rule', + 'kibana.alert.original_event.sequence': 61, + 'event.ingested': '2022-07-18T15:05:03Z', + 'event.action': 'start', + '@timestamp': '2022-07-18T15:08:48.065Z', + 'kibana.alert.rule.created_at': '2022-07-18T14:58:38.295Z', + 'kibana.alert.original_event.action': 'start', + 'kibana.alert.rule.severity': 'medium', + data_stream: { + namespace: 'default', + type: 'logs', + dataset: 'endpoint.alerts', + }, + 'event.type': 'info', + 'kibana.alert.original_event.agent_id_status': 'auth_metadata_missing', + elastic: { + agent: { + id: '465cb546-3483-4d60-976a-6df687597fab', + }, + }, + 'kibana.alert.rule.execution.uuid': '455933fc-319b-444b-a6c1-b7df2532634b', + 'kibana.space_ids': ['default'], + 'kibana.alert.uuid': '516168d06ae19a982f39b736c8f05b9304dc550ad69732cd432427907a057925', + 'kibana.version': '8.4.0', + 'event.id': 'eea8511d-7dcd-4e18-aede-9850ca8873c1', + 'event.dataset': 'endpoint', + 'kibana.alert.original_time': '2022-07-18T16:01:02.251Z', + 'kibana.alert.rule.rule_id': '9a1a2dae-0b5f-4c3d-8305-a268d404c306', + }, + { + 'kibana.alert.severity': 'medium', + agent: { + id: '465cb546-3483-4d60-976a-6df687597fab', + type: 'endpoint', + version: '7.12.15', + }, + 'kibana.alert.rule.references': [], + 'kibana.alert.rule.updated_by': 'elastic2', + 'event.category': 'behavior', + 'kibana.alert.rule.threat': [], + 'kibana.alert.rule.rule_name_override': 'message', + 'kibana.alert.original_event.code': 'behavior', + 'kibana.alert.rule.description': + 'Generates a detection alert each time an Elastic Endpoint Security alert is received. Enabling this rule allows you to immediately begin investigating your Endpoint alerts.', + source: { + port: 59406, + ip: '10.11.237.178', + }, + 'kibana.alert.rule.tags': ['Elastic', 'Endpoint Security'], + 'kibana.alert.rule.producer': 'siem', + 'kibana.alert.rule.to': 'now', + 'kibana.alert.rule.created_by': 'elastic2', + 'kibana.alert.rule.timestamp_override': 'event.ingested', + 'kibana.alert.original_event.ingested': '2022-07-18T15:05:03Z', + Endpoint: { + capabilities: [], + configuration: { + isolation: false, + }, + state: { + isolation: false, + }, + status: 'enrolled', + policy: { + applied: { + name: 'Default', + id: '00000000-0000-0000-0000-000000000000', + endpoint_policy_version: 1, + version: 3, + status: 'warning', + }, + }, + }, + ecs: { + version: '1.6.0', + }, + 'kibana.alert.risk_score': 47, + host: { + hostname: 'Host-v3mqhhvt7z', + os: { + Ext: { + variant: 'Windows Server', + }, + name: 'Windows', + family: 'windows', + version: '10.0', + platform: 'Windows', + full: 'Windows Server 2016', + }, + ip: ['10.54.27.117'], + name: 'Host-v3mqhhvt7z', + id: 'c97faec9-1f07-4052-9900-977946b22816', + mac: ['c9-e1-b9-3f-fc-5c'], + architecture: 'n467gryzu6', + }, + 'kibana.alert.rule.name': 'Endpoint Security', + 'event.agent_id_status': 'auth_metadata_missing', + 'event.kind': 'signal', + 'kibana.alert.original_event.id': '550ea209-f20d-44dd-9e91-79a28c5f41a9', + 'kibana.alert.workflow_status': 'open', + 'kibana.alert.rule.uuid': '1a6dbdc0-06aa-11ed-b2d1-8f870d1699ab', + 'kibana.alert.original_event.category': 'behavior', + 'kibana.alert.rule.risk_score_mapping': [ + { + field: 'event.risk_score', + value: '', + operator: 'equals', + }, + ], + 'kibana.alert.original_event.module': 'endpoint', + 'kibana.alert.rule.interval': '5m', + 'kibana.alert.reason': + 'behavior event with process powershell.exe, file fake_behavior.exe,:59406,:443, on Host-v3mqhhvt7z created medium alert Endpoint Security.', + 'kibana.alert.rule.type': 'query', + 'kibana.alert.rule.immutable': true, + 'event.code': 'behavior', + 'kibana.alert.original_event.type': 'info', + 'kibana.alert.depth': 1, + 'kibana.alert.rule.enabled': true, + 'kibana.alert.rule.version': 3, + 'kibana.alert.rule.from': 'now-10m', + 'kibana.alert.rule.parameters': { + severity_mapping: [ + { + severity: 'low', + field: 'event.severity', + value: '21', + operator: 'equals', + }, + { + severity: 'medium', + field: 'event.severity', + value: '47', + operator: 'equals', + }, + { + severity: 'high', + field: 'event.severity', + value: '73', + operator: 'equals', + }, + { + severity: 'critical', + field: 'event.severity', + value: '99', + operator: 'equals', + }, + ], + references: [], + description: + 'Generates a detection alert each time an Elastic Endpoint Security alert is received. Enabling this rule allows you to immediately begin investigating your Endpoint alerts.', + language: 'kuery', + type: 'query', + rule_name_override: 'message', + exceptions_list: [ + { + list_id: 'endpoint_list', + namespace_type: 'agnostic', + id: 'endpoint_list', + type: 'endpoint', + }, + ], + timestamp_override: 'event.ingested', + from: 'now-10m', + severity: 'medium', + max_signals: 10000, + risk_score: 47, + risk_score_mapping: [ + { + field: 'event.risk_score', + value: '', + operator: 'equals', + }, + ], + author: ['Elastic'], + query: 'event.kind:alert and event.module:(endpoint and not endgame)\n', + index: ['logs-endpoint.alerts-*'], + version: 3, + rule_id: '9a1a2dae-0b5f-4c3d-8305-a268d404c306', + license: 'Elastic License v2', + required_fields: [], + immutable: true, + related_integrations: [], + setup: '', + false_positives: [], + threat: [], + to: 'now', + }, + 'kibana.alert.status': 'active', + destination: { + port: 443, + ip: '10.42.124.0', + }, + 'kibana.alert.ancestors': [ + { + depth: 0, + index: '.ds-logs-endpoint.alerts-default-2022.07.18-000001', + id: 'G3XXEYIBRKQBoH3JXnVV', + type: 'event', + }, + ], + rule: { + description: 'Behavior rule description', + id: '5fef0a22-9cfe-484c-b159-0d7f040e189a', + }, + 'kibana.alert.original_event.dataset': 'endpoint.diagnostic.collection', + 'kibana.alert.rule.exceptions_list': [ + { + list_id: 'endpoint_list', + namespace_type: 'agnostic', + id: 'endpoint_list', + type: 'endpoint', + }, + ], + 'kibana.alert.rule.actions': [], + network: { + transport: 'tcp', + type: 'ipv4', + direction: 'outgoing', + }, + 'event.sequence': 35, + 'kibana.alert.rule.rule_type_id': 'siem.queryRule', + file: { + path: 'C:/fake_behavior.exe', + name: 'fake_behavior.exe', + }, + 'event.module': 'endpoint', + 'kibana.alert.rule.license': 'Elastic License v2', + 'kibana.alert.original_event.kind': 'alert', + registry: { + path: 'HKEY_USERS\\S-1-5-21-2460036010-3910878774-3458087990-1001\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run\\chrome', + data: { + strings: 'C:/fake_behavior/powershell.exe', + }, + value: 'powershell.exe', + }, + 'kibana.alert.rule.severity_mapping': [ + { + severity: 'low', + field: 'event.severity', + value: '21', + operator: 'equals', + }, + { + severity: 'medium', + field: 'event.severity', + value: '47', + operator: 'equals', + }, + { + severity: 'high', + field: 'event.severity', + value: '73', + operator: 'equals', + }, + { + severity: 'critical', + field: 'event.severity', + value: '99', + operator: 'equals', + }, + ], + process: { + Ext: { + ancestry: ['gvnad391wa', '9pn3h229yo'], + code_signature: [ + { + trusted: false, + subject_name: 'bad signer', + }, + ], + user: 'SYSTEM', + token: { + integrity_level_name: 'high', + elevation_level: 'full', + }, + }, + parent: { + pid: 1, + entity_id: 'gvnad391wa', + }, + group_leader: { + name: 'fake leader', + pid: 599, + entity_id: 'rcjx6qalkt', + }, + session_leader: { + name: 'fake session', + pid: 323, + entity_id: 'rcjx6qalkt', + }, + code_signature: { + subject_name: 'Microsoft Windows', + status: 'trusted', + }, + entry_leader: { + name: 'fake entry', + pid: 284, + entity_id: 'rcjx6qalkt', + }, + name: 'powershell.exe', + pid: 2, + entity_id: 'bcz9bx3din', + executable: 'C:/fake_behavior/powershell.exe', + }, + 'kibana.alert.rule.max_signals': 10000, + dll: [ + { + Ext: { + compile_time: 1534424710, + malware_classification: { + identifier: 'Whitelisted', + score: 0, + threshold: 0, + version: '3.0.0', + }, + mapped_address: 5362483200, + mapped_size: 0, + }, + path: 'C:\\Program Files\\Cybereason ActiveProbe\\AmSvc.exe', + code_signature: { + trusted: true, + subject_name: 'Cybereason Inc', + }, + pe: { + architecture: 'x64', + }, + hash: { + sha1: 'ca85243c0af6a6471bdaa560685c51eefd6dbc0d', + sha256: '8ad40c90a611d36eb8f9eb24fa04f7dbca713db383ff55a03aa0f382e92061a2', + md5: '1f2d082566b0fc5f2c238a5180db7451', + }, + }, + ], + 'kibana.alert.rule.updated_at': '2022-07-18T14:58:38.295Z', + 'kibana.alert.rule.risk_score': 47, + 'kibana.alert.rule.author': ['Elastic'], + 'kibana.alert.rule.false_positives': [], + 'kibana.alert.rule.consumer': 'siem', + 'kibana.alert.rule.indices': ['logs-endpoint.alerts-*'], + 'kibana.alert.rule.category': 'Custom Query Rule', + 'kibana.alert.original_event.sequence': 35, + 'event.ingested': '2022-07-18T15:05:03Z', + 'event.action': 'rule_detection', + '@timestamp': '2022-07-18T15:08:48.065Z', + 'kibana.alert.rule.created_at': '2022-07-18T14:58:38.295Z', + 'kibana.alert.original_event.action': 'rule_detection', + 'kibana.alert.rule.severity': 'medium', + data_stream: { + namespace: 'default', + type: 'logs', + dataset: 'endpoint.alerts', + }, + 'event.type': 'info', + 'kibana.alert.original_event.agent_id_status': 'auth_metadata_missing', + elastic: { + agent: { + id: '465cb546-3483-4d60-976a-6df687597fab', + }, + }, + 'kibana.alert.rule.execution.uuid': '455933fc-319b-444b-a6c1-b7df2532634b', + 'kibana.space_ids': ['default'], + 'kibana.alert.uuid': '59e2a69855a739de7fa7ac0c530d132994ed84d99cca69fd3a1ce165a7f086fd', + 'kibana.version': '8.4.0', + 'event.id': '550ea209-f20d-44dd-9e91-79a28c5f41a9', + 'event.dataset': 'endpoint.diagnostic.collection', + 'kibana.alert.original_time': '2022-07-18T16:01:32.251Z', + 'kibana.alert.rule.rule_id': '9a1a2dae-0b5f-4c3d-8305-a268d404c306', + }, + { + 'kibana.alert.severity': 'medium', + agent: { + id: '465cb546-3483-4d60-976a-6df687597fab', + type: 'endpoint', + version: '7.12.15', + }, + 'kibana.alert.rule.references': [], + 'kibana.alert.rule.updated_by': 'elastic2', + 'event.category': 'behavior', + 'kibana.alert.rule.threat': [], + 'kibana.alert.rule.rule_name_override': 'message', + 'kibana.alert.original_event.code': 'behavior', + 'kibana.alert.rule.description': + 'Generates a detection alert each time an Elastic Endpoint Security alert is received. Enabling this rule allows you to immediately begin investigating your Endpoint alerts.', + source: { + port: 59406, + ip: '10.62.210.191', + }, + 'kibana.alert.rule.tags': ['Elastic', 'Endpoint Security'], + 'kibana.alert.rule.producer': 'siem', + 'kibana.alert.rule.to': 'now', + 'kibana.alert.rule.created_by': 'elastic2', + 'kibana.alert.rule.timestamp_override': 'event.ingested', + 'kibana.alert.original_event.ingested': '2022-07-18T15:05:03Z', + Endpoint: { + capabilities: [], + configuration: { + isolation: false, + }, + state: { + isolation: false, + }, + status: 'enrolled', + policy: { + applied: { + name: 'Default', + id: '00000000-0000-0000-0000-000000000000', + endpoint_policy_version: 1, + version: 3, + status: 'warning', + }, + }, + }, + ecs: { + version: '1.6.0', + }, + 'kibana.alert.risk_score': 47, + host: { + hostname: 'Host-v3mqhhvt7z', + os: { + Ext: { + variant: 'Windows Server', + }, + name: 'Windows', + family: 'windows', + version: '10.0', + platform: 'Windows', + full: 'Windows Server 2016', + }, + ip: ['10.54.27.117'], + name: 'Host-v3mqhhvt7z', + id: 'c97faec9-1f07-4052-9900-977946b22816', + mac: ['c9-e1-b9-3f-fc-5c'], + architecture: 'n467gryzu6', + }, + 'kibana.alert.rule.name': 'Endpoint Security', + 'event.agent_id_status': 'auth_metadata_missing', + 'event.kind': 'signal', + 'kibana.alert.original_event.id': '041b6fdc-ed62-4836-9542-babe4ea89775', + 'kibana.alert.workflow_status': 'open', + 'kibana.alert.rule.uuid': '1a6dbdc0-06aa-11ed-b2d1-8f870d1699ab', + 'kibana.alert.original_event.category': 'behavior', + 'kibana.alert.rule.risk_score_mapping': [ + { + field: 'event.risk_score', + value: '', + operator: 'equals', + }, + ], + 'kibana.alert.original_event.module': 'endpoint', + 'kibana.alert.rule.interval': '5m', + 'kibana.alert.reason': + 'behavior event with process lsass.exe, file fake_behavior.exe,:59406,:443, on Host-v3mqhhvt7z created medium alert Endpoint Security.', + 'kibana.alert.rule.type': 'query', + 'kibana.alert.rule.immutable': true, + 'event.code': 'behavior', + 'kibana.alert.original_event.type': 'info', + 'kibana.alert.depth': 1, + 'kibana.alert.rule.enabled': true, + 'kibana.alert.rule.version': 3, + 'kibana.alert.rule.from': 'now-10m', + 'kibana.alert.rule.parameters': { + severity_mapping: [ + { + severity: 'low', + field: 'event.severity', + value: '21', + operator: 'equals', + }, + { + severity: 'medium', + field: 'event.severity', + value: '47', + operator: 'equals', + }, + { + severity: 'high', + field: 'event.severity', + value: '73', + operator: 'equals', + }, + { + severity: 'critical', + field: 'event.severity', + value: '99', + operator: 'equals', + }, + ], + references: [], + description: + 'Generates a detection alert each time an Elastic Endpoint Security alert is received. Enabling this rule allows you to immediately begin investigating your Endpoint alerts.', + language: 'kuery', + type: 'query', + rule_name_override: 'message', + exceptions_list: [ + { + list_id: 'endpoint_list', + namespace_type: 'agnostic', + id: 'endpoint_list', + type: 'endpoint', + }, + ], + timestamp_override: 'event.ingested', + from: 'now-10m', + severity: 'medium', + max_signals: 10000, + risk_score: 47, + risk_score_mapping: [ + { + field: 'event.risk_score', + value: '', + operator: 'equals', + }, + ], + author: ['Elastic'], + query: 'event.kind:alert and event.module:(endpoint and not endgame)\n', + index: ['logs-endpoint.alerts-*'], + version: 3, + rule_id: '9a1a2dae-0b5f-4c3d-8305-a268d404c306', + license: 'Elastic License v2', + required_fields: [], + immutable: true, + related_integrations: [], + setup: '', + false_positives: [], + threat: [], + to: 'now', + }, + 'kibana.alert.status': 'active', + destination: { + port: 443, + ip: '10.108.205.18', + }, + 'kibana.alert.ancestors': [ + { + depth: 0, + index: '.ds-logs-endpoint.alerts-default-2022.07.18-000001', + id: 'HHXXEYIBRKQBoH3JXndW', + type: 'event', + }, + ], + rule: { + description: 'Behavior rule description', + id: '5e73f4b2-2d31-4ad7-912f-545a6a719262', + }, + 'kibana.alert.original_event.dataset': 'endpoint.diagnostic.collection', + 'kibana.alert.rule.exceptions_list': [ + { + list_id: 'endpoint_list', + namespace_type: 'agnostic', + id: 'endpoint_list', + type: 'endpoint', + }, + ], + 'kibana.alert.rule.actions': [], + network: { + transport: 'tcp', + type: 'ipv4', + direction: 'outgoing', + }, + 'event.sequence': 548, + 'kibana.alert.rule.rule_type_id': 'siem.queryRule', + file: { + path: 'C:/fake_behavior.exe', + name: 'fake_behavior.exe', + }, + 'event.module': 'endpoint', + 'kibana.alert.rule.license': 'Elastic License v2', + 'kibana.alert.original_event.kind': 'alert', + registry: { + path: 'HKEY_USERS\\S-1-5-21-2460036010-3910878774-3458087990-1001\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run\\chrome', + data: { + strings: 'C:/fake_behavior/lsass.exe', + }, + value: 'lsass.exe', + }, + 'kibana.alert.rule.severity_mapping': [ + { + severity: 'low', + field: 'event.severity', + value: '21', + operator: 'equals', + }, + { + severity: 'medium', + field: 'event.severity', + value: '47', + operator: 'equals', + }, + { + severity: 'high', + field: 'event.severity', + value: '73', + operator: 'equals', + }, + { + severity: 'critical', + field: 'event.severity', + value: '99', + operator: 'equals', + }, + ], + process: { + Ext: { + ancestry: ['kvcs5wsatx', '3ckuzyk9ei'], + code_signature: [ + { + trusted: false, + subject_name: 'bad signer', + }, + ], + user: 'SYSTEM', + token: { + integrity_level_name: 'high', + elevation_level: 'full', + }, + }, + parent: { + pid: 1, + entity_id: 'kvcs5wsatx', + }, + group_leader: { + name: 'fake leader', + pid: 201, + entity_id: 'ah4nfw1r7k', + }, + session_leader: { + name: 'fake session', + pid: 637, + entity_id: 'ah4nfw1r7k', + }, + code_signature: { + subject_name: 'Microsoft Windows', + status: 'trusted', + }, + entry_leader: { + name: 'fake entry', + pid: 682, + entity_id: 'ah4nfw1r7k', + }, + name: 'lsass.exe', + pid: 2, + entity_id: 'r4um0r7eo7', + executable: 'C:/fake_behavior/lsass.exe', + }, + 'kibana.alert.rule.max_signals': 10000, + dll: [ + { + Ext: { + compile_time: 1534424710, + malware_classification: { + identifier: 'Whitelisted', + score: 0, + threshold: 0, + version: '3.0.0', + }, + mapped_address: 5362483200, + mapped_size: 0, + }, + path: 'C:\\Program Files\\Cybereason ActiveProbe\\AmSvc.exe', + code_signature: { + trusted: true, + subject_name: 'Cybereason Inc', + }, + pe: { + architecture: 'x64', + }, + hash: { + sha1: 'ca85243c0af6a6471bdaa560685c51eefd6dbc0d', + sha256: '8ad40c90a611d36eb8f9eb24fa04f7dbca713db383ff55a03aa0f382e92061a2', + md5: '1f2d082566b0fc5f2c238a5180db7451', + }, + }, + ], + 'kibana.alert.rule.updated_at': '2022-07-18T14:58:38.295Z', + 'kibana.alert.rule.risk_score': 47, + 'kibana.alert.rule.author': ['Elastic'], + 'kibana.alert.rule.false_positives': [], + 'kibana.alert.rule.consumer': 'siem', + 'kibana.alert.rule.indices': ['logs-endpoint.alerts-*'], + 'kibana.alert.rule.category': 'Custom Query Rule', + 'kibana.alert.original_event.sequence': 548, + 'event.ingested': '2022-07-18T15:05:03Z', + 'event.action': 'rule_detection', + '@timestamp': '2022-07-18T15:08:48.065Z', + 'kibana.alert.rule.created_at': '2022-07-18T14:58:38.295Z', + 'kibana.alert.original_event.action': 'rule_detection', + 'kibana.alert.rule.severity': 'medium', + data_stream: { + namespace: 'default', + type: 'logs', + dataset: 'endpoint.alerts', + }, + 'event.type': 'info', + 'kibana.alert.original_event.agent_id_status': 'auth_metadata_missing', + elastic: { + agent: { + id: '465cb546-3483-4d60-976a-6df687597fab', + }, + }, + 'kibana.alert.rule.execution.uuid': '455933fc-319b-444b-a6c1-b7df2532634b', + 'kibana.space_ids': ['default'], + 'kibana.alert.uuid': '78a423405fbcf380ea734588533df7812c41c5f9dff5581d687474d430f13c72', + 'kibana.version': '8.4.0', + 'event.id': '041b6fdc-ed62-4836-9542-babe4ea89775', + 'event.dataset': 'endpoint.diagnostic.collection', + 'kibana.alert.original_time': '2022-07-18T16:04:04.265Z', + 'kibana.alert.rule.rule_id': '9a1a2dae-0b5f-4c3d-8305-a268d404c306', + }, + ], + count: 10, +}; diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/__mocks__/timeline.ts b/x-pack/plugins/security_solution/server/lib/telemetry/__mocks__/timeline.ts index 7b3d124e2508f..3f5dd8e1755a5 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/__mocks__/timeline.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/__mocks__/timeline.ts @@ -284,6 +284,11 @@ export const stubEndpointAlertResponse = () => { }, ], }, + aggregations: { + endpoint_alert_count: { + value: 1, + }, + }, }; }; diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/filterlists/endpoint_alerts.ts b/x-pack/plugins/security_solution/server/lib/telemetry/filterlists/endpoint_alerts.ts index 34777cc225260..089c7592c3848 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/filterlists/endpoint_alerts.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/filterlists/endpoint_alerts.ts @@ -42,6 +42,7 @@ const baseAllowlistFields: AllowlistFields = { // Allow list for event-related fields, which can also be nested under events[] const allowlistBaseEventFields: AllowlistFields = { + credential_access: true, dll: { name: true, path: true, diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/receiver.ts b/x-pack/plugins/security_solution/server/lib/telemetry/receiver.ts index 4d93834790b6c..ae089e5566c5d 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/receiver.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/receiver.ts @@ -142,7 +142,7 @@ export interface ITelemetryReceiver { type: string; }; - fetchPrebuiltRuleAlerts(): Promise; + fetchPrebuiltRuleAlerts(): Promise<{ events: TelemetryEvent[]; count: number }>; fetchTimelineEndpointAlerts( interval: number @@ -650,6 +650,13 @@ export class TelemetryReceiver implements ITelemetryReceiver { ], }, }, + aggs: { + prebuilt_rule_alert_count: { + cardinality: { + field: 'event.id', + }, + }, + }, }, }; @@ -660,7 +667,11 @@ export class TelemetryReceiver implements ITelemetryReceiver { h._source != null ? ([h._source] as TelemetryEvent[]) : [] ); - return telemetryEvents; + const aggregations = response.body?.aggregations as unknown as { + prebuilt_rule_alert_count: { value: number }; + }; + + return { events: telemetryEvents, count: aggregations?.prebuilt_rule_alert_count.value ?? 0 }; } public async fetchTimelineEndpointAlerts(interval: number) { @@ -710,6 +721,13 @@ export class TelemetryReceiver implements ITelemetryReceiver { ], }, }, + aggs: { + endpoint_alert_count: { + cardinality: { + field: 'event.id', + }, + }, + }, }, }; diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts b/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts index dbe9a2f1f2fa6..bb0cc6b1707c0 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts @@ -33,6 +33,46 @@ describe('TelemetryEventsSender', () => { const sender = new TelemetryEventsSender(logger); const input = [ { + credential_access: { + Target: { + process: { + path: 'DeviceHarddiskVolume3WindowsSystem32lsass.exe', + pid: 808, + ppid: 584, + sid: 0, + }, + }, + handle_type: 'process', + desired_access_numeric: 2097151, + desired_access: [ + 'DELETE', + 'READ_CONTROL', + 'SYNCHRONIZE', + 'WRITE_DAC', + 'WRITE_OWNER', + 'STANDARD_RIGHTS_REQUIRED', + 'PROCESS_ALL_ACCESS', + ], + call_stack: { + entries: [ + { + memory_address: 140706712704004, + start_address_allocation_offset: 644100, + module_path: 'DeviceHarddiskVolume3WindowsSystem32\ntdll.dll', + }, + { + memory_address: 140706669379902, + start_address_allocation_offset: 180542, + module_path: 'DeviceHarddiskVolume3WindowsSystem32KernelBase.dll', + }, + { + memory_address: 140704414232808, + start_address_allocation_offset: 127208, + module_path: 'Unbacked', + }, + ], + }, + }, event: { kind: 'alert', id: 'test', @@ -116,6 +156,46 @@ describe('TelemetryEventsSender', () => { const result = sender.processEvents(input); expect(result).toStrictEqual([ { + credential_access: { + Target: { + process: { + path: 'DeviceHarddiskVolume3WindowsSystem32lsass.exe', + pid: 808, + ppid: 584, + sid: 0, + }, + }, + handle_type: 'process', + desired_access_numeric: 2097151, + desired_access: [ + 'DELETE', + 'READ_CONTROL', + 'SYNCHRONIZE', + 'WRITE_DAC', + 'WRITE_OWNER', + 'STANDARD_RIGHTS_REQUIRED', + 'PROCESS_ALL_ACCESS', + ], + call_stack: { + entries: [ + { + memory_address: 140706712704004, + start_address_allocation_offset: 644100, + module_path: 'DeviceHarddiskVolume3WindowsSystem32\ntdll.dll', + }, + { + memory_address: 140706669379902, + start_address_allocation_offset: 180542, + module_path: 'DeviceHarddiskVolume3WindowsSystem32KernelBase.dll', + }, + { + memory_address: 140704414232808, + start_address_allocation_offset: 127208, + module_path: 'Unbacked', + }, + ], + }, + }, event: { kind: 'alert', id: 'test', diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/tasks/prebuilt_rule_alerts.test.ts b/x-pack/plugins/security_solution/server/lib/telemetry/tasks/prebuilt_rule_alerts.test.ts index cc784f54a33e0..a7eaa3ca58b12 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/tasks/prebuilt_rule_alerts.test.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/tasks/prebuilt_rule_alerts.test.ts @@ -8,6 +8,12 @@ import { loggingSystemMock } from '@kbn/core/server/mocks'; import { createTelemetryPrebuiltRuleAlertsTaskConfig } from './prebuilt_rule_alerts'; import { createMockTelemetryEventsSender, createMockTelemetryReceiver } from '../__mocks__'; +import { usageCountersServiceMock } from '@kbn/usage-collection-plugin/server/usage_counters/usage_counters_service.mock'; + +const usageCountersServiceSetup = usageCountersServiceMock.createSetupContract(); +const telemetryUsageCounter = usageCountersServiceSetup.createUsageCounter( + 'testTelemetryUsageCounter' +); describe('security telemetry - detection rule alerts task test', () => { let logger: ReturnType; @@ -22,6 +28,9 @@ describe('security telemetry - detection rule alerts task test', () => { current: new Date().toISOString(), }; const mockTelemetryEventsSender = createMockTelemetryEventsSender(); + mockTelemetryEventsSender.getTelemetryUsageCluster = jest + .fn() + .mockReturnValue(telemetryUsageCounter); const mockTelemetryReceiver = createMockTelemetryReceiver(); const telemetryDetectionRuleAlertsTaskConfig = createTelemetryPrebuiltRuleAlertsTaskConfig(1); @@ -32,5 +41,11 @@ describe('security telemetry - detection rule alerts task test', () => { mockTelemetryEventsSender, testTaskExecutionPeriod ); + expect(mockTelemetryReceiver.fetchPrebuiltRuleAlerts).toHaveBeenCalled(); + expect(mockTelemetryEventsSender.getTelemetryUsageCluster).toHaveBeenCalled(); + expect(mockTelemetryEventsSender.getTelemetryUsageCluster()?.incrementCounter).toBeCalledTimes( + 1 + ); + expect(mockTelemetryEventsSender.sendOnDemand).toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/tasks/prebuilt_rule_alerts.ts b/x-pack/plugins/security_solution/server/lib/telemetry/tasks/prebuilt_rule_alerts.ts index 184663e5a19bd..081bf82a944e3 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/tasks/prebuilt_rule_alerts.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/tasks/prebuilt_rule_alerts.ts @@ -43,7 +43,15 @@ export function createTelemetryPrebuiltRuleAlertsTaskConfig(maxTelemetryBatch: n ? licenseInfoPromise.value : ({} as ESLicense | undefined); - const telemetryEvents = await receiver.fetchPrebuiltRuleAlerts(); + const { events: telemetryEvents, count: totalPrebuiltAlertCount } = + await receiver.fetchPrebuiltRuleAlerts(); + + sender.getTelemetryUsageCluster()?.incrementCounter({ + counterName: 'telemetry_prebuilt_rule_alerts', + counterType: 'prebuilt_alert_count', + incrementBy: totalPrebuiltAlertCount, + }); + if (telemetryEvents.length === 0) { logger.debug('no prebuilt rule alerts retrieved'); return 0; diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/tasks/timelines.ts b/x-pack/plugins/security_solution/server/lib/telemetry/tasks/timelines.ts index 2b729843c19a5..88da76f545768 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/tasks/timelines.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/tasks/timelines.ts @@ -68,8 +68,16 @@ export function createTelemetryTimelineTaskConfig() { const endpointAlerts = await receiver.fetchTimelineEndpointAlerts(3); - // No EP Alerts -> Nothing to do + const aggregations = endpointAlerts?.aggregations as unknown as { + endpoint_alert_count: { value: number }; + }; + sender.getTelemetryUsageCluster()?.incrementCounter({ + counterName: 'telemetry_endpoint_alert', + counterType: 'endpoint_alert_count', + incrementBy: aggregations?.endpoint_alert_count.value, + }); + // No EP Alerts -> Nothing to do if ( endpointAlerts.hits.hits?.length === 0 || endpointAlerts.hits.hits?.length === undefined diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 0fe1adc22f880..b8a70d5b338ef 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -72,10 +72,7 @@ import { PolicyWatcher } from './endpoint/lib/policy/license_watch'; import { migrateArtifactsToFleet } from './endpoint/lib/artifacts/migrate_artifacts_to_fleet'; import aadFieldConversion from './lib/detection_engine/routes/index/signal_aad_mapping.json'; import previewPolicy from './lib/detection_engine/routes/index/preview_policy.json'; -import { - registerEventLogProvider, - ruleExecutionLogForExecutorsFactory, -} from './lib/detection_engine/rule_execution_log'; +import { createRuleExecutionLogService } from './lib/detection_engine/rule_monitoring'; import { getKibanaPrivilegesFeaturePrivileges, getCasesKibanaFeature } from './features'; import { EndpointMetadataService } from './endpoint/services/metadata'; import type { CreateRuleOptions } from './lib/detection_engine/rule_types/types'; @@ -150,8 +147,8 @@ export class Plugin implements ISecuritySolutionPlugin { initSavedObjects(core.savedObjects); initUiSettings(core.uiSettings, experimentalFeatures); - const eventLogService = plugins.eventLog; - registerEventLogProvider(eventLogService); + const ruleExecutionLogService = createRuleExecutionLogService(config, logger, core, plugins); + ruleExecutionLogService.registerEventLogProvider(); const requestContextFactory = new RequestContextFactory({ config, @@ -159,6 +156,7 @@ export class Plugin implements ISecuritySolutionPlugin { core, plugins, endpointAppContextService: this.endpointAppContextService, + ruleExecutionLogService, }); const router = core.http.createRouter(); @@ -180,7 +178,7 @@ export class Plugin implements ISecuritySolutionPlugin { initUsageCollectors({ core, - eventLogIndex: eventLogService.getIndexPattern(), + eventLogIndex: plugins.eventLog.getIndexPattern(), signalsIndex: DEFAULT_ALERTS_INDEX, ml: plugins.ml, usageCollection: plugins.usageCollection, @@ -242,8 +240,7 @@ export class Plugin implements ISecuritySolutionPlugin { logger: this.logger, config: this.config, ruleDataClient, - eventLogService, - ruleExecutionLoggerFactory: ruleExecutionLogForExecutorsFactory, + ruleExecutionLoggerFactory: ruleExecutionLogService.createClientForExecutors, version: pluginContext.env.packageInfo.version, }; diff --git a/x-pack/plugins/security_solution/server/request_context_factory.ts b/x-pack/plugins/security_solution/server/request_context_factory.ts index 72050aba3f9db..7812dd4d7c040 100644 --- a/x-pack/plugins/security_solution/server/request_context_factory.ts +++ b/x-pack/plugins/security_solution/server/request_context_factory.ts @@ -13,7 +13,7 @@ import type { FleetAuthz } from '@kbn/fleet-plugin/common'; import { DEFAULT_SPACE_ID } from '../common/constants'; import { AppClientFactory } from './client'; import type { ConfigType } from './config'; -import { ruleExecutionLogForRoutesFactory } from './lib/detection_engine/rule_execution_log'; +import type { IRuleExecutionLogService } from './lib/detection_engine/rule_monitoring'; import { buildFrameworkRequest } from './lib/timeline/utils/common'; import type { SecuritySolutionPluginCoreSetupDependencies, @@ -45,6 +45,7 @@ interface ConstructorOptions { core: SecuritySolutionPluginCoreSetupDependencies; plugins: SecuritySolutionPluginSetupDependencies; endpointAppContextService: EndpointAppContextService; + ruleExecutionLogService: IRuleExecutionLogService; } export class RequestContextFactory implements IRequestContextFactory { @@ -59,7 +60,7 @@ export class RequestContextFactory implements IRequestContextFactory { request: KibanaRequest ): Promise { const { options, appClientFactory } = this; - const { config, logger, core, plugins, endpointAppContextService } = options; + const { config, core, plugins, endpointAppContextService, ruleExecutionLogService } = options; const { lists, ruleRegistry, security } = plugins; const [, startPlugins] = await core.getStartServices(); @@ -109,11 +110,10 @@ export class RequestContextFactory implements IRequestContextFactory { getRuleDataService: () => ruleRegistry.ruleDataService, getRuleExecutionLog: memoize(() => - ruleExecutionLogForRoutesFactory( - coreContext.savedObjects.client, - startPlugins.eventLog.getClient(request), - logger - ) + ruleExecutionLogService.createClientForRoutes({ + savedObjectsClient: coreContext.savedObjects.client, + eventLogClient: startPlugins.eventLog.getClient(request), + }) ), getExceptionListClient: () => { diff --git a/x-pack/plugins/security_solution/server/routes/index.ts b/x-pack/plugins/security_solution/server/routes/index.ts index 129f611e4aa96..98b6b5ec5933f 100644 --- a/x-pack/plugins/security_solution/server/routes/index.ts +++ b/x-pack/plugins/security_solution/server/routes/index.ts @@ -35,7 +35,7 @@ import { deleteRulesBulkRoute } from '../lib/detection_engine/routes/rules/delet import { performBulkActionRoute } from '../lib/detection_engine/routes/rules/perform_bulk_action_route'; import { importRulesRoute } from '../lib/detection_engine/routes/rules/import_rules_route'; import { exportRulesRoute } from '../lib/detection_engine/routes/rules/export_rules_route'; -import { getRuleExecutionEventsRoute } from '../lib/detection_engine/routes/rules/get_rule_execution_events_route'; +import { registerRuleMonitoringRoutes } from '../lib/detection_engine/rule_monitoring'; import { getPrepackagedRulesStatusRoute } from '../lib/detection_engine/routes/rules/get_prepackaged_rules_status_route'; import { createTimelinesRoute, @@ -117,7 +117,7 @@ export const initRoutes = ( deleteRulesBulkRoute(router, logger); performBulkActionRoute(router, ml, logger); - getRuleExecutionEventsRoute(router); + registerRuleMonitoringRoutes(router); getInstalledIntegrationsRoute(router, logger); diff --git a/x-pack/plugins/security_solution/server/saved_objects.ts b/x-pack/plugins/security_solution/server/saved_objects.ts index e8943c66b3ad7..225c0ad453a3d 100644 --- a/x-pack/plugins/security_solution/server/saved_objects.ts +++ b/x-pack/plugins/security_solution/server/saved_objects.ts @@ -10,7 +10,7 @@ import type { CoreSetup } from '@kbn/core/server'; import { noteType, pinnedEventType, timelineType } from './lib/timeline/saved_object_mappings'; // eslint-disable-next-line no-restricted-imports import { legacyType as legacyRuleActionsType } from './lib/detection_engine/rule_actions/legacy_saved_object_mappings'; -import { ruleExecutionType } from './lib/detection_engine/rule_execution_log'; +import { ruleExecutionType } from './lib/detection_engine/rule_monitoring'; import { ruleAssetType } from './lib/detection_engine/rules/rule_asset/rule_asset_saved_object_mappings'; import { type as signalsMigrationType } from './lib/detection_engine/migrations/saved_objects'; import { diff --git a/x-pack/plugins/security_solution/server/types.ts b/x-pack/plugins/security_solution/server/types.ts index 7e9a29b4dd0b5..73b5216ae5e9a 100644 --- a/x-pack/plugins/security_solution/server/types.ts +++ b/x-pack/plugins/security_solution/server/types.ts @@ -20,7 +20,7 @@ import type { IRuleDataService } from '@kbn/rule-registry-plugin/server'; import { AppClient } from './client'; import type { ConfigType } from './config'; -import type { IRuleExecutionLogForRoutes } from './lib/detection_engine/rule_execution_log'; +import type { IRuleExecutionLogForRoutes } from './lib/detection_engine/rule_monitoring'; import type { FrameworkRequest } from './lib/framework'; import type { EndpointAuthz } from '../common/endpoint/types/authz'; import type { diff --git a/x-pack/plugins/security_solution/server/ui_settings.ts b/x-pack/plugins/security_solution/server/ui_settings.ts index ddc41ae3e2926..4b8fe1be50ec0 100644 --- a/x-pack/plugins/security_solution/server/ui_settings.ts +++ b/x-pack/plugins/security_solution/server/ui_settings.ts @@ -33,8 +33,11 @@ import { NEWS_FEED_URL_SETTING_DEFAULT, ENABLE_CCS_READ_WARNING_SETTING, SHOW_RELATED_INTEGRATIONS_SETTING, + EXTENDED_RULE_EXECUTION_LOGGING_ENABLED_SETTING, + EXTENDED_RULE_EXECUTION_LOGGING_MIN_LEVEL_SETTING, } from '../common/constants'; import type { ExperimentalFeatures } from '../common/experimental_features'; +import { LogLevelSetting } from '../common/detection_engine/rule_monitoring'; type SettingsConfig = Record>; @@ -264,6 +267,103 @@ export const initUiSettings = ( requiresPageReload: true, schema: schema.boolean(), }, + ...(experimentalFeatures.extendedRuleExecutionLoggingEnabled + ? { + [EXTENDED_RULE_EXECUTION_LOGGING_ENABLED_SETTING]: { + name: i18n.translate( + 'xpack.securitySolution.uiSettings.extendedRuleExecutionLoggingEnabledLabel', + { + defaultMessage: 'Extended rule execution logging', + } + ), + description: i18n.translate( + 'xpack.securitySolution.uiSettings.extendedRuleExecutionLoggingEnabledDescription', + { + defaultMessage: + '

Enables extended rule execution logging to .kibana-event-log-* indices. Shows plain execution events on the Rule Details page.

', + } + ), + type: 'boolean', + schema: schema.boolean(), + value: true, + category: [APP_ID], + requiresPageReload: false, + }, + [EXTENDED_RULE_EXECUTION_LOGGING_MIN_LEVEL_SETTING]: { + name: i18n.translate( + 'xpack.securitySolution.uiSettings.extendedRuleExecutionLoggingMinLevelLabel', + { + defaultMessage: 'Extended rule execution logging: min level', + } + ), + description: i18n.translate( + 'xpack.securitySolution.uiSettings.extendedRuleExecutionLoggingMinLevelDescription', + { + defaultMessage: + '

Sets minimum log level starting from which rules will write extended logs to .kibana-event-log-* indices. This affects only events of type Message, other events are being written to .kibana-event-log-* regardless of this setting and their log level.

', + } + ), + type: 'select', + schema: schema.oneOf([ + schema.literal(LogLevelSetting.off), + schema.literal(LogLevelSetting.error), + schema.literal(LogLevelSetting.warn), + schema.literal(LogLevelSetting.info), + schema.literal(LogLevelSetting.debug), + schema.literal(LogLevelSetting.trace), + ]), + value: LogLevelSetting.error, + options: [ + LogLevelSetting.off, + LogLevelSetting.error, + LogLevelSetting.warn, + LogLevelSetting.info, + LogLevelSetting.debug, + LogLevelSetting.trace, + ], + optionLabels: { + [LogLevelSetting.off]: i18n.translate( + 'xpack.securitySolution.uiSettings.extendedRuleExecutionLoggingMinLevelOff', + { + defaultMessage: 'Off', + } + ), + [LogLevelSetting.error]: i18n.translate( + 'xpack.securitySolution.uiSettings.extendedRuleExecutionLoggingMinLevelError', + { + defaultMessage: 'Error', + } + ), + [LogLevelSetting.warn]: i18n.translate( + 'xpack.securitySolution.uiSettings.extendedRuleExecutionLoggingMinLevelWarn', + { + defaultMessage: 'Warn', + } + ), + [LogLevelSetting.info]: i18n.translate( + 'xpack.securitySolution.uiSettings.extendedRuleExecutionLoggingMinLevelInfo', + { + defaultMessage: 'Info', + } + ), + [LogLevelSetting.debug]: i18n.translate( + 'xpack.securitySolution.uiSettings.extendedRuleExecutionLoggingMinLevelDebug', + { + defaultMessage: 'Debug', + } + ), + [LogLevelSetting.trace]: i18n.translate( + 'xpack.securitySolution.uiSettings.extendedRuleExecutionLoggingMinLevelTrace', + { + defaultMessage: 'Trace', + } + ), + }, + category: [APP_ID], + requiresPageReload: false, + }, + } + : {}), }; uiSettings.register(orderSettings(securityUiSettings)); diff --git a/x-pack/plugins/security_solution/server/usage/queries/utils/get_search_for_all_rules.ts b/x-pack/plugins/security_solution/server/usage/queries/utils/get_search_for_all_rules.ts index c74ce1a2262c3..acd2a7aaa7ba7 100644 --- a/x-pack/plugins/security_solution/server/usage/queries/utils/get_search_for_all_rules.ts +++ b/x-pack/plugins/security_solution/server/usage/queries/utils/get_search_for_all_rules.ts @@ -7,7 +7,7 @@ import type { SearchRequest } from '@elastic/elasticsearch/lib/api/types'; import type { AggregationsAggregationContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { RULE_EXECUTION_LOG_PROVIDER } from '../../../lib/detection_engine/rule_execution_log/event_log/constants'; +import { RULE_EXECUTION_LOG_PROVIDER } from '../../../lib/detection_engine/rule_monitoring'; /** * Given an aggregation of "aggs" this will return a search for all rules within 24 hours. diff --git a/x-pack/plugins/security_solution/server/usage/queries/utils/get_search_for_custom_rules.ts b/x-pack/plugins/security_solution/server/usage/queries/utils/get_search_for_custom_rules.ts index 88f70469e57ee..88470ee2f3cae 100644 --- a/x-pack/plugins/security_solution/server/usage/queries/utils/get_search_for_custom_rules.ts +++ b/x-pack/plugins/security_solution/server/usage/queries/utils/get_search_for_custom_rules.ts @@ -9,7 +9,7 @@ import type { AggregationsAggregationContainer, SearchRequest, } from '@elastic/elasticsearch/lib/api/types'; -import { RULE_EXECUTION_LOG_PROVIDER } from '../../../lib/detection_engine/rule_execution_log/event_log/constants'; +import { RULE_EXECUTION_LOG_PROVIDER } from '../../../lib/detection_engine/rule_monitoring'; /** * Given an aggregation of "aggs" this will return a search for rules that are NOT elastic diff --git a/x-pack/plugins/security_solution/server/usage/queries/utils/get_search_for_elastic_rules.ts b/x-pack/plugins/security_solution/server/usage/queries/utils/get_search_for_elastic_rules.ts index c2624fedb6e60..4cd8bb0ae17a8 100644 --- a/x-pack/plugins/security_solution/server/usage/queries/utils/get_search_for_elastic_rules.ts +++ b/x-pack/plugins/security_solution/server/usage/queries/utils/get_search_for_elastic_rules.ts @@ -9,7 +9,7 @@ import type { AggregationsAggregationContainer, SearchRequest, } from '@elastic/elasticsearch/lib/api/types'; -import { RULE_EXECUTION_LOG_PROVIDER } from '../../../lib/detection_engine/rule_execution_log/event_log/constants'; +import { RULE_EXECUTION_LOG_PROVIDER } from '../../../lib/detection_engine/rule_monitoring'; /** * Given an aggregation of "aggs" this will return a search for rules that are elastic diff --git a/x-pack/plugins/security_solution/tsconfig.json b/x-pack/plugins/security_solution/tsconfig.json index 0e1f49e52858e..7a8d161ad062f 100644 --- a/x-pack/plugins/security_solution/tsconfig.json +++ b/x-pack/plugins/security_solution/tsconfig.json @@ -32,19 +32,20 @@ { "path": "../actions/tsconfig.json" }, { "path": "../alerting/tsconfig.json" }, { "path": "../cases/tsconfig.json" }, + { "path": "../cloud_security_posture/tsconfig.json" }, { "path": "../encrypted_saved_objects/tsconfig.json" }, { "path": "../features/tsconfig.json" }, { "path": "../fleet/tsconfig.json" }, + { "path": "../kubernetes_security/tsconfig.json" }, { "path": "../licensing/tsconfig.json" }, { "path": "../lists/tsconfig.json" }, { "path": "../maps/tsconfig.json" }, { "path": "../ml/tsconfig.json" }, { "path": "../osquery/tsconfig.json" }, - { "path": "../spaces/tsconfig.json" }, - { "path": "../security/tsconfig.json" }, - { "path": "../timelines/tsconfig.json" }, { "path": "../session_view/tsconfig.json" }, - { "path": "../kubernetes_security/tsconfig.json" }, - { "path": "../cloud_security_posture/tsconfig.json" } + { "path": "../security/tsconfig.json" }, + { "path": "../spaces/tsconfig.json" }, + { "path": "../threat_intelligence/tsconfig.json" }, + { "path": "../timelines/tsconfig.json" } ] } diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_summary/tabs_content/availability_sparklines.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_summary/tabs_content/availability_sparklines.tsx new file mode 100644 index 0000000000000..3a9cfcf72812a --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_summary/tabs_content/availability_sparklines.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { ReportTypes, useTheme } from '@kbn/observability-plugin/public'; +import { useParams } from 'react-router-dom'; +import { ClientPluginsStart } from '../../../../../plugin'; + +export const AvailabilitySparklines = () => { + const { + services: { + observability: { ExploratoryViewEmbeddable }, + }, + } = useKibana(); + const { monitorId } = useParams<{ monitorId: string }>(); + + const theme = useTheme(); + + return ( + <> + + + ); +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_summary/tabs_content/summary_tab_content.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_summary/tabs_content/summary_tab_content.tsx index 402967e2e8d61..e17f31c155a03 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_summary/tabs_content/summary_tab_content.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_summary/tabs_content/summary_tab_content.tsx @@ -13,6 +13,7 @@ import { MonitorDurationTrend } from './duration_trend'; import { StepDurationPanel } from './step_duration_panel'; import { AvailabilityPanel } from './availability_panel'; import { MonitorDetailsPanel } from './monitor_details_panel'; +import { AvailabilitySparklines } from './availability_sparklines'; export const SummaryTabContent = () => { return ( @@ -27,17 +28,20 @@ export const SummaryTabContent = () => { - +

{LAST_30DAYS_LABEL}

- + - {/* TODO: Add availability sparkline*/} + + + {/* TODO: Add duration metric*/} {/* TODO: Add duration metric sparkline*/} + {/* TODO: Add error metric and sparkline*/}
diff --git a/x-pack/plugins/synthetics/server/legacy_uptime/lib/lib.ts b/x-pack/plugins/synthetics/server/legacy_uptime/lib/lib.ts index dfc040bc974a6..fc6b570b034b9 100644 --- a/x-pack/plugins/synthetics/server/legacy_uptime/lib/lib.ts +++ b/x-pack/plugins/synthetics/server/legacy_uptime/lib/lib.ts @@ -9,7 +9,7 @@ import { ElasticsearchClient, SavedObjectsClientContract, KibanaRequest } from ' import chalk from 'chalk'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { ESSearchResponse } from '@kbn/core/types/elasticsearch'; -import { RequestStatus } from '@kbn/inspector-plugin'; +import { RequestStatus } from '@kbn/inspector-plugin/common'; import { getInspectResponse } from '@kbn/observability-plugin/server'; import { InspectResponse } from '@kbn/observability-plugin/typings/common'; import { UMLicenseCheck } from './domains'; diff --git a/x-pack/plugins/threat_intelligence/.storybook/main.js b/x-pack/plugins/threat_intelligence/.storybook/main.js new file mode 100644 index 0000000000000..86b48c32f103e --- /dev/null +++ b/x-pack/plugins/threat_intelligence/.storybook/main.js @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = require('@kbn/storybook').defaultConfig; diff --git a/x-pack/plugins/threat_intelligence/CONTRIBUTING.md b/x-pack/plugins/threat_intelligence/CONTRIBUTING.md new file mode 100644 index 0000000000000..d27b26442e56f --- /dev/null +++ b/x-pack/plugins/threat_intelligence/CONTRIBUTING.md @@ -0,0 +1,135 @@ +# Contributing + +Before contributing to this plugin, make sure you read the [contributing guide for Kibana](https://github.com/elastic/kibana/blob/main/CONTRIBUTING.md), as well as the [STYLEGUIDE](https://github.com/elastic/kibana/blob/main/STYLEGUIDE.mdx) and [TYPESCRIPT](https://github.com/elastic/kibana/blob/main/TYPESCRIPT.md) md files.. + +> Kibana recommends working on a fork of the [elastic/kibana repository](https://github.com/elastic/kibana) (see [here](https://docs.github.com/en/get-started/quickstart/fork-a-repo) to learn about forks). + +> This plugin uses TypeScript, see Kibana's recommendation here. + +## Submitting a Pull Request (PR) + +Before you submit your PR, consider the following guidelines: + +1. Be sure that an issue describes the problem you're fixing, or documents the design for the feature you'd like to add. + +2. Make your changes in a new git branch. + + ``` + git checkout -b my-branch main + ``` + +3. Commit your changes using a descriptive commit message that follows our commit message conventions: + + ``` + git commit -a + ``` + +4. Push your branch to GitHub: + + ``` + git push origin my-fix-branch + ``` + +5. In GitHub, create a PR. + + Note: If changes are suggested, then make the required updates, [rebase](https://hackernoon.com/git-merge-vs-rebase-whats-the-diff-76413c117333) your branch, and force push (this will update your PR): + + ``` + git rebase main -i + git push -f + ``` + +## Commit Message Guidelines + +> **Note:** These guidelines are **recommended - not mandatory**. + +### Commit Message Format + +Each commit message consists of a **header**, **body**, and **footer**. + +``` + + + + +