diff --git a/src/legacy/server/logging/log_format.js b/src/legacy/server/logging/log_format.js index 8a80cbef1a9c5..6edda8c4be907 100644 --- a/src/legacy/server/logging/log_format.js +++ b/src/legacy/server/logging/log_format.js @@ -91,7 +91,7 @@ export default class TransformObjStream extends Stream.Transform { method: event.method || '', headers: event.headers, remoteAddress: source.remoteAddress, - userAgent: source.remoteAddress, + userAgent: source.userAgent, referer: source.referer, }; diff --git a/src/legacy/server/logging/log_format_json.test.js b/src/legacy/server/logging/log_format_json.test.js index f4fb939750566..ec7296d21672b 100644 --- a/src/legacy/server/logging/log_format_json.test.js +++ b/src/legacy/server/logging/log_format_json.test.js @@ -65,12 +65,14 @@ describe('KbnLoggerJsonFormat', () => { }, }; const result = await createPromiseFromStreams([createListStream([event]), format]); - const { type, method, statusCode, message } = JSON.parse(result); + const { type, method, statusCode, message, req } = JSON.parse(result); expect(type).toBe('response'); expect(method).toBe('GET'); expect(statusCode).toBe(200); expect(message).toBe('GET /path/to/resource 200 12000ms - 13.0B'); + expect(req.remoteAddress).toBe('127.0.0.1'); + expect(req.userAgent).toBe('Test Thing'); }); it('ops', async () => { diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 1a93959e56998..94353647da61c 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -1473,7 +1473,7 @@ export interface QueryState { // Warning: (ae-missing-release-tag) "QueryStringInput" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const QueryStringInput: React.FC>; +export const QueryStringInput: React.FC>; // @public (undocumented) export type QuerySuggestion = QuerySuggestionBasic | QuerySuggestionField; diff --git a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx index 2d311fd88eb39..0bfac2a07a7eb 100644 --- a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx @@ -19,7 +19,7 @@ import React, { Component, RefObject, createRef } from 'react'; import { i18n } from '@kbn/i18n'; - +import classNames from 'classnames'; import { EuiTextArea, EuiOutsideClickDetector, @@ -62,6 +62,7 @@ interface Props { onSubmit?: (query: Query) => void; dataTestSubj?: string; size?: SuggestionsListSize; + className?: string; } interface State { @@ -586,9 +587,12 @@ export class QueryStringInputUI extends Component { 'aria-owns': 'kbnTypeahead__items', }; const ariaCombobox = { ...isSuggestionsVisible, role: 'combobox' }; - + const className = classNames( + 'euiFormControlLayout euiFormControlLayout--group kbnQueryBar__wrap', + this.props.className + ); return ( -
+
{this.props.prepend}
{ + mockWidth = jest.spyOn($.prototype, 'width').mockImplementation(function (width) { + if (width === undefined) { + return currentWidth; + } else { + currentWidth = width; + return this; + } + }); + mockHeight = jest.spyOn($.prototype, 'height').mockImplementation(function (height) { + if (height === undefined) { + return currentHeight; + } else { + currentHeight = height; + return this; + } + }); + angular.element.prototype.width = jest.fn(function (width) { + if (width === undefined) { + return currentJqLiteWidth; + } else { + currentJqLiteWidth = width; + return this; + } + }); + angular.element.prototype.offset = jest.fn(() => ({ top: 0 })); + }); + + beforeEach(() => { + currentJqLiteWidth = 120; + initAngularBootstrap(); + + angular.mock.module(testModuleName); + angular.mock.inject(($compile, $rootScope, $timeout) => { + flushPendingTasks = function flushPendingTasks() { + $rootScope.$digest(); + $timeout.flush(); + }; + + compile = function (ratioY, ratioX) { + if (ratioX == null) ratioX = ratioY; + + // since the directive works at the sibling level we create a + // parent for everything to happen in + const $parent = $('
').css({ + position: 'fixed', + top: 0, + left: 0, + right: 0, + bottom: 0, + }); + + $parent.appendTo(document.body); + trash.push($parent); + + const $el = $('
') + .css({ + 'overflow-x': 'auto', + width: $parent.width(), + }) + .appendTo($parent); + + spyScrollWidth = jest.spyOn(window.HTMLElement.prototype, 'scrollWidth', 'get'); + spyScrollWidth.mockReturnValue($parent.width() * ratioX); + angular.element.prototype.height = jest.fn(() => $parent.height() * ratioY); + + const $content = $('
') + .css({ + width: $parent.width() * ratioX, + height: $parent.height() * ratioY, + }) + .appendTo($el); + + $compile($parent)($rootScope); + flushPendingTasks(); + + return { + $container: $el, + $content: $content, + $scroller: $parent.find('.fixed-scroll-scroller'), + }; + }; + }); + }); + + afterEach(function () { + trash.splice(0).forEach(function ($el) { + $el.remove(); + }); + + sandbox.restore(); + spyScrollWidth.mockRestore(); + }); + + afterAll(() => { + mockWidth.mockRestore(); + mockHeight.mockRestore(); + delete angular.element.prototype.width; + delete angular.element.prototype.height; + delete angular.element.prototype.offset; + }); + + test('does nothing when not needed', function () { + let els = compile(0.5, 1.5); + expect(els.$scroller).toHaveLength(0); + + els = compile(1.5, 0.5); + expect(els.$scroller).toHaveLength(0); + }); + + test('attaches a scroller below the element when the content is larger then the container', function () { + const els = compile(1.5); + expect(els.$scroller.length).toBe(1); + }); + + test('copies the width of the container', function () { + const els = compile(1.5); + expect(els.$scroller.width()).toBe(els.$container.width()); + }); + + test('mimics the scrollWidth of the element', function () { + const els = compile(1.5); + expect(els.$scroller.prop('scrollWidth')).toBe(els.$container.prop('scrollWidth')); + }); + + describe('scroll event handling / tug of war prevention', function () { + test('listens when needed, unlistens when not needed', function (done) { + const on = sandbox.spy($.fn, 'on'); + const off = sandbox.spy($.fn, 'off'); + const jqLiteOn = sandbox.spy(angular.element.prototype, 'on'); + const jqLiteOff = sandbox.spy(angular.element.prototype, 'off'); + + const els = compile(1.5); + expect(on.callCount).toBe(1); + expect(jqLiteOn.callCount).toBe(1); + checkThisVals('$.fn.on', on, jqLiteOn); + + expect(off.callCount).toBe(0); + expect(jqLiteOff.callCount).toBe(0); + currentJqLiteWidth = els.$container.prop('scrollWidth'); + flushPendingTasks(); + expect(off.callCount).toBe(1); + expect(jqLiteOff.callCount).toBe(1); + checkThisVals('$.fn.off', off, jqLiteOff); + done(); + + function checkThisVals(namejQueryFn, spyjQueryFn, spyjqLiteFn) { + // the this values should be different + expect(spyjQueryFn.thisValues[0].is(spyjqLiteFn.thisValues[0])).toBeFalsy(); + // but they should be either $scroller or $container + const el = spyjQueryFn.thisValues[0]; + + if (el.is(els.$scroller) || el.is(els.$container)) return; + + done.fail('expected ' + namejQueryFn + ' to be called with $scroller or $container'); + } + }); + + // Turn off this row because tests failed. + // Scroll event is not catched in fixed_scroll. + // As container is jquery element in test but inside fixed_scroll it's a jqLite element. + // it would need jquery in jest to make this work. + [ + //{ from: '$container', to: '$scroller' }, + { from: '$scroller', to: '$container' }, + ].forEach(function (names) { + describe('scroll events ' + JSON.stringify(names), function () { + let spyJQueryScrollLeft; + let spyJQLiteScrollLeft; + let els; + let $from; + let $to; + + beforeEach(function () { + spyJQueryScrollLeft = sandbox.spy($.fn, 'scrollLeft'); + spyJQLiteScrollLeft = sandbox.stub(); + angular.element.prototype.scrollLeft = spyJQLiteScrollLeft; + els = compile(1.5); + $from = els[names.from]; + $to = els[names.to]; + }); + + test('transfers the scrollLeft', function () { + expect(spyJQueryScrollLeft.callCount).toBe(0); + expect(spyJQLiteScrollLeft.callCount).toBe(0); + $from.scroll(); + expect(spyJQueryScrollLeft.callCount).toBe(1); + expect(spyJQLiteScrollLeft.callCount).toBe(1); + + // first call should read the scrollLeft from the $container + const firstCall = spyJQueryScrollLeft.getCall(0); + expect(firstCall.args).toEqual([]); + + // second call should be setting the scrollLeft on the $scroller + const secondCall = spyJQLiteScrollLeft.getCall(0); + expect(secondCall.args).toEqual([firstCall.returnValue]); + }); + + /** + * In practice, calling $el.scrollLeft() causes the "scroll" event to trigger, + * but the browser seems to be very careful about triggering the event too much + * and I can't reliably recreate the browsers behavior in a test. So... faking it! + */ + test('prevents tug of war by ignoring echo scroll events', function () { + $from.scroll(); + expect(spyJQueryScrollLeft.callCount).toBe(1); + expect(spyJQLiteScrollLeft.callCount).toBe(1); + + spyJQueryScrollLeft.resetHistory(); + spyJQLiteScrollLeft.resetHistory(); + $to.scroll(); + expect(spyJQueryScrollLeft.callCount).toBe(0); + expect(spyJQLiteScrollLeft.callCount).toBe(0); + }); + }); + }); + }); +}); diff --git a/src/plugins/telemetry/README.md b/src/plugins/telemetry/README.md index 196d596fb784f..0a05facfbbe0f 100644 --- a/src/plugins/telemetry/README.md +++ b/src/plugins/telemetry/README.md @@ -7,3 +7,61 @@ Telemetry allows Kibana features to have usage tracked in the wild. The general 3. Viewing usage data in the Kibana instance of the telemetry cluster (Viewing). This plugin is responsible for sending usage data to the telemetry cluster. For collecting usage data, use the [`usageCollection` plugin](../usage_collection/README.md) + +## Telemetry Plugin public API + +### Setup + +The `setup` function exposes the following interface: + +- `getTelemetryUrl: () => Promise`: + An async function that resolves into the telemetry Url used to send telemetry. The url is wrapped with node's [URL constructor](https://nodejs.org/api/url.html). Here is an example on how to grab the url origin: + ``` + const telemetryUrl = await getTelemetryUrl(); + > telemetryUrl.origin; // 'https://telemetry.elastic.co' + ``` + Note that the telemetry URL is a kibana.yml configuration hence it is recommended to call the `getTelemetryUrl` everytime before using the actual url. + +### Start + +The `start` function exposes the following interface: + +- `async getIsOptedIn(): Promise`: + An async function that resolves into `true` if the user has opted into send Elastic usage data. + Resolves to `false` if the user explicitly opted out of sending usage data to Elastic or did not choose + to opt-in or out yet after a minor or major upgrade (only when previously opted out). + +### Usage + +To use the exposed plugin start and setup contracts: + +1. Make sure `telemetry` is in your `optionalPlugins` in the `kibana.json` file: + +```json5 +// /kibana.json +{ +"id": "...", +"optionalPlugins": ["telemetry"] +} +``` + +2. Use the exposed contracts: +```ts +// /server/plugin.ts + +import { TelemetryPluginsStart } from '../telemetry/server`; + +interface MyPlyginStartDeps { + telemetry?: TelemetryPluginsStart; +} + +class MyPlugin { + public async start( + core: CoreStart, + { telemetry }: MyPlyginStartDeps + ) { + const isOptedIn = await telemetry?.getIsOptedIn(); + ... + } +} +``` diff --git a/src/plugins/telemetry/server/index.ts b/src/plugins/telemetry/server/index.ts index 42259d2e5187c..e9887456e2f36 100644 --- a/src/plugins/telemetry/server/index.ts +++ b/src/plugins/telemetry/server/index.ts @@ -24,7 +24,7 @@ import { configSchema, TelemetryConfigType } from './config'; export { FetcherTask } from './fetcher'; export { handleOldSettings } from './handle_old_settings'; -export { TelemetryPluginsSetup } from './plugin'; +export { TelemetryPluginSetup, TelemetryPluginStart } from './plugin'; export const config: PluginConfigDescriptor = { schema: configSchema, diff --git a/src/plugins/telemetry/server/mocks.ts b/src/plugins/telemetry/server/mocks.ts new file mode 100644 index 0000000000000..8952dd619f426 --- /dev/null +++ b/src/plugins/telemetry/server/mocks.ts @@ -0,0 +1,46 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { URL } from 'url'; +import { TelemetryPluginStart, TelemetryPluginSetup } from './plugin'; + +export type Setup = jest.Mocked; +export type Start = jest.Mocked; + +export const telemetryPluginMock = { + createSetupContract, + createStartContract, +}; + +function createSetupContract(): Setup { + const telemetryUrl = new URL('https://telemetry-staging.elastic.co/xpack/MOCK_URL/send'); + const setupContract: Setup = { + getTelemetryUrl: jest.fn().mockResolvedValue(telemetryUrl), + }; + + return setupContract; +} + +function createStartContract(): Start { + const startContract: Start = { + getIsOptedIn: jest.fn(), + }; + + return startContract; +} diff --git a/src/plugins/telemetry/server/plugin.ts b/src/plugins/telemetry/server/plugin.ts index bd7a2a8c1a8ca..005c5f96d98d0 100644 --- a/src/plugins/telemetry/server/plugin.ts +++ b/src/plugins/telemetry/server/plugin.ts @@ -17,12 +17,14 @@ * under the License. */ +import { URL } from 'url'; import { Observable } from 'rxjs'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { TelemetryCollectionManagerPluginSetup, TelemetryCollectionManagerPluginStart, } from 'src/plugins/telemetry_collection_manager/server'; +import { take } from 'rxjs/operators'; import { CoreSetup, PluginInitializerContext, @@ -42,19 +44,38 @@ import { import { TelemetryConfigType } from './config'; import { FetcherTask } from './fetcher'; import { handleOldSettings } from './handle_old_settings'; +import { getTelemetrySavedObject } from './telemetry_repository'; +import { getTelemetryOptIn } from '../common/telemetry_config'; -export interface TelemetryPluginsSetup { +interface TelemetryPluginsDepsSetup { usageCollection: UsageCollectionSetup; telemetryCollectionManager: TelemetryCollectionManagerPluginSetup; } -export interface TelemetryPluginsStart { +interface TelemetryPluginsDepsStart { telemetryCollectionManager: TelemetryCollectionManagerPluginStart; } +export interface TelemetryPluginSetup { + /** + * Resolves into the telemetry Url used to send telemetry. + * The url is wrapped with node's [URL constructor](https://nodejs.org/api/url.html). + */ + getTelemetryUrl: () => Promise; +} + +export interface TelemetryPluginStart { + /** + * Resolves `true` if the user has opted into send Elastic usage data. + * Resolves `false` if the user explicitly opted out of sending usage data to Elastic + * or did not choose to opt-in or out -yet- after a minor or major upgrade (only when previously opted-out). + */ + getIsOptedIn: () => Promise; +} + type SavedObjectsRegisterType = CoreSetup['savedObjects']['registerType']; -export class TelemetryPlugin implements Plugin { +export class TelemetryPlugin implements Plugin { private readonly logger: Logger; private readonly currentKibanaVersion: string; private readonly config$: Observable; @@ -76,8 +97,8 @@ export class TelemetryPlugin implements Plugin { public async setup( { elasticsearch, http, savedObjects }: CoreSetup, - { usageCollection, telemetryCollectionManager }: TelemetryPluginsSetup - ) { + { usageCollection, telemetryCollectionManager }: TelemetryPluginsDepsSetup + ): Promise { const currentKibanaVersion = this.currentKibanaVersion; const config$ = this.config$; const isDev = this.isDev; @@ -96,9 +117,19 @@ export class TelemetryPlugin implements Plugin { this.registerMappings((opts) => savedObjects.registerType(opts)); this.registerUsageCollectors(usageCollection); + + return { + getTelemetryUrl: async () => { + const config = await config$.pipe(take(1)).toPromise(); + return new URL(config.url); + }, + }; } - public async start(core: CoreStart, { telemetryCollectionManager }: TelemetryPluginsStart) { + public async start( + core: CoreStart, + { telemetryCollectionManager }: TelemetryPluginsDepsStart + ): Promise { const { savedObjects, uiSettings } = core; this.savedObjectsClient = savedObjects.createInternalRepository(); const savedObjectsClient = new SavedObjectsClient(this.savedObjectsClient); @@ -111,6 +142,25 @@ export class TelemetryPlugin implements Plugin { } this.fetcherTask.start(core, { telemetryCollectionManager }); + + return { + getIsOptedIn: async () => { + const internalRepository = new SavedObjectsClient(savedObjects.createInternalRepository()); + const telemetrySavedObject = await getTelemetrySavedObject(internalRepository!); + const config = await this.config$.pipe(take(1)).toPromise(); + const allowChangingOptInStatus = config.allowChangingOptInStatus; + const configTelemetryOptIn = typeof config.optIn === 'undefined' ? null : config.optIn; + const currentKibanaVersion = this.currentKibanaVersion; + const isOptedIn = getTelemetryOptIn({ + currentKibanaVersion, + telemetrySavedObject, + allowChangingOptInStatus, + configTelemetryOptIn, + }); + + return isOptedIn === true; + }, + }; } private registerMappings(registerType: SavedObjectsRegisterType) { diff --git a/src/plugins/visualizations/server/saved_objects/visualization_migrations.test.ts b/src/plugins/visualizations/server/saved_objects/visualization_migrations.test.ts index 05f00e12c172e..dc8bae69ca377 100644 --- a/src/plugins/visualizations/server/saved_objects/visualization_migrations.test.ts +++ b/src/plugins/visualizations/server/saved_objects/visualization_migrations.test.ts @@ -1578,4 +1578,46 @@ describe('migration visualization', () => { expect(metric.denominator).toHaveProperty('language'); }); }); + + describe('7.10.0 remove tsvb search source', () => { + const migrate = (doc: any) => + visualizationSavedObjectTypeMigrations['7.10.0']( + doc as Parameters[0], + savedObjectMigrationContext + ); + const generateDoc = (visState: any) => ({ + attributes: { + title: 'My Vis', + description: 'This is my super cool vis.', + visState: JSON.stringify(visState), + uiStateJSON: '{}', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ + filter: [], + query: { + query: { + query_string: { + query: '*', + }, + }, + language: 'lucene', + }, + }), + }, + }, + }); + + it('should remove the search source JSON', () => { + const timeSeriesDoc = generateDoc({ type: 'metrics' }); + const migratedtimeSeriesDoc = migrate(timeSeriesDoc); + expect(migratedtimeSeriesDoc.attributes.kibanaSavedObjectMeta.searchSourceJSON).toEqual('{}'); + const { kibanaSavedObjectMeta, ...attributes } = migratedtimeSeriesDoc.attributes; + const { + kibanaSavedObjectMeta: oldKibanaSavedObjectMeta, + ...oldAttributes + } = migratedtimeSeriesDoc.attributes; + expect(attributes).toEqual(oldAttributes); + }); + }); }); diff --git a/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts b/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts index 64491d02aa0a3..170d7c460b06a 100644 --- a/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts +++ b/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts @@ -721,6 +721,35 @@ const migrateTsvbDefaultColorPalettes: SavedObjectMigrationFn = (doc) return doc; }; +// [TSVB] Remove serialized search source as it's not used in TSVB visualizations +const removeTSVBSearchSource: SavedObjectMigrationFn = (doc) => { + const visStateJSON = get(doc, 'attributes.visState'); + let visState; + + const searchSourceJSON = get(doc, 'attributes.kibanaSavedObjectMeta.searchSourceJSON'); + + if (visStateJSON) { + try { + visState = JSON.parse(visStateJSON); + } catch (e) { + // Let it go, the data is invalid and we'll leave it as is + } + if (visState && visState.type === 'metrics' && searchSourceJSON !== '{}') { + return { + ...doc, + attributes: { + ...doc.attributes, + kibanaSavedObjectMeta: { + ...get(doc, 'attributes.kibanaSavedObjectMeta'), + searchSourceJSON: '{}', + }, + }, + }; + } + } + return doc; +}; + export const visualizationSavedObjectTypeMigrations = { /** * We need to have this migration twice, once with a version prior to 7.0.0 once with a version @@ -752,5 +781,5 @@ export const visualizationSavedObjectTypeMigrations = { '7.4.2': flow(transformSplitFiltersStringToQueryObject), '7.7.0': flow(migrateOperatorKeyTypo, migrateSplitByChartRow), '7.8.0': flow(migrateTsvbDefaultColorPalettes), - '7.10.0': flow(migrateFilterRatioQuery), + '7.10.0': flow(migrateFilterRatioQuery, removeTSVBSearchSource), }; diff --git a/test/functional/apps/visualize/_vega_chart.ts b/test/functional/apps/visualize/_vega_chart.ts index f599afa3afc32..b59d9590bb62a 100644 --- a/test/functional/apps/visualize/_vega_chart.ts +++ b/test/functional/apps/visualize/_vega_chart.ts @@ -50,8 +50,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const retry = getService('retry'); const browser = getService('browser'); - // FLAKY: https://github.com/elastic/kibana/issues/75699 - describe.skip('vega chart in visualize app', () => { + describe('vega chart in visualize app', () => { before(async () => { log.debug('navigateToApp visualize'); await PageObjects.visualize.navigateToNewVisualization(); diff --git a/test/functional/page_objects/vega_chart_page.ts b/test/functional/page_objects/vega_chart_page.ts index 6c64f9dda2efd..557c6bfada01e 100644 --- a/test/functional/page_objects/vega_chart_page.ts +++ b/test/functional/page_objects/vega_chart_page.ts @@ -94,8 +94,9 @@ export function VegaChartPageProvider({ const aceGutter = await this.getAceGutterContainer(); await aceGutter.doubleClick(); - await browser.pressKeys(Key.LEFT); await browser.pressKeys(Key.RIGHT); + await browser.pressKeys(Key.LEFT); + await browser.pressKeys(Key.LEFT); await browser.pressKeys(text); } diff --git a/test/scripts/jenkins_security_solution_cypress.sh b/test/scripts/jenkins_security_solution_cypress.sh index a5a1a2103801f..c018b632706b1 100755 --- a/test/scripts/jenkins_security_solution_cypress.sh +++ b/test/scripts/jenkins_security_solution_cypress.sh @@ -5,11 +5,11 @@ source test/scripts/jenkins_test_setup_xpack.sh echo " -> Running security solution cypress tests" cd "$XPACK_DIR" -checks-reporter-with-killswitch "Security solution Cypress Tests" \ +checks-reporter-with-killswitch "Security Solution Cypress Tests" \ node scripts/functional_tests \ --debug --bail \ --kibana-install-dir "$KIBANA_INSTALL_DIR" \ - --config test/security_solution_cypress/config.ts + --config test/security_solution_cypress/cli_config.ts echo "" echo "" diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts index acf642f250a7b..4dff70518c115 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PolicyFromES } from '../../../public/application/services/policies/types'; +import { PolicyFromES } from '../../../common/types'; export const POLICY_NAME = 'my_policy'; export const SNAPSHOT_POLICY_NAME = 'my_snapshot_policy'; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.js b/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx similarity index 90% rename from x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.js rename to x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx index e4227bac520fe..28b25c3eb4530 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.js +++ b/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx @@ -4,13 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { ReactElement } from 'react'; import { act } from 'react-dom/test-utils'; import moment from 'moment-timezone'; -// axios has a $http like interface so using it to simulate $http + +import { findTestSubject } from '@elastic/eui/lib/test'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { SinonFakeServer } from 'sinon'; +import { ReactWrapper } from 'enzyme'; import axios from 'axios'; import axiosXhrAdapter from 'axios/lib/adapters/xhr'; -import { findTestSubject } from '@elastic/eui/lib/test'; import { init as initHttpRequests } from './helpers/http_requests'; import { @@ -19,11 +22,11 @@ import { } from '../../../../../src/core/public/mocks'; import { usageCollectionPluginMock } from '../../../../../src/plugins/usage_collection/public/mocks'; -import { mountWithIntl } from '../../../../test_utils/enzyme_helpers'; import { EditPolicy } from '../../public/application/sections/edit_policy/edit_policy'; import { init as initHttp } from '../../public/application/services/http'; import { init as initUiMetric } from '../../public/application/services/ui_metric'; import { init as initNotification } from '../../public/application/services/notification'; +import { PolicyFromES } from '../../common/types'; import { positiveNumbersAboveZeroErrorMessage, positiveNumberRequiredMessage, @@ -38,7 +41,10 @@ import { policyNameAlreadyUsedErrorMessage, maximumDocumentsRequiredMessage, } from '../../public/application/services/policies/policy_validation'; +import { HttpResponse } from './helpers/http_requests'; +import { createMemoryHistory } from 'history'; +// @ts-ignore initHttp(axios.create({ adapter: axiosXhrAdapter })); initUiMetric(usageCollectionPluginMock.createSetupContract()); initNotification( @@ -46,8 +52,13 @@ initNotification( fatalErrorsServiceMock.createSetupContract() ); -let server; -let httpRequestsMockHelpers; +const history = createMemoryHistory(); +let server: SinonFakeServer; +let httpRequestsMockHelpers: { + setPoliciesResponse: (response: HttpResponse) => void; + setNodesListResponse: (response: HttpResponse) => void; + setNodesDetailsResponse: (nodeAttributes: string, response: HttpResponse) => void; +}; const policy = { phases: { hot: { @@ -60,32 +71,33 @@ const policy = { }, }, }; -const policies = []; +const policies: PolicyFromES[] = []; for (let i = 0; i < 105; i++) { policies.push({ version: i, - modified_date: moment().subtract(i, 'days').valueOf(), - linkedIndices: i % 2 === 0 ? [`index${i}`] : null, + modified_date: moment().subtract(i, 'days').toISOString(), + linkedIndices: i % 2 === 0 ? [`index${i}`] : undefined, name: `testy${i}`, policy: { ...policy, + name: `testy${i}`, }, }); } window.scrollTo = jest.fn(); -window.TextEncoder = null; -let component; -const activatePhase = async (rendered, phase) => { + +let component: ReactElement; +const activatePhase = async (rendered: ReactWrapper, phase: string) => { const testSubject = `enablePhaseSwitch-${phase}`; await act(async () => { await findTestSubject(rendered, testSubject).simulate('click'); }); rendered.update(); }; -const expectedErrorMessages = (rendered, expectedErrorMessages) => { +const expectedErrorMessages = (rendered: ReactWrapper, expectedMessages: string[]) => { const errorMessages = rendered.find('.euiFormErrorText'); - expect(errorMessages.length).toBe(expectedErrorMessages.length); - expectedErrorMessages.forEach((expectedErrorMessage) => { + expect(errorMessages.length).toBe(expectedMessages.length); + expectedMessages.forEach((expectedErrorMessage) => { let foundErrorMessage; for (let i = 0; i < errorMessages.length; i++) { if (errorMessages.at(i).text() === expectedErrorMessage) { @@ -95,29 +107,29 @@ const expectedErrorMessages = (rendered, expectedErrorMessages) => { expect(foundErrorMessage).toBe(true); }); }; -const noRollover = (rendered) => { +const noRollover = (rendered: ReactWrapper) => { findTestSubject(rendered, 'rolloverSwitch').simulate('click'); rendered.update(); }; -const getNodeAttributeSelect = (rendered, phase) => { +const getNodeAttributeSelect = (rendered: ReactWrapper, phase: string) => { return rendered.find(`select#${phase}-selectedNodeAttrs`); }; -const setPolicyName = (rendered, policyName) => { +const setPolicyName = (rendered: ReactWrapper, policyName: string) => { const policyNameField = findTestSubject(rendered, 'policyNameField'); policyNameField.simulate('change', { target: { value: policyName } }); rendered.update(); }; -const setPhaseAfter = (rendered, phase, after) => { +const setPhaseAfter = (rendered: ReactWrapper, phase: string, after: string) => { const afterInput = rendered.find(`input#${phase}-selectedMinimumAge`); afterInput.simulate('change', { target: { value: after } }); rendered.update(); }; -const setPhaseIndexPriority = (rendered, phase, priority) => { +const setPhaseIndexPriority = (rendered: ReactWrapper, phase: string, priority: string) => { const priorityInput = rendered.find(`input#${phase}-phaseIndexPriority`); priorityInput.simulate('change', { target: { value: priority } }); rendered.update(); }; -const save = (rendered) => { +const save = (rendered: ReactWrapper) => { const saveButton = findTestSubject(rendered, 'savePolicyButton'); saveButton.simulate('click'); rendered.update(); @@ -125,12 +137,7 @@ const save = (rendered) => { describe('edit policy', () => { beforeEach(() => { component = ( - {} }} - getUrlForApp={() => {}} - policies={policies} - policyName={''} - /> + ); ({ server, httpRequestsMockHelpers } = initHttpRequests()); @@ -162,8 +169,8 @@ describe('edit policy', () => { {}} - history={{ push: () => {} }} + getUrlForApp={jest.fn()} + history={history} /> ); const rendered = mountWithIntl(component); @@ -272,7 +279,7 @@ describe('edit policy', () => { const rendered = mountWithIntl(component); noRollover(rendered); setPolicyName(rendered, 'mypolicy'); - setPhaseIndexPriority(rendered, 'hot', -1); + setPhaseIndexPriority(rendered, 'hot', '-1'); save(rendered); expectedErrorMessages(rendered, [positiveNumberRequiredMessage]); }); @@ -300,7 +307,7 @@ describe('edit policy', () => { noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'warm'); - setPhaseAfter(rendered, 'warm', 0); + setPhaseAfter(rendered, 'warm', '0'); save(rendered); expectedErrorMessages(rendered, []); }); @@ -309,7 +316,7 @@ describe('edit policy', () => { noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'warm'); - setPhaseAfter(rendered, 'warm', -1); + setPhaseAfter(rendered, 'warm', '-1'); save(rendered); expectedErrorMessages(rendered, [positiveNumberRequiredMessage]); }); @@ -318,8 +325,8 @@ describe('edit policy', () => { noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'warm'); - setPhaseAfter(rendered, 'warm', 1); - setPhaseIndexPriority(rendered, 'warm', -1); + setPhaseAfter(rendered, 'warm', '1'); + setPhaseIndexPriority(rendered, 'warm', '-1'); save(rendered); expectedErrorMessages(rendered, [positiveNumberRequiredMessage]); }); @@ -330,7 +337,7 @@ describe('edit policy', () => { await activatePhase(rendered, 'warm'); findTestSubject(rendered, 'shrinkSwitch').simulate('click'); rendered.update(); - setPhaseAfter(rendered, 'warm', 1); + setPhaseAfter(rendered, 'warm', '1'); const shrinkInput = rendered.find('input#warm-selectedPrimaryShardCount'); shrinkInput.simulate('change', { target: { value: '0' } }); rendered.update(); @@ -342,7 +349,7 @@ describe('edit policy', () => { noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'warm'); - setPhaseAfter(rendered, 'warm', 1); + setPhaseAfter(rendered, 'warm', '1'); findTestSubject(rendered, 'shrinkSwitch').simulate('click'); rendered.update(); const shrinkInput = rendered.find('input#warm-selectedPrimaryShardCount'); @@ -356,7 +363,7 @@ describe('edit policy', () => { noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'warm'); - setPhaseAfter(rendered, 'warm', 1); + setPhaseAfter(rendered, 'warm', '1'); findTestSubject(rendered, 'forceMergeSwitch').simulate('click'); rendered.update(); const shrinkInput = rendered.find('input#warm-selectedForceMergeSegments'); @@ -370,7 +377,7 @@ describe('edit policy', () => { noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'warm'); - setPhaseAfter(rendered, 'warm', 1); + setPhaseAfter(rendered, 'warm', '1'); findTestSubject(rendered, 'forceMergeSwitch').simulate('click'); rendered.update(); const shrinkInput = rendered.find('input#warm-selectedForceMergeSegments'); @@ -446,7 +453,7 @@ describe('edit policy', () => { noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'cold'); - setPhaseAfter(rendered, 'cold', 0); + setPhaseAfter(rendered, 'cold', '0'); save(rendered); expectedErrorMessages(rendered, []); }); @@ -455,7 +462,7 @@ describe('edit policy', () => { noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'cold'); - setPhaseAfter(rendered, 'cold', -1); + setPhaseAfter(rendered, 'cold', '-1'); save(rendered); expectedErrorMessages(rendered, [positiveNumberRequiredMessage]); }); @@ -517,8 +524,8 @@ describe('edit policy', () => { noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'cold'); - setPhaseAfter(rendered, 'cold', 1); - setPhaseIndexPriority(rendered, 'cold', -1); + setPhaseAfter(rendered, 'cold', '1'); + setPhaseIndexPriority(rendered, 'cold', '-1'); save(rendered); expectedErrorMessages(rendered, [positiveNumberRequiredMessage]); }); @@ -529,7 +536,7 @@ describe('edit policy', () => { noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'delete'); - setPhaseAfter(rendered, 'delete', 0); + setPhaseAfter(rendered, 'delete', '0'); save(rendered); expectedErrorMessages(rendered, []); }); @@ -538,7 +545,7 @@ describe('edit policy', () => { noRollover(rendered); setPolicyName(rendered, 'mypolicy'); await activatePhase(rendered, 'delete'); - setPhaseAfter(rendered, 'delete', -1); + setPhaseAfter(rendered, 'delete', '-1'); save(rendered); expectedErrorMessages(rendered, [positiveNumberRequiredMessage]); }); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/http_requests.ts b/x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/http_requests.ts index 668cbedbf0c95..6cbe3bdf1f8c6 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/http_requests.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/components/helpers/http_requests.ts @@ -6,7 +6,7 @@ import sinon, { SinonFakeServer } from 'sinon'; -type HttpResponse = Record | any[]; +export type HttpResponse = Record | any[]; const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { const setPoliciesResponse = (response: HttpResponse = []) => { diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/policy_table.test.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/components/policy_table.test.tsx index d95b4503c266b..0d66d9a8cdf9f 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/components/policy_table.test.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/components/policy_table.test.tsx @@ -17,10 +17,10 @@ import { import { HttpService } from '../../../../../src/core/public/http'; import { usageCollectionPluginMock } from '../../../../../src/plugins/usage_collection/public/mocks'; +import { PolicyFromES } from '../../common/types'; import { PolicyTable } from '../../public/application/sections/policy_table/policy_table'; import { init as initHttp } from '../../public/application/services/http'; import { init as initUiMetric } from '../../public/application/services/ui_metric'; -import { PolicyFromES } from '../../public/application/services/policies/types'; initHttp( new HttpService().setup({ diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.tsx index 17573cb81c408..ca3121bf6b7a6 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.tsx @@ -10,6 +10,7 @@ import axiosXhrAdapter from 'axios/lib/adapters/xhr'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { usageCollectionPluginMock } from '../../../../src/plugins/usage_collection/public/mocks'; +import { Index } from '../common/types'; import { retryLifecycleActionExtension, removeLifecyclePolicyActionExtension, @@ -20,7 +21,6 @@ import { } from '../public/extend_index_management'; import { init as initHttp } from '../public/application/services/http'; import { init as initUiMetric } from '../public/application/services/ui_metric'; -import { Index } from '../public/application/services/policies/types'; // We need to init the http with a mock for any tests that depend upon the http service. // For example, add_lifecycle_confirm_modal makes an API request in its componentDidMount diff --git a/x-pack/plugins/index_lifecycle_management/common/types/index.ts b/x-pack/plugins/index_lifecycle_management/common/types/index.ts new file mode 100644 index 0000000000000..fef79c7782bb0 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/common/types/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './policies'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/types.ts b/x-pack/plugins/index_lifecycle_management/common/types/policies.ts similarity index 96% rename from x-pack/plugins/index_lifecycle_management/public/application/services/policies/types.ts rename to x-pack/plugins/index_lifecycle_management/common/types/policies.ts index 0e00b5a02b71d..d88d5b5021a25 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/types.ts +++ b/x-pack/plugins/index_lifecycle_management/common/types/policies.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Index as IndexInterface } from '../../../../../index_management/public'; +import { Index as IndexInterface } from '../../../index_management/common/types'; export interface SerializedPolicy { name: string; @@ -28,7 +28,7 @@ export interface PolicyFromES { } export interface SerializedPhase { - min_age: string; + min_age?: string; actions: { [action: string]: any; }; @@ -94,10 +94,10 @@ export interface SerializedDeletePhase extends SerializedPhase { } export interface AllocateAction { - number_of_replicas: number; + number_of_replicas?: number; include: {}; exclude: {}; - require: { + require?: { [attribute: string]: string; }; } diff --git a/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts b/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts index fb626e7d7fe76..4fd74da06f1b3 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts @@ -11,7 +11,7 @@ import { HotPhase, WarmPhase, FrozenPhase, -} from '../services/policies/types'; +} from '../../../common/types'; export const defaultNewHotPhase: HotPhase = { phaseEnabled: true, diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input.tsx index 5128ba1c881a0..d7edbac3d1c54 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input.tsx @@ -12,7 +12,7 @@ import { EuiFieldNumber, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSelect } from import { LearnMoreLink } from './learn_more_link'; import { ErrableFormRow } from './form_errors'; import { PhaseValidationErrors, propertyof } from '../../../services/policies/policy_validation'; -import { PhaseWithMinAge, Phases } from '../../../services/policies/types'; +import { PhaseWithMinAge, Phases } from '../../../../../common/types'; function getTimingLabelForPhase(phase: keyof Phases) { // NOTE: Hot phase isn't necessary, because indices begin in the hot phase. diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation.tsx index b4ff62bfb03dc..6f80afccbff5e 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation.tsx @@ -20,7 +20,7 @@ import { LearnMoreLink } from './learn_more_link'; import { ErrableFormRow } from './form_errors'; import { useLoadNodes } from '../../../services/api'; import { NodeAttrsDetails } from './node_attrs_details'; -import { PhaseWithAllocationAction, Phases } from '../../../services/policies/types'; +import { PhaseWithAllocationAction, Phases } from '../../../../../common/types'; import { PhaseValidationErrors, propertyof } from '../../../services/policies/policy_validation'; const learnMoreLink = ( diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/policy_json_flyout.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/policy_json_flyout.tsx index 2f246f21aaf2e..98d2409ffea6d 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/policy_json_flyout.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/policy_json_flyout.tsx @@ -18,7 +18,7 @@ import { EuiText, EuiTitle, } from '@elastic/eui'; -import { Policy, PolicyFromES } from '../../../services/policies/types'; +import { Policy, PolicyFromES } from '../../../../../common/types'; import { serializePolicy } from '../../../services/policies/policy_serialization'; interface Props { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/set_priority_input.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/set_priority_input.tsx index 1505532a2b16e..7f839fc94918b 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/set_priority_input.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/set_priority_input.tsx @@ -10,7 +10,7 @@ import { EuiFieldNumber, EuiTextColor, EuiDescribedFormGroup } from '@elastic/eu import { LearnMoreLink } from './'; import { OptionalLabel } from './'; import { ErrableFormRow } from './'; -import { PhaseWithIndexPriority, Phases } from '../../../services/policies/types'; +import { PhaseWithIndexPriority, Phases } from '../../../../../common/types'; import { PhaseValidationErrors, propertyof } from '../../../services/policies/policy_validation'; interface Props { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx index db58c64a8ae8c..f1c287788e08d 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx @@ -5,7 +5,9 @@ */ import React, { Fragment, useEffect, useState } from 'react'; +import { RouteComponentProps } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; + import { i18n } from '@kbn/i18n'; import { @@ -25,10 +27,9 @@ import { EuiText, EuiTitle, } from '@elastic/eui'; - import { toasts } from '../../services/notification'; -import { Phases, Policy, PolicyFromES } from '../../services/policies/types'; +import { Phases, Policy, PolicyFromES } from '../../../../common/types'; import { validatePolicy, ValidationErrors, @@ -54,7 +55,7 @@ interface Props { absolute?: boolean; } ) => string; - history: any; + history: RouteComponentProps['history']; } export const EditPolicy: React.FunctionComponent = ({ policies, diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/cold_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/cold_phase.tsx index 9df6da7a88b2f..ae2858e7a84ae 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/cold_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/cold_phase.tsx @@ -18,7 +18,7 @@ import { EuiTextColor, } from '@elastic/eui'; -import { ColdPhase as ColdPhaseInterface, Phases } from '../../../services/policies/types'; +import { ColdPhase as ColdPhaseInterface, Phases } from '../../../../../common/types'; import { PhaseValidationErrors } from '../../../services/policies/policy_validation'; import { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/delete_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/delete_phase.tsx index eab93777a72bd..11adebdd094bf 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/delete_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/delete_phase.tsx @@ -8,7 +8,7 @@ import React, { PureComponent, Fragment } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiDescribedFormGroup, EuiSwitch, EuiTextColor, EuiFormRow } from '@elastic/eui'; -import { DeletePhase as DeletePhaseInterface, Phases } from '../../../services/policies/types'; +import { DeletePhase as DeletePhaseInterface, Phases } from '../../../../../common/types'; import { PhaseValidationErrors } from '../../../services/policies/policy_validation'; import { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/frozen_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/frozen_phase.tsx index 782906a56a9ba..bfaf141438169 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/frozen_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/frozen_phase.tsx @@ -18,7 +18,7 @@ import { EuiTextColor, } from '@elastic/eui'; -import { FrozenPhase as FrozenPhaseInterface, Phases } from '../../../services/policies/types'; +import { FrozenPhase as FrozenPhaseInterface, Phases } from '../../../../../common/types'; import { PhaseValidationErrors } from '../../../services/policies/policy_validation'; import { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/hot_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/hot_phase.tsx index 106e3b9139a9b..59949ad93fa5d 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/hot_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/hot_phase.tsx @@ -18,7 +18,7 @@ import { EuiDescribedFormGroup, } from '@elastic/eui'; -import { HotPhase as HotPhaseInterface, Phases } from '../../../services/policies/types'; +import { HotPhase as HotPhaseInterface, Phases } from '../../../../../common/types'; import { PhaseValidationErrors } from '../../../services/policies/policy_validation'; import { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/warm_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/warm_phase.tsx index 2733d01ac222d..71286475bcfe9 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/warm_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/warm_phase.tsx @@ -18,6 +18,8 @@ import { EuiDescribedFormGroup, } from '@elastic/eui'; +import { Phases, WarmPhase as WarmPhaseInterface } from '../../../../../common/types'; +import { PhaseValidationErrors } from '../../../services/policies/policy_validation'; import { LearnMoreLink, ActiveBadge, @@ -29,9 +31,6 @@ import { MinAgeInput, } from '../components'; -import { Phases, WarmPhase as WarmPhaseInterface } from '../../../services/policies/types'; -import { PhaseValidationErrors } from '../../../services/policies/policy_validation'; - const shrinkLabel = i18n.translate('xpack.indexLifecycleMgmt.warmPhase.shrinkIndexLabel', { defaultMessage: 'Shrink index', }); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/add_policy_to_template_confirm_modal.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/add_policy_to_template_confirm_modal.tsx index 90ac3c03856de..265d5146b2c37 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/add_policy_to_template_confirm_modal.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/add_policy_to_template_confirm_modal.tsx @@ -20,8 +20,8 @@ import { EuiText, } from '@elastic/eui'; +import { PolicyFromES } from '../../../../../common/types'; import { LearnMoreLink } from '../../edit_policy/components'; -import { PolicyFromES } from '../../../services/policies/types'; import { addLifecyclePolicyToTemplate, loadIndexTemplates } from '../../../services/api'; import { toasts } from '../../../services/notification'; import { showApiError } from '../../../services/api_errors'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/confirm_delete.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/confirm_delete.tsx index 8d8e5ac2a2472..59e213fae846b 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/confirm_delete.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/confirm_delete.tsx @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiOverlayMask, EuiConfirmModal } from '@elastic/eui'; -import { PolicyFromES } from '../../../services/policies/types'; +import { PolicyFromES } from '../../../../../common/types'; import { toasts } from '../../../services/notification'; import { showApiError } from '../../../services/api_errors'; import { deletePolicy } from '../../../services/api'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/table_content.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/table_content.tsx index da36ff4df98f5..3481a2f0d4a2a 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/table_content.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/table_content.tsx @@ -34,7 +34,7 @@ import { METRIC_TYPE } from '@kbn/analytics'; import { RouteComponentProps } from 'react-router-dom'; import { reactRouterNavigate } from '../../../../../../../../src/plugins/kibana_react/public'; import { getIndexListUri } from '../../../../../../index_management/public'; -import { PolicyFromES } from '../../../services/policies/types'; +import { PolicyFromES } from '../../../../../common/types'; import { getPolicyPath } from '../../../services/navigation'; import { sortTable } from '../../../services'; import { trackUiMetric } from '../../../services/ui_metric'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/policy_table.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/policy_table.tsx index 048ab922a65b5..0c396dae75783 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/policy_table.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/policy_table.tsx @@ -23,7 +23,7 @@ import { import { ApplicationStart } from 'kibana/public'; import { RouteComponentProps } from 'react-router-dom'; import { reactRouterNavigate } from '../../../../../../../src/plugins/kibana_react/public'; -import { PolicyFromES } from '../../services/policies/types'; +import { PolicyFromES } from '../../../../common/types'; import { filterItems } from '../../services'; import { TableContent } from './components/table_content'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/api.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/api.ts index e9365bfe06ea4..3d068433becbd 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/api.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/api.ts @@ -6,6 +6,8 @@ import { METRIC_TYPE } from '@kbn/analytics'; +import { PolicyFromES, SerializedPolicy } from '../../../common/types'; + import { UIM_POLICY_DELETE, UIM_POLICY_ATTACH_INDEX, @@ -13,10 +15,8 @@ import { UIM_POLICY_DETACH_INDEX, UIM_INDEX_RETRY_STEP, } from '../constants'; - import { trackUiMetric } from './ui_metric'; import { sendGet, sendPost, sendDelete, useRequest } from './http'; -import { PolicyFromES, SerializedPolicy } from './policies/types'; interface GenericObject { [key: string]: any; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/cold_phase.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/cold_phase.ts index 7fa82a004b872..3b71c11349752 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/cold_phase.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/cold_phase.ts @@ -5,8 +5,8 @@ */ import { isEmpty } from 'lodash'; +import { AllocateAction, ColdPhase, SerializedColdPhase } from '../../../../common/types'; import { serializedPhaseInitialization } from '../../constants'; -import { AllocateAction, ColdPhase, SerializedColdPhase } from './types'; import { isNumber, splitSizeAndUnits } from './policy_serialization'; import { numberRequiredMessage, @@ -90,7 +90,6 @@ export const coldPhaseToES = ( }; } else { if (esPhase.actions.allocate) { - // @ts-expect-error delete esPhase.actions.allocate.require; } } @@ -100,7 +99,6 @@ export const coldPhaseToES = ( esPhase.actions.allocate.number_of_replicas = parseInt(phase.selectedReplicaCount, 10); } else { if (esPhase.actions.allocate) { - // @ts-expect-error delete esPhase.actions.allocate.number_of_replicas; } } diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/delete_phase.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/delete_phase.ts index 70e7c21da8cb6..6ada039d45cd9 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/delete_phase.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/delete_phase.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { DeletePhase, SerializedDeletePhase } from '../../../../common/types'; import { serializedPhaseInitialization } from '../../constants'; -import { DeletePhase, SerializedDeletePhase } from './types'; import { isNumber, splitSizeAndUnits } from './policy_serialization'; import { numberRequiredMessage, diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/frozen_phase.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/frozen_phase.ts index bad43bfcf8a9c..6249507bcb407 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/frozen_phase.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/frozen_phase.ts @@ -6,7 +6,7 @@ import { isEmpty } from 'lodash'; import { serializedPhaseInitialization } from '../../constants'; -import { AllocateAction, FrozenPhase, SerializedFrozenPhase } from './types'; +import { AllocateAction, FrozenPhase, SerializedFrozenPhase } from '../../../../common/types'; import { isNumber, splitSizeAndUnits } from './policy_serialization'; import { numberRequiredMessage, @@ -90,7 +90,6 @@ export const frozenPhaseToES = ( }; } else { if (esPhase.actions.allocate) { - // @ts-expect-error delete esPhase.actions.allocate.require; } } @@ -100,7 +99,6 @@ export const frozenPhaseToES = ( esPhase.actions.allocate.number_of_replicas = parseInt(phase.selectedReplicaCount, 10); } else { if (esPhase.actions.allocate) { - // @ts-expect-error delete esPhase.actions.allocate.number_of_replicas; } } diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/hot_phase.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/hot_phase.ts index 34ac8f3e270e6..fb7f74efeb66e 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/hot_phase.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/hot_phase.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { HotPhase, SerializedHotPhase } from '../../../../common/types'; import { serializedPhaseInitialization } from '../../constants'; import { isNumber, splitSizeAndUnits } from './policy_serialization'; -import { HotPhase, SerializedHotPhase } from './types'; import { maximumAgeRequiredMessage, maximumDocumentsRequiredMessage, diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_save.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_save.ts index 12df071544952..a96b6f57a0f9f 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_save.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_save.ts @@ -7,12 +7,12 @@ import { i18n } from '@kbn/i18n'; import { METRIC_TYPE } from '@kbn/analytics'; +import { Policy, PolicyFromES } from '../../../../common/types'; import { savePolicy as savePolicyApi } from '../api'; import { showApiError } from '../api_errors'; import { getUiMetricsForPhases, trackUiMetric } from '../ui_metric'; -import { UIM_POLICY_CREATE, UIM_POLICY_UPDATE } from '../../constants/ui_metric'; +import { UIM_POLICY_CREATE, UIM_POLICY_UPDATE } from '../../constants'; import { toasts } from '../notification'; -import { Policy, PolicyFromES } from './types'; import { serializePolicy } from './policy_serialization'; export const savePolicy = async ( diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.ts index 807a6fe8ec395..31c063aba2c4a 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_serialization.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Policy, PolicyFromES, SerializedPolicy } from '../../../../common/types'; + import { defaultNewColdPhase, defaultNewDeletePhase, @@ -13,8 +15,6 @@ import { serializedPhaseInitialization, } from '../../constants'; -import { Policy, PolicyFromES, SerializedPolicy } from './types'; - import { hotPhaseFromES, hotPhaseToES } from './hot_phase'; import { warmPhaseFromES, warmPhaseToES } from './warm_phase'; import { coldPhaseFromES, coldPhaseToES } from './cold_phase'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_validation.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_validation.ts index 6fdbc4babd3f3..f5197e6ffec99 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_validation.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/policy_validation.ts @@ -5,12 +5,6 @@ */ import { i18n } from '@kbn/i18n'; -import { validateHotPhase } from './hot_phase'; -import { validateWarmPhase } from './warm_phase'; -import { validateColdPhase } from './cold_phase'; -import { validateDeletePhase } from './delete_phase'; -import { validateFrozenPhase } from './frozen_phase'; - import { ColdPhase, DeletePhase, @@ -19,7 +13,12 @@ import { Policy, PolicyFromES, WarmPhase, -} from './types'; +} from '../../../../common/types'; +import { validateHotPhase } from './hot_phase'; +import { validateWarmPhase } from './warm_phase'; +import { validateColdPhase } from './cold_phase'; +import { validateDeletePhase } from './delete_phase'; +import { validateFrozenPhase } from './frozen_phase'; export const propertyof = (propertyName: keyof T & string) => propertyName; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/warm_phase.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/warm_phase.ts index c331f4ccce38f..cc815d67dbc18 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/policies/warm_phase.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/policies/warm_phase.ts @@ -5,8 +5,8 @@ */ import { isEmpty } from 'lodash'; +import { AllocateAction, WarmPhase, SerializedWarmPhase } from '../../../../common/types'; import { serializedPhaseInitialization } from '../../constants'; -import { AllocateAction, WarmPhase, SerializedWarmPhase } from './types'; import { isNumber, splitSizeAndUnits } from './policy_serialization'; import { @@ -96,7 +96,6 @@ export const warmPhaseToES = ( // An index lifecycle switches to warm phase when rollover occurs, so you cannot specify a warm phase time // They are mutually exclusive if (phase.warmPhaseOnRollover) { - // @ts-expect-error delete esPhase.min_age; } @@ -110,7 +109,6 @@ export const warmPhaseToES = ( }; } else { if (esPhase.actions.allocate) { - // @ts-expect-error delete esPhase.actions.allocate.require; } } @@ -120,7 +118,6 @@ export const warmPhaseToES = ( esPhase.actions.allocate.number_of_replicas = parseInt(phase.selectedReplicaCount, 10); } else { if (esPhase.actions.allocate) { - // @ts-expect-error delete esPhase.actions.allocate.number_of_replicas; } } diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/sort_table.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/sort_table.ts index 6b41d671b673f..509c827904232 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/sort_table.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/sort_table.ts @@ -5,7 +5,7 @@ */ import { sortBy } from 'lodash'; -import { PolicyFromES } from './policies/types'; +import { PolicyFromES } from '../../../common/types'; export const sortTable = ( array: PolicyFromES[] = [], diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.ts index b38a734770546..aeb2c8ce917c6 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/ui_metric.ts @@ -18,9 +18,9 @@ import { defaultNewWarmPhase, } from '../constants'; -import { Phases } from './policies/types'; +import { Phases } from '../../../common/types'; -export let trackUiMetric = (metricType: UiStatsMetricType, eventName: string) => {}; +export let trackUiMetric = (metricType: UiStatsMetricType, eventName: string | string[]) => {}; export function init(usageCollection?: UsageCollectionSetup): void { if (usageCollection) { @@ -28,7 +28,7 @@ export function init(usageCollection?: UsageCollectionSetup): void { } } -export function getUiMetricsForPhases(phases: Phases): any { +export function getUiMetricsForPhases(phases: Phases): string[] { const phaseUiMetrics = [ { metric: UIM_CONFIG_COLD_PHASE, @@ -72,7 +72,7 @@ export function getUiMetricsForPhases(phases: Phases): any { }, ]; - return phaseUiMetrics.reduce((tracked: any, { metric, isTracked }) => { + return phaseUiMetrics.reduce((tracked: string[], { metric, isTracked }) => { if (isTracked()) { tracked.push(metric); } diff --git a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/add_lifecycle_confirm_modal.tsx b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/add_lifecycle_confirm_modal.tsx index 060b208006bf3..54b09b95c51ec 100644 --- a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/add_lifecycle_confirm_modal.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/add_lifecycle_confirm_modal.tsx @@ -28,7 +28,7 @@ import { import { loadPolicies, addLifecyclePolicyToIndex } from '../../application/services/api'; import { showApiError } from '../../application/services/api_errors'; import { toasts } from '../../application/services/notification'; -import { Index, PolicyFromES } from '../../application/services/policies/types'; +import { Index, PolicyFromES } from '../../../common/types'; interface Props { indexName: string; diff --git a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/index_lifecycle_summary.tsx b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/index_lifecycle_summary.tsx index 02e4595a333bc..ce36a3650c2ff 100644 --- a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/index_lifecycle_summary.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/index_lifecycle_summary.tsx @@ -26,7 +26,7 @@ import { import { ApplicationStart } from 'kibana/public'; import { getPolicyPath } from '../../application/services/navigation'; -import { Index, IndexLifecyclePolicy } from '../../application/services/policies/types'; +import { Index, IndexLifecyclePolicy } from '../../../common/types'; const getHeaders = (): Array<[keyof IndexLifecyclePolicy, string]> => { return [ diff --git a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.tsx b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.tsx index bb5642cf3a476..e36f376961794 100644 --- a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.tsx @@ -17,7 +17,7 @@ import { IndexLifecycleSummary } from './components/index_lifecycle_summary'; import { AddLifecyclePolicyConfirmModal } from './components/add_lifecycle_confirm_modal'; import { RemoveLifecyclePolicyConfirmModal } from './components/remove_lifecycle_confirm_modal'; -import { Index } from '../application/services/policies/types'; +import { Index } from '../../common/types'; const stepPath = 'ilm.step'; diff --git a/x-pack/plugins/index_lifecycle_management/server/plugin.ts b/x-pack/plugins/index_lifecycle_management/server/plugin.ts index ed17925522610..76d8539eb4a07 100644 --- a/x-pack/plugins/index_lifecycle_management/server/plugin.ts +++ b/x-pack/plugins/index_lifecycle_management/server/plugin.ts @@ -15,16 +15,21 @@ import { LegacyAPICaller, } from 'src/core/server'; +import { Index as IndexWithoutIlm } from '../../index_management/common/types'; import { PLUGIN } from '../common/constants'; +import { Index, IndexLifecyclePolicy } from '../common/types'; import { Dependencies } from './types'; import { registerApiRoutes } from './routes'; import { License } from './services'; import { IndexLifecycleManagementConfig } from './config'; import { isEsError } from './shared_imports'; -const indexLifecycleDataEnricher = async (indicesList: any, callAsCurrentUser: LegacyAPICaller) => { +const indexLifecycleDataEnricher = async ( + indicesList: IndexWithoutIlm[], + callAsCurrentUser: LegacyAPICaller +): Promise => { if (!indicesList || !indicesList.length) { - return; + return []; } const params = { @@ -32,9 +37,11 @@ const indexLifecycleDataEnricher = async (indicesList: any, callAsCurrentUser: L method: 'GET', }; - const { indices: ilmIndicesData } = await callAsCurrentUser('transport.request', params); + const { indices: ilmIndicesData } = await callAsCurrentUser<{ + indices: { [indexName: string]: IndexLifecyclePolicy }; + }>('transport.request', params); - return indicesList.map((index: any): any => { + return indicesList.map((index: IndexWithoutIlm) => { return { ...index, ilm: { ...(ilmIndicesData[index.name] || {}) }, diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_fetch_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_fetch_route.ts index 2dc1ed1006adb..4fb21ea8c6a62 100644 --- a/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_fetch_route.ts +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_fetch_route.ts @@ -7,15 +7,21 @@ import { schema } from '@kbn/config-schema'; import { LegacyAPICaller } from 'src/core/server'; +import { IndexLifecyclePolicy, PolicyFromES } from '../../../../common/types'; import { RouteDependencies } from '../../../types'; import { addBasePath } from '../../../services'; -function formatPolicies(policiesMap: any): any { +type PoliciesMap = { + [K: string]: Omit; +} & { + status?: number; +}; +function formatPolicies(policiesMap: PoliciesMap): PolicyFromES[] { if (policiesMap.status === 404) { return []; } - return Object.keys(policiesMap).reduce((accum: any[], lifecycleName: string) => { + return Object.keys(policiesMap).reduce((accum: PolicyFromES[], lifecycleName: string) => { const policyEntry = policiesMap[lifecycleName]; accum.push({ ...policyEntry, @@ -25,7 +31,7 @@ function formatPolicies(policiesMap: any): any { }, []); } -async function fetchPolicies(callAsCurrentUser: LegacyAPICaller): Promise { +async function fetchPolicies(callAsCurrentUser: LegacyAPICaller): Promise { const params = { method: 'GET', path: '/_ilm/policy', @@ -36,7 +42,7 @@ async function fetchPolicies(callAsCurrentUser: LegacyAPICaller): Promise { return await callAsCurrentUser('transport.request', params); } -async function addLinkedIndices(callAsCurrentUser: LegacyAPICaller, policiesMap: any) { +async function addLinkedIndices(callAsCurrentUser: LegacyAPICaller, policiesMap: PoliciesMap) { if (policiesMap.status === 404) { return policiesMap; } @@ -47,11 +53,13 @@ async function addLinkedIndices(callAsCurrentUser: LegacyAPICaller, policiesMap: ignore: [404], }; - const policyExplanation: any = await callAsCurrentUser('transport.request', params); - Object.entries(policyExplanation.indices).forEach(([indexName, { policy }]: [string, any]) => { + const policyExplanation: { + indices: { [indexName: string]: IndexLifecyclePolicy }; + } = await callAsCurrentUser('transport.request', params); + Object.entries(policyExplanation.indices).forEach(([indexName, { policy }]) => { if (policy && policiesMap[policy]) { policiesMap[policy].linkedIndices = policiesMap[policy].linkedIndices || []; - policiesMap[policy].linkedIndices.push(indexName); + policiesMap[policy].linkedIndices!.push(indexName); } }); } diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/templates/register_add_policy_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/templates/register_add_policy_route.ts index 21a389b9a0e35..c11d981b33dfe 100644 --- a/x-pack/plugins/index_lifecycle_management/server/routes/api/templates/register_add_policy_route.ts +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/templates/register_add_policy_route.ts @@ -8,13 +8,14 @@ import { merge } from 'lodash'; import { schema } from '@kbn/config-schema'; import { LegacyAPICaller } from 'src/core/server'; +import { LegacyTemplateSerialized } from '../../../../../index_management/server'; import { RouteDependencies } from '../../../types'; import { addBasePath } from '../../../services'; async function getIndexTemplate( callAsCurrentUser: LegacyAPICaller, templateName: string -): Promise { +): Promise { const response = await callAsCurrentUser('indices.getTemplate', { name: templateName }); return response[templateName]; } diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/templates/register_fetch_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/templates/register_fetch_route.ts index c8d02783864e1..afbee246af0d9 100644 --- a/x-pack/plugins/index_lifecycle_management/server/routes/api/templates/register_fetch_route.ts +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/templates/register_fetch_route.ts @@ -5,6 +5,7 @@ */ import { LegacyAPICaller } from 'src/core/server'; +import { LegacyTemplateSerialized } from '../../../../../index_management/server'; import { RouteDependencies } from '../../../types'; import { addBasePath } from '../../../services'; @@ -27,7 +28,9 @@ function isReservedSystemTemplate(templateName: string, indexPatterns: string[]) ); } -function filterAndFormatTemplates(templates: any): any { +function filterAndFormatTemplates(templates: { + [templateName: string]: LegacyTemplateSerialized; +}): Array<{}> { const formattedTemplates = []; const templateNames = Object.keys(templates); for (const templateName of templateNames) { @@ -38,10 +41,10 @@ function filterAndFormatTemplates(templates: any): any { } const formattedTemplate = { index_lifecycle_name: - settings.index && settings.index.lifecycle ? settings.index.lifecycle.name : undefined, + settings!.index && settings!.index.lifecycle ? settings!.index.lifecycle.name : undefined, index_patterns, allocation_rules: - settings.index && settings.index.routing ? settings.index.routing : undefined, + settings!.index && settings!.index.routing ? settings!.index.routing : undefined, settings, name: templateName, }; @@ -50,7 +53,9 @@ function filterAndFormatTemplates(templates: any): any { return formattedTemplates; } -async function fetchTemplates(callAsCurrentUser: LegacyAPICaller): Promise { +async function fetchTemplates( + callAsCurrentUser: LegacyAPICaller +): Promise<{ [templateName: string]: LegacyTemplateSerialized }> { const params = { method: 'GET', path: '/_template', diff --git a/x-pack/plugins/index_management/common/types/indices.ts b/x-pack/plugins/index_management/common/types/indices.ts index 354e4fe67cd19..6e471047ffc20 100644 --- a/x-pack/plugins/index_management/common/types/indices.ts +++ b/x-pack/plugins/index_management/common/types/indices.ts @@ -15,6 +15,14 @@ interface IndexModule { number_of_replicas: number; auto_expand_replicas: false | string; lifecycle: LifecycleModule; + routing: { + allocation: { + enable: 'all' | 'primaries' | 'new_primaries' | 'none'; + }; + rebalance: { + enable: 'all' | 'primaries' | 'replicas' | 'none'; + }; + }; } interface AnalysisModule { diff --git a/x-pack/plugins/index_management/server/index.ts b/x-pack/plugins/index_management/server/index.ts index bf52d8a09c84c..b8d9124b4135a 100644 --- a/x-pack/plugins/index_management/server/index.ts +++ b/x-pack/plugins/index_management/server/index.ts @@ -18,5 +18,5 @@ export const config = { /** @public */ export { Dependencies } from './types'; export { IndexManagementPluginSetup } from './plugin'; -export { Index } from '../common'; +export { Index, LegacyTemplateSerialized } from '../common'; export { IndexManagementConfig } from './config'; diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_chart_preview.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_chart_preview.ts index 026f003463ef2..71115ad3a5745 100644 --- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_chart_preview.ts +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_chart_preview.ts @@ -50,8 +50,8 @@ export async function getChartPreviewData( const { rangeFilter } = buildFiltersFromCriteria(expandedAlertParams, timestampField); const query = isGrouped - ? getGroupedESQuery(expandedAlertParams, sourceConfiguration.configuration, indexPattern) - : getUngroupedESQuery(expandedAlertParams, sourceConfiguration.configuration, indexPattern); + ? getGroupedESQuery(expandedAlertParams, timestampField, indexPattern) + : getUngroupedESQuery(expandedAlertParams, timestampField, indexPattern); if (!query) { throw new Error('ES query could not be built from the provided alert params'); diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts index 940afd72f6c73..f730513991a78 100644 --- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts @@ -4,527 +4,617 @@ * you may not use this file except in compliance with the Elastic License. */ -import { createLogThresholdExecutor } from './log_threshold_executor'; +import { + getPositiveComparators, + getNegativeComparators, + queryMappings, + buildFiltersFromCriteria, + getUngroupedESQuery, + getGroupedESQuery, + processUngroupedResults, + processGroupByResults, +} from './log_threshold_executor'; import { Comparator, AlertStates, LogDocumentCountAlertParams, Criterion, + UngroupedSearchQueryResponse, + GroupedSearchQueryResponse, } from '../../../../common/alerting/logs/types'; -import { AlertExecutorOptions } from '../../../../../alerts/server'; -import { - alertsMock, - AlertInstanceMock, - AlertServicesMock, -} from '../../../../../alerts/server/mocks'; -import { libsMock } from './mocks'; - -interface AlertTestInstance { - instance: AlertInstanceMock; - actionQueue: any[]; - state: any; -} - -/* - * Mocks - */ -const alertInstances = new Map(); - -const services: AlertServicesMock = alertsMock.createAlertServices(); -services.alertInstanceFactory.mockImplementation((instanceId: string) => { - const alertInstance: AlertTestInstance = { - instance: alertsMock.createAlertInstanceFactory(), - actionQueue: [], - state: {}, - }; - alertInstance.instance.replaceState.mockImplementation((newState: any) => { - alertInstance.state = newState; - return alertInstance.instance; - }); - alertInstance.instance.scheduleActions.mockImplementation((id: string, action: any) => { - alertInstance.actionQueue.push({ id, action }); - return alertInstance.instance; - }); - - alertInstances.set(instanceId, alertInstance); - - return alertInstance.instance; -}); - -/* - * Helper functions - */ -function getAlertState(): AlertStates { - const alert = alertInstances.get('*'); - if (alert) { - return alert.state.alertState; - } else { - throw new Error('Could not find alert instance'); - } -} - -/* - * Executor instance (our test subject) - */ -const executor = (createLogThresholdExecutor(libsMock) as unknown) as (opts: { - params: LogDocumentCountAlertParams; - services: { callCluster: AlertExecutorOptions['params']['callCluster'] }; -}) => Promise; - -// Wrapper to test -type Comparison = [number, Comparator, number]; - -async function callExecutor( - [value, comparator, threshold]: Comparison, - criteria: Criterion[] = [] -) { - services.callCluster.mockImplementationOnce(async (..._) => ({ - _shards: { - total: 1, - successful: 1, - skipped: 0, - failed: 0, - }, - timed_out: false, - took: 123456789, - hits: { - total: { - value, - }, - }, - })); - - return await executor({ - services, - params: { - count: { value: threshold, comparator }, - timeSize: 1, - timeUnit: 'm', - criteria, - }, - }); -} - -describe('Ungrouped alerts', () => { - describe('Comparators trigger alerts correctly', () => { - it('does not alert when counts do not reach the threshold', async () => { - await callExecutor([0, Comparator.GT, 1]); - expect(getAlertState()).toBe(AlertStates.OK); - - await callExecutor([0, Comparator.GT_OR_EQ, 1]); - expect(getAlertState()).toBe(AlertStates.OK); - - await callExecutor([1, Comparator.LT, 0]); - expect(getAlertState()).toBe(AlertStates.OK); - - await callExecutor([1, Comparator.LT_OR_EQ, 0]); - expect(getAlertState()).toBe(AlertStates.OK); +import { alertsMock } from '../../../../../alerts/server/mocks'; + +// Mocks // +const numericField = { + field: 'numericField', + value: 10, +}; + +const keywordField = { + field: 'keywordField', + value: 'error', +}; + +const textField = { + field: 'textField', + value: 'Something went wrong', +}; + +const positiveCriteria: Criterion[] = [ + { ...numericField, comparator: Comparator.GT }, + { ...numericField, comparator: Comparator.GT_OR_EQ }, + { ...numericField, comparator: Comparator.LT }, + { ...numericField, comparator: Comparator.LT_OR_EQ }, + { ...keywordField, comparator: Comparator.EQ }, + { ...textField, comparator: Comparator.MATCH }, + { ...textField, comparator: Comparator.MATCH_PHRASE }, +]; + +const negativeCriteria: Criterion[] = [ + { ...keywordField, comparator: Comparator.NOT_EQ }, + { ...textField, comparator: Comparator.NOT_MATCH }, + { ...textField, comparator: Comparator.NOT_MATCH_PHRASE }, +]; + +const baseAlertParams: Pick = { + count: { + comparator: Comparator.GT, + value: 5, + }, + timeSize: 5, + timeUnit: 'm', +}; + +const TIMESTAMP_FIELD = '@timestamp'; +const FILEBEAT_INDEX = 'filebeat-*'; + +describe('Log threshold executor', () => { + describe('Comparators', () => { + test('Correctly categorises positive comparators', () => { + expect(getPositiveComparators().length).toBe(7); }); - it('alerts when counts reach the threshold', async () => { - await callExecutor([2, Comparator.GT, 1]); - expect(getAlertState()).toBe(AlertStates.ALERT); - - await callExecutor([1, Comparator.GT_OR_EQ, 1]); - expect(getAlertState()).toBe(AlertStates.ALERT); - - await callExecutor([1, Comparator.LT, 2]); - expect(getAlertState()).toBe(AlertStates.ALERT); - - await callExecutor([2, Comparator.LT_OR_EQ, 2]); - expect(getAlertState()).toBe(AlertStates.ALERT); + test('Correctly categorises negative comparators', () => { + expect(getNegativeComparators().length).toBe(3); }); - }); - describe('Comparators create the correct ES queries', () => { - beforeEach(() => { - services.callCluster.mockReset(); + test('There is a query mapping for every comparator', () => { + const comparators = [...getPositiveComparators(), ...getNegativeComparators()]; + expect(Object.keys(queryMappings).length).toBe(comparators.length); }); - - it('Works with `Comparator.EQ`', async () => { - await callExecutor( - [2, Comparator.GT, 1], // Not relevant - [{ field: 'foo', comparator: Comparator.EQ, value: 'bar' }] - ); - - const query = services.callCluster.mock.calls[0][1]!; - - expect(query.body).toMatchObject({ - track_total_hits: true, - query: { - bool: { - filter: [ - { - range: { - '@timestamp': { - format: 'epoch_millis', - }, - }, - }, - { - term: { - foo: { - value: 'bar', - }, - }, - }, - ], + }); + describe('Criteria filter building', () => { + test('Handles positive criteria', () => { + const alertParams: LogDocumentCountAlertParams = { + ...baseAlertParams, + criteria: positiveCriteria, + }; + const filters = buildFiltersFromCriteria(alertParams, TIMESTAMP_FIELD); + expect(filters.mustFilters).toEqual([ + { + range: { + numericField: { + gt: 10, + }, }, }, - size: 0, - }); - }); - - it('works with `Comparator.NOT_EQ`', async () => { - await callExecutor( - [2, Comparator.GT, 1], // Not relevant - [{ field: 'foo', comparator: Comparator.NOT_EQ, value: 'bar' }] - ); - - const query = services.callCluster.mock.calls[0][1]!; - - expect(query.body).toMatchObject({ - track_total_hits: true, - query: { - bool: { - filter: [ - { - range: { - '@timestamp': { - format: 'epoch_millis', - }, - }, - }, - ], - must_not: [ - { - term: { - foo: { - value: 'bar', - }, - }, - }, - ], + { + range: { + numericField: { + gte: 10, + }, }, }, - size: 0, - }); - }); - - it('works with `Comparator.MATCH`', async () => { - await callExecutor( - [2, Comparator.GT, 1], // Not relevant - [{ field: 'foo', comparator: Comparator.MATCH, value: 'bar' }] - ); - - const query = services.callCluster.mock.calls[0][1]!; - - expect(query.body).toMatchObject({ - track_total_hits: true, - query: { - bool: { - filter: [ - { - range: { - '@timestamp': { - format: 'epoch_millis', - }, - }, - }, - { - match: { - foo: 'bar', - }, - }, - ], + { + range: { + numericField: { + lt: 10, + }, }, }, - size: 0, - }); - }); - - it('works with `Comparator.NOT_MATCH`', async () => { - await callExecutor( - [2, Comparator.GT, 1], // Not relevant - [{ field: 'foo', comparator: Comparator.NOT_MATCH, value: 'bar' }] - ); - - const query = services.callCluster.mock.calls[0][1]!; - - expect(query.body).toMatchObject({ - track_total_hits: true, - query: { - bool: { - filter: [ - { - range: { - '@timestamp': { - format: 'epoch_millis', - }, - }, - }, - ], - must_not: [ - { - match: { - foo: 'bar', - }, - }, - ], + { + range: { + numericField: { + lte: 10, + }, }, }, - size: 0, - }); - }); - - it('works with `Comparator.MATCH_PHRASE`', async () => { - await callExecutor( - [2, Comparator.GT, 1], // Not relevant - [{ field: 'foo', comparator: Comparator.MATCH_PHRASE, value: 'bar' }] - ); - - const query = services.callCluster.mock.calls[0][1]!; - - expect(query.body).toMatchObject({ - track_total_hits: true, - query: { - bool: { - filter: [ - { - range: { - '@timestamp': { - format: 'epoch_millis', - }, - }, - }, - { - match_phrase: { - foo: 'bar', - }, - }, - ], + { + term: { + keywordField: { + value: 'error', + }, }, }, - size: 0, - }); - }); - - it('works with `Comparator.NOT_MATCH_PHRASE`', async () => { - await callExecutor( - [2, Comparator.GT, 1], // Not relevant - [{ field: 'foo', comparator: Comparator.NOT_MATCH_PHRASE, value: 'bar' }] - ); - - const query = services.callCluster.mock.calls[0][1]!; - - expect(query.body).toMatchObject({ - track_total_hits: true, - query: { - bool: { - filter: [ - { - range: { - '@timestamp': { - format: 'epoch_millis', - }, - }, - }, - ], - must_not: [ - { - match_phrase: { - foo: 'bar', - }, - }, - ], + { + match: { + textField: 'Something went wrong', }, }, - size: 0, - }); - }); - - it('works with `Comparator.GT`', async () => { - await callExecutor( - [2, Comparator.GT, 1], // Not relevant - [{ field: 'foo', comparator: Comparator.GT, value: 1 }] - ); - - const query = services.callCluster.mock.calls[0][1]!; - - expect(query.body).toMatchObject({ - track_total_hits: true, - query: { - bool: { - filter: [ - { - range: { - '@timestamp': { - format: 'epoch_millis', - }, - }, - }, - { - range: { - foo: { - gt: 1, - }, - }, - }, - ], + { + match_phrase: { + textField: 'Something went wrong', }, }, - size: 0, - }); + ]); }); - it('works with `Comparator.GT_OR_EQ`', async () => { - await callExecutor( - [2, Comparator.GT, 1], // Not relevant - [{ field: 'foo', comparator: Comparator.GT_OR_EQ, value: 1 }] - ); - - const query = services.callCluster.mock.calls[0][1]!; - - expect(query.body).toMatchObject({ - track_total_hits: true, - query: { - bool: { - filter: [ - { - range: { - '@timestamp': { - format: 'epoch_millis', - }, - }, - }, - { - range: { - foo: { - gte: 1, - }, - }, - }, - ], + test('Handles negative criteria', () => { + const alertParams: LogDocumentCountAlertParams = { + ...baseAlertParams, + criteria: negativeCriteria, + }; + const filters = buildFiltersFromCriteria(alertParams, TIMESTAMP_FIELD); + + expect(filters.mustNotFilters).toEqual([ + { + term: { + keywordField: { + value: 'error', + }, }, }, - size: 0, - }); + { + match: { + textField: 'Something went wrong', + }, + }, + { + match_phrase: { + textField: 'Something went wrong', + }, + }, + ]); }); - it('works with `Comparator.LT`', async () => { - await callExecutor( - [2, Comparator.GT, 1], // Not relevant - [{ field: 'foo', comparator: Comparator.LT, value: 1 }] - ); + test('Handles time range', () => { + const alertParams: LogDocumentCountAlertParams = { ...baseAlertParams, criteria: [] }; + const filters = buildFiltersFromCriteria(alertParams, TIMESTAMP_FIELD); + expect(typeof filters.rangeFilter.range[TIMESTAMP_FIELD].gte).toBe('number'); + expect(typeof filters.rangeFilter.range[TIMESTAMP_FIELD].lte).toBe('number'); + expect(filters.rangeFilter.range[TIMESTAMP_FIELD].format).toBe('epoch_millis'); - const query = services.callCluster.mock.calls[0][1]!; + expect(typeof filters.groupedRangeFilter.range[TIMESTAMP_FIELD].gte).toBe('number'); + expect(typeof filters.groupedRangeFilter.range[TIMESTAMP_FIELD].lte).toBe('number'); + expect(filters.groupedRangeFilter.range[TIMESTAMP_FIELD].format).toBe('epoch_millis'); + }); + }); - expect(query.body).toMatchObject({ - track_total_hits: true, - query: { - bool: { - filter: [ - { - range: { - '@timestamp': { - format: 'epoch_millis', + describe('ES queries', () => { + describe('Query generation', () => { + test('Correctly generates ungrouped queries', () => { + const alertParams: LogDocumentCountAlertParams = { + ...baseAlertParams, + criteria: [...positiveCriteria, ...negativeCriteria], + }; + const query = getUngroupedESQuery(alertParams, TIMESTAMP_FIELD, FILEBEAT_INDEX); + expect(query).toEqual({ + index: 'filebeat-*', + allowNoIndices: true, + ignoreUnavailable: true, + body: { + track_total_hits: true, + query: { + bool: { + filter: [ + { + range: { + '@timestamp': { + gte: expect.any(Number), + lte: expect.any(Number), + format: 'epoch_millis', + }, + }, }, - }, - }, - { - range: { - foo: { - lt: 1, + { + range: { + numericField: { + gt: 10, + }, + }, }, - }, + { + range: { + numericField: { + gte: 10, + }, + }, + }, + { + range: { + numericField: { + lt: 10, + }, + }, + }, + { + range: { + numericField: { + lte: 10, + }, + }, + }, + { + term: { + keywordField: { + value: 'error', + }, + }, + }, + { + match: { + textField: 'Something went wrong', + }, + }, + { + match_phrase: { + textField: 'Something went wrong', + }, + }, + ], + must_not: [ + { + term: { + keywordField: { + value: 'error', + }, + }, + }, + { + match: { + textField: 'Something went wrong', + }, + }, + { + match_phrase: { + textField: 'Something went wrong', + }, + }, + ], }, - ], + }, + size: 0, }, - }, - size: 0, + }); }); - }); - - it('works with `Comparator.LT_OR_EQ`', async () => { - await callExecutor( - [2, Comparator.GT, 1], // Not relevant - [{ field: 'foo', comparator: Comparator.LT_OR_EQ, value: 1 }] - ); - - const query = services.callCluster.mock.calls[0][1]!; - expect(query.body).toMatchObject({ - track_total_hits: true, - query: { - bool: { - filter: [ - { - range: { - '@timestamp': { - format: 'epoch_millis', + test('Correctly generates grouped queries', () => { + const alertParams: LogDocumentCountAlertParams = { + ...baseAlertParams, + groupBy: ['host.name'], + criteria: [...positiveCriteria, ...negativeCriteria], + }; + const query = getGroupedESQuery(alertParams, TIMESTAMP_FIELD, FILEBEAT_INDEX); + expect(query).toEqual({ + index: 'filebeat-*', + allowNoIndices: true, + ignoreUnavailable: true, + body: { + query: { + bool: { + filter: [ + { + range: { + '@timestamp': { + gte: expect.any(Number), + lte: expect.any(Number), + format: 'epoch_millis', + }, + }, }, - }, + ], + must_not: [ + { + term: { + keywordField: { + value: 'error', + }, + }, + }, + { + match: { + textField: 'Something went wrong', + }, + }, + { + match_phrase: { + textField: 'Something went wrong', + }, + }, + ], }, - { - range: { - foo: { - lte: 1, + }, + aggregations: { + groups: { + composite: { + size: 40, + sources: [ + { + 'group-0-host.name': { + terms: { + field: 'host.name', + }, + }, + }, + ], + }, + aggregations: { + filtered_results: { + filter: { + bool: { + filter: [ + { + range: { + '@timestamp': { + gte: expect.any(Number), + lte: expect.any(Number), + format: 'epoch_millis', + }, + }, + }, + { + range: { + numericField: { + gt: 10, + }, + }, + }, + { + range: { + numericField: { + gte: 10, + }, + }, + }, + { + range: { + numericField: { + lt: 10, + }, + }, + }, + { + range: { + numericField: { + lte: 10, + }, + }, + }, + { + term: { + keywordField: { + value: 'error', + }, + }, + }, + { + match: { + textField: 'Something went wrong', + }, + }, + { + match_phrase: { + textField: 'Something went wrong', + }, + }, + ], + }, + }, }, }, }, - ], + }, + size: 0, }, - }, - size: 0, + }); }); }); }); - describe('Multiple criteria create the right ES query', () => { - beforeEach(() => { - services.callCluster.mockReset(); + describe('Results processors', () => { + describe('Can process ungrouped results', () => { + test('It handles the OK state correctly', () => { + const alertInstanceUpdaterMock = jest.fn(); + const alertParams = { + ...baseAlertParams, + criteria: [positiveCriteria[0]], + }; + const results = { + hits: { + total: { + value: 2, + }, + }, + } as UngroupedSearchQueryResponse; + processUngroupedResults( + results, + alertParams, + alertsMock.createAlertInstanceFactory, + alertInstanceUpdaterMock + ); + // First call, second argument + expect(alertInstanceUpdaterMock.mock.calls[0][1]).toBe(AlertStates.OK); + // First call, third argument + expect(alertInstanceUpdaterMock.mock.calls[0][2]).toBe(undefined); + }); + + test('It handles the ALERT state correctly', () => { + const alertInstanceUpdaterMock = jest.fn(); + const alertParams = { + ...baseAlertParams, + criteria: [positiveCriteria[0]], + }; + const results = { + hits: { + total: { + value: 10, + }, + }, + } as UngroupedSearchQueryResponse; + processUngroupedResults( + results, + alertParams, + alertsMock.createAlertInstanceFactory, + alertInstanceUpdaterMock + ); + // First call, second argument + expect(alertInstanceUpdaterMock.mock.calls[0][1]).toBe(AlertStates.ALERT); + // First call, third argument + expect(alertInstanceUpdaterMock.mock.calls[0][2]).toEqual([ + { + actionGroup: 'logs.threshold.fired', + context: { + conditions: ' numericField more than 10', + group: null, + matchingDocuments: 10, + }, + }, + ]); + }); }); - it('works', async () => { - await callExecutor( - [2, Comparator.GT, 1], // Not relevant - [ - { field: 'foo', comparator: Comparator.EQ, value: 'bar' }, - { field: 'http.status', comparator: Comparator.LT, value: 400 }, - ] - ); - const query = services.callCluster.mock.calls[0][1]!; + describe('Can process grouped results', () => { + test('It handles the OK state correctly', () => { + const alertInstanceUpdaterMock = jest.fn(); + const alertParams = { + ...baseAlertParams, + criteria: [positiveCriteria[0]], + groupBy: ['host.name', 'event.dataset'], + }; + const results = [ + { + key: { + 'host.name': 'i-am-a-host-name', + 'event.dataset': 'i-am-a-dataset', + }, + doc_count: 100, + filtered_results: { + doc_count: 1, + }, + }, + { + key: { + 'host.name': 'i-am-a-host-name', + 'event.dataset': 'i-am-a-dataset', + }, + doc_count: 100, + filtered_results: { + doc_count: 2, + }, + }, + { + key: { + 'host.name': 'i-am-a-host-name', + 'event.dataset': 'i-am-a-dataset', + }, + doc_count: 100, + filtered_results: { + doc_count: 3, + }, + }, + ] as GroupedSearchQueryResponse['aggregations']['groups']['buckets']; + processGroupByResults( + results, + alertParams, + alertsMock.createAlertInstanceFactory, + alertInstanceUpdaterMock + ); + expect(alertInstanceUpdaterMock.mock.calls.length).toBe(3); + // First call, second argument + expect(alertInstanceUpdaterMock.mock.calls[0][1]).toBe(AlertStates.OK); + // First call, third argument + expect(alertInstanceUpdaterMock.mock.calls[0][2]).toBe(undefined); + + // Second call, second argument + expect(alertInstanceUpdaterMock.mock.calls[1][1]).toBe(AlertStates.OK); + // Second call, third argument + expect(alertInstanceUpdaterMock.mock.calls[1][2]).toBe(undefined); + + // Third call, second argument + expect(alertInstanceUpdaterMock.mock.calls[2][1]).toBe(AlertStates.OK); + // Third call, third argument + expect(alertInstanceUpdaterMock.mock.calls[2][2]).toBe(undefined); + }); - expect(query.body).toMatchObject({ - track_total_hits: true, - query: { - bool: { - filter: [ - { - range: { - '@timestamp': { - format: 'epoch_millis', - }, - }, - }, - { - term: { - foo: { - value: 'bar', - }, - }, - }, - { - range: { - 'http.status': { - lt: 400, - }, - }, - }, - ], + test('It handles the ALERT state correctly', () => { + const alertInstanceUpdaterMock = jest.fn(); + const alertParams = { + ...baseAlertParams, + criteria: [positiveCriteria[0]], + groupBy: ['host.name', 'event.dataset'], + }; + // Two groups should fire, one shouldn't + const results = [ + { + key: { + 'host.name': 'i-am-a-host-name-1', + 'event.dataset': 'i-am-a-dataset-1', + }, + doc_count: 100, + filtered_results: { + doc_count: 10, + }, }, - }, - size: 0, + { + key: { + 'host.name': 'i-am-a-host-name-2', + 'event.dataset': 'i-am-a-dataset-2', + }, + doc_count: 100, + filtered_results: { + doc_count: 2, + }, + }, + { + key: { + 'host.name': 'i-am-a-host-name-3', + 'event.dataset': 'i-am-a-dataset-3', + }, + doc_count: 100, + filtered_results: { + doc_count: 20, + }, + }, + ] as GroupedSearchQueryResponse['aggregations']['groups']['buckets']; + processGroupByResults( + results, + alertParams, + alertsMock.createAlertInstanceFactory, + alertInstanceUpdaterMock + ); + expect(alertInstanceUpdaterMock.mock.calls.length).toBe(results.length); + // First call, second argument + expect(alertInstanceUpdaterMock.mock.calls[0][1]).toBe(AlertStates.ALERT); + // First call, third argument + expect(alertInstanceUpdaterMock.mock.calls[0][2]).toEqual([ + { + actionGroup: 'logs.threshold.fired', + context: { + conditions: ' numericField more than 10', + group: 'i-am-a-host-name-1, i-am-a-dataset-1', + matchingDocuments: 10, + }, + }, + ]); + + // Second call, second argument + expect(alertInstanceUpdaterMock.mock.calls[1][1]).toBe(AlertStates.OK); + // Second call, third argument + expect(alertInstanceUpdaterMock.mock.calls[1][2]).toBe(undefined); + + // Third call, second argument + expect(alertInstanceUpdaterMock.mock.calls[2][1]).toBe(AlertStates.ALERT); + // Third call, third argument + expect(alertInstanceUpdaterMock.mock.calls[2][2]).toEqual([ + { + actionGroup: 'logs.threshold.fired', + context: { + conditions: ' numericField more than 10', + group: 'i-am-a-host-name-3, i-am-a-dataset-3', + matchingDocuments: 20, + }, + }, + ]); }); }); }); diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts index db76e955f0073..224b898141c36 100644 --- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts @@ -5,7 +5,12 @@ */ import { i18n } from '@kbn/i18n'; -import { AlertExecutorOptions, AlertServices } from '../../../../../alerts/server'; +import { + AlertExecutorOptions, + AlertServices, + AlertInstance, + AlertInstanceContext, +} from '../../../../../alerts/server'; import { AlertStates, Comparator, @@ -19,7 +24,6 @@ import { } from '../../../../common/alerting/logs/types'; import { InfraBackendLibs } from '../../infra_types'; import { getIntervalInSeconds } from '../../../utils/get_interval_in_seconds'; -import { InfraSource } from '../../../../common/http_api/source_api'; import { decodeOrThrow } from '../../../../common/runtime_types'; import { UNGROUPED_FACTORY_KEY } from '../common/utils'; @@ -42,6 +46,7 @@ export const createLogThresholdExecutor = (libs: InfraBackendLibs) => const sourceConfiguration = await sources.getSourceConfiguration(savedObjectsClient, 'default'); const indexPattern = sourceConfiguration.configuration.logAlias; + const timestampField = sourceConfiguration.configuration.fields.timestamp; const alertInstance = alertInstanceFactory(UNGROUPED_FACTORY_KEY); try { @@ -49,8 +54,8 @@ export const createLogThresholdExecutor = (libs: InfraBackendLibs) => const query = groupBy && groupBy.length > 0 - ? getGroupedESQuery(validatedParams, sourceConfiguration.configuration, indexPattern) - : getUngroupedESQuery(validatedParams, sourceConfiguration.configuration, indexPattern); + ? getGroupedESQuery(validatedParams, timestampField, indexPattern) + : getUngroupedESQuery(validatedParams, timestampField, indexPattern); if (!query) { throw new Error('ES query could not be built from the provided alert params'); @@ -60,13 +65,15 @@ export const createLogThresholdExecutor = (libs: InfraBackendLibs) => processGroupByResults( await getGroupedResults(query, callCluster), validatedParams, - alertInstanceFactory + alertInstanceFactory, + updateAlertInstance ); } else { processUngroupedResults( await getUngroupedResults(query, callCluster), validatedParams, - alertInstanceFactory + alertInstanceFactory, + updateAlertInstance ); } } catch (e) { @@ -78,10 +85,11 @@ export const createLogThresholdExecutor = (libs: InfraBackendLibs) => } }; -const processUngroupedResults = ( +export const processUngroupedResults = ( results: UngroupedSearchQueryResponse, params: LogDocumentCountAlertParams, - alertInstanceFactory: AlertExecutorOptions['services']['alertInstanceFactory'] + alertInstanceFactory: AlertExecutorOptions['services']['alertInstanceFactory'], + alertInstaceUpdater: AlertInstanceUpdater ) => { const { count, criteria } = params; @@ -89,19 +97,18 @@ const processUngroupedResults = ( const documentCount = results.hits.total.value; if (checkValueAgainstComparatorMap[count.comparator](documentCount, count.value)) { - alertInstance.scheduleActions(FIRED_ACTIONS.id, { - matchingDocuments: documentCount, - conditions: createConditionsMessage(criteria), - group: null, - }); - - alertInstance.replaceState({ - alertState: AlertStates.ALERT, - }); + alertInstaceUpdater(alertInstance, AlertStates.ALERT, [ + { + actionGroup: FIRED_ACTIONS.id, + context: { + matchingDocuments: documentCount, + conditions: createConditionsMessage(criteria), + group: null, + }, + }, + ]); } else { - alertInstance.replaceState({ - alertState: AlertStates.OK, - }); + alertInstaceUpdater(alertInstance, AlertStates.OK); } }; @@ -110,10 +117,11 @@ interface ReducedGroupByResults { documentCount: number; } -const processGroupByResults = ( +export const processGroupByResults = ( results: GroupedSearchQueryResponse['aggregations']['groups']['buckets'], params: LogDocumentCountAlertParams, - alertInstanceFactory: AlertExecutorOptions['services']['alertInstanceFactory'] + alertInstanceFactory: AlertExecutorOptions['services']['alertInstanceFactory'], + alertInstaceUpdater: AlertInstanceUpdater ) => { const { count, criteria } = params; @@ -128,23 +136,41 @@ const processGroupByResults = ( const documentCount = group.documentCount; if (checkValueAgainstComparatorMap[count.comparator](documentCount, count.value)) { - alertInstance.scheduleActions(FIRED_ACTIONS.id, { - matchingDocuments: documentCount, - conditions: createConditionsMessage(criteria), - group: group.name, - }); - - alertInstance.replaceState({ - alertState: AlertStates.ALERT, - }); + alertInstaceUpdater(alertInstance, AlertStates.ALERT, [ + { + actionGroup: FIRED_ACTIONS.id, + context: { + matchingDocuments: documentCount, + conditions: createConditionsMessage(criteria), + group: group.name, + }, + }, + ]); } else { - alertInstance.replaceState({ - alertState: AlertStates.OK, - }); + alertInstaceUpdater(alertInstance, AlertStates.OK); } }); }; +type AlertInstanceUpdater = ( + alertInstance: AlertInstance, + state: AlertStates, + actions?: Array<{ actionGroup: string; context: AlertInstanceContext }> +) => void; + +export const updateAlertInstance: AlertInstanceUpdater = (alertInstance, state, actions) => { + if (actions && actions.length > 0) { + actions.forEach((actionSet) => { + const { actionGroup, context } = actionSet; + alertInstance.scheduleActions(actionGroup, context); + }); + } + + alertInstance.replaceState({ + alertState: state, + }); +}; + export const buildFiltersFromCriteria = ( params: Omit, timestampField: string @@ -198,7 +224,7 @@ export const buildFiltersFromCriteria = ( export const getGroupedESQuery = ( params: Omit, - sourceConfiguration: InfraSource['configuration'], + timestampField: string, index: string ): object | undefined => { const { groupBy } = params; @@ -207,8 +233,6 @@ export const getGroupedESQuery = ( return; } - const timestampField = sourceConfiguration.fields.timestamp; - const { rangeFilter, groupedRangeFilter, mustFilters, mustNotFilters } = buildFiltersFromCriteria( params, timestampField @@ -258,12 +282,12 @@ export const getGroupedESQuery = ( export const getUngroupedESQuery = ( params: Omit, - sourceConfiguration: InfraSource['configuration'], + timestampField: string, index: string ): object => { const { rangeFilter, mustFilters, mustNotFilters } = buildFiltersFromCriteria( params, - sourceConfiguration.fields.timestamp + timestampField ); const body = { @@ -357,7 +381,7 @@ const buildCriterionQuery = (criterion: Criterion): Filter | undefined => { } }; -const getPositiveComparators = () => { +export const getPositiveComparators = () => { return [ Comparator.GT, Comparator.GT_OR_EQ, @@ -369,11 +393,11 @@ const getPositiveComparators = () => { ]; }; -const getNegativeComparators = () => { +export const getNegativeComparators = () => { return [Comparator.NOT_EQ, Comparator.NOT_MATCH, Comparator.NOT_MATCH_PHRASE]; }; -const queryMappings: { +export const queryMappings: { [key: string]: string; } = { [Comparator.GT]: 'range', diff --git a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx index 0fefa71dea48b..235e5d0f20f87 100644 --- a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx +++ b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx @@ -19,7 +19,6 @@ import { ExplorerSwimlane, ExplorerSwimlaneProps } from './explorer_swimlane'; import { MlTooltipComponent } from '../components/chart_tooltip'; import { SwimLanePagination } from './swimlane_pagination'; -import { SWIMLANE_TYPE } from './explorer_constants'; import { ViewBySwimLaneData } from './explorer_utils'; /** @@ -91,7 +90,6 @@ export const SwimlaneContainer: FC< (showSwimlane || isLoading) && swimlaneLimit !== undefined && onPaginationChange && - props.swimlaneType === SWIMLANE_TYPE.VIEW_BY && fromPage && perPage; diff --git a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx index f89e27925d745..30b9bc2af219f 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx @@ -218,10 +218,8 @@ const ExplorerUrlStateManager: FC = ({ jobsWithTim loadExplorerData({ ...loadExplorerDataConfig, swimlaneLimit: - explorerState?.viewBySwimlaneData && - isViewBySwimLaneData(explorerState?.viewBySwimlaneData) - ? explorerState?.viewBySwimlaneData.cardinality - : undefined, + isViewBySwimLaneData(explorerState?.viewBySwimlaneData) && + explorerState?.viewBySwimlaneData.cardinality, }); } }, [JSON.stringify(loadExplorerDataConfig)]); diff --git a/x-pack/plugins/ml/public/application/services/results_service/results_service.js b/x-pack/plugins/ml/public/application/services/results_service/results_service.js index 0c3b2e40c8e26..ef00c9025763e 100644 --- a/x-pack/plugins/ml/public/application/services/results_service/results_service.js +++ b/x-pack/plugins/ml/public/application/services/results_service/results_service.js @@ -88,6 +88,11 @@ export function resultsServiceProvider(mlApiServices) { }, }, aggs: { + jobsCardinality: { + cardinality: { + field: 'job_id', + }, + }, jobId: { terms: { field: 'job_id', @@ -148,6 +153,7 @@ export function resultsServiceProvider(mlApiServices) { }); obj.results[jobId] = resultsForTime; }); + obj.cardinality = resp.aggregations?.jobsCardinality?.value ?? 0; resolve(obj); }) diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/index.ts index f5d46078fcea4..297e17fd127b3 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/index.ts @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './authentications'; export * from './all'; +export * from './authentications'; export * from './common'; -export * from './overview'; export * from './first_last_seen'; +export * from './overview'; export * from './uncommon_processes'; export enum HostsQueries { diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts index 7721f2ae97d75..4d6bd87bb9d42 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts @@ -21,6 +21,8 @@ import { } from './hosts'; import { NetworkQueries, + NetworkDnsStrategyResponse, + NetworkDnsRequestOptions, NetworkTlsStrategyResponse, NetworkTlsRequestOptions, NetworkHttpStrategyResponse, @@ -30,6 +32,11 @@ import { NetworkTopNFlowStrategyResponse, NetworkTopNFlowRequestOptions, } from './network'; +import { + MatrixHistogramQuery, + MatrixHistogramRequestOptions, + MatrixHistogramStrategyResponse, +} from './matrix_histogram'; import { DocValueFields, TimerangeInput, @@ -39,9 +46,10 @@ import { } from '../common'; export * from './hosts'; +export * from './matrix_histogram'; export * from './network'; -export type FactoryQueryTypes = HostsQueries | NetworkQueries; +export type FactoryQueryTypes = HostsQueries | NetworkQueries | typeof MatrixHistogramQuery; export interface RequestBasicOptions extends IEsSearchRequest { timerange: TimerangeInput; @@ -73,14 +81,18 @@ export type StrategyResponseType = T extends HostsQ ? HostFirstLastSeenStrategyResponse : T extends HostsQueries.uncommonProcesses ? HostUncommonProcessesStrategyResponse - : T extends NetworkQueries.tls - ? NetworkTlsStrategyResponse + : T extends NetworkQueries.dns + ? NetworkDnsStrategyResponse : T extends NetworkQueries.http ? NetworkHttpStrategyResponse + : T extends NetworkQueries.tls + ? NetworkTlsStrategyResponse : T extends NetworkQueries.topCountries ? NetworkTopCountriesStrategyResponse : T extends NetworkQueries.topNFlow ? NetworkTopNFlowStrategyResponse + : T extends typeof MatrixHistogramQuery + ? MatrixHistogramStrategyResponse : never; export type StrategyRequestType = T extends HostsQueries.hosts @@ -93,12 +105,16 @@ export type StrategyRequestType = T extends HostsQu ? HostFirstLastSeenRequestOptions : T extends HostsQueries.uncommonProcesses ? HostUncommonProcessesRequestOptions - : T extends NetworkQueries.tls - ? NetworkTlsRequestOptions + : T extends NetworkQueries.dns + ? NetworkDnsRequestOptions : T extends NetworkQueries.http ? NetworkHttpRequestOptions + : T extends NetworkQueries.tls + ? NetworkTlsRequestOptions : T extends NetworkQueries.topCountries ? NetworkTopCountriesRequestOptions : T extends NetworkQueries.topNFlow ? NetworkTopNFlowRequestOptions + : T extends typeof MatrixHistogramQuery + ? MatrixHistogramRequestOptions : never; diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/alerts/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/alerts/index.ts new file mode 100644 index 0000000000000..28953d7df8550 --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/alerts/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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { HistogramBucket } from '../common'; + +export interface AlertsGroupData { + key: string; + doc_count: number; + alerts: { + buckets: HistogramBucket[]; + }; +} diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/anomalies/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/anomalies/index.ts new file mode 100644 index 0000000000000..dbd7fe6d1c427 --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/anomalies/index.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SearchHit } from '../../../common'; + +interface AnomaliesOverTimeHistogramData { + key_as_string: string; + key: number; + doc_count: number; +} + +export interface AnomaliesActionGroupData { + key: number; + anomalies: { + bucket: AnomaliesOverTimeHistogramData[]; + }; + doc_count: number; +} + +export interface AnomalySource { + [field: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any +} + +export interface AnomalyHit extends SearchHit { + sort: string[]; + _source: AnomalySource; + aggregations: { + [agg: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any + }; +} diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/authentications/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/authentications/index.ts new file mode 100644 index 0000000000000..23d656be5044e --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/authentications/index.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; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface AuthenticationsOverTimeHistogramData { + key_as_string: string; + key: number; + doc_count: number; +} + +export interface AuthenticationsActionGroupData { + key: number; + events: { + bucket: AuthenticationsOverTimeHistogramData[]; + }; + doc_count: number; +} diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/common/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/common/index.ts new file mode 100644 index 0000000000000..687d55414f78e --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/common/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface HistogramBucket { + key: number; + doc_count: number; +} diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/dns/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/dns/index.ts new file mode 100644 index 0000000000000..7667dce383e54 --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/dns/index.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; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface DnsHistogramSubBucket { + key: string; + doc_count: number; + orderAgg: { + value: number; + }; +} +interface DnsHistogramBucket { + doc_count_error_upper_bound: number; + sum_other_doc_count: number; + buckets: DnsHistogramSubBucket[]; +} + +export interface DnsHistogramGroupData { + key: number; + doc_count: number; + key_as_string: string; + histogram: DnsHistogramBucket; +} diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/events/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/events/index.ts new file mode 100644 index 0000000000000..f1307335215ed --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/events/index.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SearchHit } from '../../../common'; + +interface EventsMatrixHistogramData { + key_as_string: string; + key: number; + doc_count: number; +} + +export interface EventSource { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [field: string]: any; +} + +export interface EventsActionGroupData { + key: number; + events: { + bucket: EventsMatrixHistogramData[]; + }; + doc_count: number; +} + +export interface EventHit extends SearchHit { + sort: string[]; + _source: EventSource; + aggregations: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [agg: string]: any; + }; +} diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/index.ts new file mode 100644 index 0000000000000..238300801cfc6 --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/index.ts @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IEsSearchResponse } from '../../../../../../../src/plugins/data/common'; +import { AuthenticationHit } from '../hosts'; +import { Inspect, Maybe, TimerangeInput } from '../../common'; +import { RequestBasicOptions } from '../'; +import { AlertsGroupData } from './alerts'; +import { AnomaliesActionGroupData, AnomalyHit } from './anomalies'; +import { DnsHistogramGroupData } from './dns'; +import { AuthenticationsActionGroupData } from './authentications'; +import { EventsActionGroupData, EventHit } from './events'; + +export * from './alerts'; +export * from './anomalies'; +export * from './authentications'; +export * from './common'; +export * from './dns'; +export * from './events'; + +export const MatrixHistogramQuery = 'matrixHistogram'; + +export enum MatrixHistogramType { + authentications = 'authentications', + anomalies = 'anomalies', + events = 'events', + alerts = 'alerts', + dns = 'dns', +} + +export interface MatrixHistogramRequestOptions extends RequestBasicOptions { + timerange: TimerangeInput; + histogramType: MatrixHistogramType; + stackByField: string; + inspect?: Maybe; +} + +export interface MatrixHistogramStrategyResponse extends IEsSearchResponse { + inspect?: Maybe; + matrixHistogramData: MatrixHistogramData[]; + totalCount: number; +} + +export interface MatrixHistogramData { + x?: Maybe; + y?: Maybe; + g?: Maybe; +} + +export interface MatrixHistogramBucket { + key: number; + doc_count: number; +} + +export interface MatrixHistogramSchema { + buildDsl: (options: MatrixHistogramRequestOptions) => {}; + aggName: string; + parseKey: string; + parser?: (data: MatrixHistogramParseData, keyBucket: string) => MatrixHistogramData[]; +} + +export type MatrixHistogramParseData = T extends MatrixHistogramType.alerts + ? AlertsGroupData[] + : T extends MatrixHistogramType.anomalies + ? AnomaliesActionGroupData[] + : T extends MatrixHistogramType.dns + ? DnsHistogramGroupData[] + : T extends MatrixHistogramType.authentications + ? AuthenticationsActionGroupData[] + : T extends MatrixHistogramType.events + ? EventsActionGroupData[] + : never; + +export type MatrixHistogramHit = T extends MatrixHistogramType.alerts + ? EventHit + : T extends MatrixHistogramType.anomalies + ? AnomalyHit + : T extends MatrixHistogramType.dns + ? EventHit + : T extends MatrixHistogramType.authentications + ? AuthenticationHit + : T extends MatrixHistogramType.events + ? EventHit + : never; + +export type MatrixHistogramDataConfig = Record< + MatrixHistogramType, + MatrixHistogramSchema +>; diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/dns/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/dns/index.ts new file mode 100644 index 0000000000000..e3899a914ee3a --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/dns/index.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common'; +import { CursorType, Inspect, Maybe, PageInfoPaginated, SortField } from '../../../common'; +import { RequestOptionsPaginated } from '../..'; + +export enum NetworkDnsFields { + dnsName = 'dnsName', + queryCount = 'queryCount', + uniqueDomains = 'uniqueDomains', + dnsBytesIn = 'dnsBytesIn', + dnsBytesOut = 'dnsBytesOut', +} + +export interface NetworkDnsRequestOptions extends RequestOptionsPaginated { + isPtrIncluded: boolean; + sort: SortField; + stackByField?: Maybe; +} + +export interface NetworkDnsStrategyResponse extends IEsSearchResponse { + edges: NetworkDnsEdges[]; + totalCount: number; + pageInfo: PageInfoPaginated; + inspect?: Maybe; + histogram?: Maybe; +} + +export interface NetworkDnsEdges { + node: NetworkDnsItem; + cursor: CursorType; +} + +export interface NetworkDnsItem { + _id?: Maybe; + dnsBytesIn?: Maybe; + dnsBytesOut?: Maybe; + dnsName?: Maybe; + queryCount?: Maybe; + uniqueDomains?: Maybe; +} + +export interface MatrixOverOrdinalHistogramData { + x: string; + y: number; + g: string; +} + +export interface NetworkDnsBuckets { + key: string; + doc_count: number; + unique_domains: { + value: number; + }; + dns_bytes_in: { + value: number; + }; + dns_bytes_out: { + value: number; + }; +} diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/index.ts index 2992ee32f8ac7..a0ef43eb3d899 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/index.ts @@ -5,12 +5,14 @@ */ export * from './common'; +export * from './dns'; export * from './http'; export * from './tls'; export * from './top_countries'; export * from './top_n_flow'; export enum NetworkQueries { + dns = 'dns', http = 'http', tls = 'tls', topCountries = 'topCountries', diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/top_countries/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/top_countries/index.ts index f499db82d6479..a28388a2c6f8f 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/top_countries/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/top_countries/index.ts @@ -14,14 +14,6 @@ import { TopNetworkTablesEcsField, } from '../common'; -export enum NetworkDnsFields { - dnsName = 'dnsName', - queryCount = 'queryCount', - uniqueDomains = 'uniqueDomains', - dnsBytesIn = 'dnsBytesIn', - dnsBytesOut = 'dnsBytesOut', -} - export enum FlowTarget { client = 'client', destination = 'destination', diff --git a/x-pack/plugins/security_solution/package.json b/x-pack/plugins/security_solution/package.json index 687099541b3d2..70dbaa0d31681 100644 --- a/x-pack/plugins/security_solution/package.json +++ b/x-pack/plugins/security_solution/package.json @@ -8,9 +8,10 @@ "extract-mitre-attacks": "node scripts/extract_tactics_techniques_mitre.js && node ../../../scripts/eslint ./public/pages/detection_engine/mitre/mitre_tactics_techniques.ts --fix", "build-graphql-types": "node scripts/generate_types_from_graphql.js", "cypress:open": "cypress open --config-file ./cypress/cypress.json", + "cypress:open-as-ci": "node ../../../scripts/functional_tests --config ../../test/security_solution_cypress/visual_config.ts", "cypress:run": "cypress run --browser chrome --headless --spec ./cypress/integration/**/*.spec.ts --config-file ./cypress/cypress.json --reporter ../../node_modules/cypress-multi-reporters --reporter-options configFile=./cypress/reporter_config.json; status=$?; ../../node_modules/.bin/mochawesome-merge --reportDir ../../../target/kibana-security-solution/cypress/results > ../../../target/kibana-security-solution/cypress/results/output.json; ../../../node_modules/.bin/marge ../../../target/kibana-security-solution/cypress/results/output.json --reportDir ../../../target/kibana-security-solution/cypress/results; mkdir -p ../../../target/junit && cp ../../../target/kibana-security-solution/cypress/results/*.xml ../../../target/junit/ && exit $status;", - "cypress:run-as-ci": "node ../../../scripts/functional_tests --config ../../test/security_solution_cypress/config.ts", - "test:generate": "ts-node --project scripts/endpoint/cli_tsconfig.json scripts/endpoint/resolver_generator.ts" + "cypress:run-as-ci": "node ../../../scripts/functional_tests --config ../../test/security_solution_cypress/cli_config.ts", + "test:generate": "node scripts/endpoint/resolver_generator" }, "devDependencies": { "@types/md5": "^2.2.0", 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 index c7376b67c5188..ce79d839f2162 100644 --- 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 @@ -6,7 +6,7 @@ import * as i18n from './translations'; import { MatrixHistogramOption, MatrixHistogramConfigs } from '../matrix_histogram/types'; -import { HistogramType } from '../../../graphql/types'; +import { MatrixHistogramType } from '../../../../common/search_strategy/security_solution/matrix_histogram'; export const alertsStackByOptions: MatrixHistogramOption[] = [ { @@ -25,7 +25,7 @@ export const histogramConfigs: MatrixHistogramConfigs = { defaultStackByOption: alertsStackByOptions.find((o) => o.text === DEFAULT_STACK_BY) ?? alertsStackByOptions[1], errorMessage: i18n.ERROR_FETCHING_ALERTS_DATA, - histogramType: HistogramType.alerts, + histogramType: MatrixHistogramType.alerts, stackByOptions: alertsStackByOptions, subtitle: undefined, title: i18n.ALERTS_GRAPH_TITLE, 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 index de9a8b32f1f90..d522e372d7734 100644 --- 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 @@ -13,12 +13,13 @@ import { AlertsComponentsProps } from './types'; import { AlertsTable } from './alerts_table'; import * as i18n from './translations'; import { useUiSetting$ } from '../../lib/kibana'; -import { MatrixHistogramContainer } from '../matrix_histogram'; +import { MatrixHistogram } from '../matrix_histogram'; import { histogramConfigs } from './histogram_configs'; import { MatrixHistogramConfigs } from '../matrix_histogram/types'; -const ID = 'alertsOverTimeQuery'; -export const AlertsView = ({ +const ID = 'alertsHistogramQuery'; + +const AlertsViewComponent: React.FC = ({ timelineId, deleteQuery, endDate, @@ -26,18 +27,18 @@ export const AlertsView = ({ pageFilters, setQuery, startDate, - type, -}: AlertsComponentsProps) => { +}) => { const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); + const { globalFullScreen } = useFullScreen(); + const getSubtitle = useCallback( (totalCount: number) => `${i18n.SHOWING}: ${numeral(totalCount).format(defaultNumberFormat)} ${i18n.UNIT( totalCount )}`, - // eslint-disable-next-line react-hooks/exhaustive-deps - [] + [defaultNumberFormat] ); - const { globalFullScreen } = useFullScreen(); + const alertsHistogramConfigs: MatrixHistogramConfigs = useMemo( () => ({ ...histogramConfigs, @@ -45,6 +46,7 @@ export const AlertsView = ({ }), [getSubtitle] ); + useEffect(() => { return () => { if (deleteQuery) { @@ -56,14 +58,12 @@ export const AlertsView = ({ return ( <> {!globalFullScreen && ( - )} @@ -76,4 +76,7 @@ export const AlertsView = ({ ); }; -AlertsView.displayName = 'AlertsView'; + +AlertsViewComponent.displayName = 'AlertsViewComponent'; + +export const AlertsView = React.memo(AlertsViewComponent); 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 index 78a6332c90fbc..b2637eeb2c65e 100644 --- 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 @@ -15,7 +15,7 @@ type CommonQueryProps = HostsComponentsQueryProps | NetworkComponentQueryProps; export interface AlertsComponentsProps extends Pick< CommonQueryProps, - 'deleteQuery' | 'endDate' | 'filterQuery' | 'skip' | 'setQuery' | 'startDate' | 'type' + 'deleteQuery' | 'endDate' | 'filterQuery' | 'skip' | 'setQuery' | 'startDate' > { timelineId: TimelineIdLiteral; pageFilters: Filter[]; 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 a80ea48b93f3d..7286c6b743692 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 @@ -10,42 +10,32 @@ import { mount, ReactWrapper } from 'enzyme'; import React from 'react'; import { MatrixHistogram } from '.'; -import { useQuery } from '../../containers/matrix_histogram'; -import { HistogramType } from '../../../graphql/types'; +import { useMatrixHistogram } from '../../containers/matrix_histogram'; +import { MatrixHistogramType } from '../../../../common/search_strategy/security_solution'; +import { TestProviders } from '../../mock'; + jest.mock('../../lib/kibana'); -jest.mock('./matrix_loader', () => { - return { - MatrixLoader: () => { - return
; - }, - }; -}); +jest.mock('./matrix_loader', () => ({ + MatrixLoader: () =>
, +})); -jest.mock('../header_section', () => { - return { - HeaderSection: () =>
, - }; -}); +jest.mock('../header_section', () => ({ + HeaderSection: () =>
, +})); -jest.mock('../charts/barchart', () => { - return { - BarChart: () =>
, - }; -}); +jest.mock('../charts/barchart', () => ({ + BarChart: () =>
, +})); -jest.mock('../../containers/matrix_histogram', () => { - return { - useQuery: jest.fn(), - }; -}); +jest.mock('../../containers/matrix_histogram', () => ({ + useMatrixHistogram: jest.fn(), +})); -jest.mock('../../components/matrix_histogram/utils', () => { - return { - getBarchartConfigs: jest.fn(), - getCustomChartData: jest.fn().mockReturnValue(true), - }; -}); +jest.mock('../../components/matrix_histogram/utils', () => ({ + getBarchartConfigs: jest.fn(), + getCustomChartData: jest.fn().mockReturnValue(true), +})); describe('Matrix Histogram Component', () => { let wrapper: ReactWrapper; @@ -55,7 +45,7 @@ describe('Matrix Histogram Component', () => { defaultStackByOption: { text: 'text', value: 'value' }, endDate: '2019-07-18T20:00:00.000Z', errorMessage: 'error', - histogramType: HistogramType.alerts, + histogramType: MatrixHistogramType.alerts, id: 'mockId', isInspected: false, isPtrIncluded: false, @@ -68,17 +58,20 @@ describe('Matrix Histogram Component', () => { subtitle: 'mockSubtitle', totalCount: -1, title: 'mockTitle', - dispatchSetAbsoluteRangeDatePicker: jest.fn(), }; beforeAll(() => { - (useQuery as jest.Mock).mockReturnValue({ - data: null, - loading: false, - inspect: false, - totalCount: null, + (useMatrixHistogram as jest.Mock).mockReturnValue([ + false, + { + data: null, + inspect: false, + totalCount: null, + }, + ]); + wrapper = mount(, { + wrappingComponent: TestProviders, }); - wrapper = mount(); }); describe('on initial load', () => { test('it renders MatrixLoader', () => { @@ -92,26 +85,33 @@ describe('Matrix Histogram Component', () => { }); test('it does NOT render a spacer when showSpacer is false', () => { - wrapper = mount(); + wrapper = mount( + , + { + wrappingComponent: TestProviders, + } + ); expect(wrapper.find('[data-test-subj="spacer"]').exists()).toBe(false); }); }); describe('not initial load', () => { beforeAll(() => { - (useQuery as jest.Mock).mockReturnValue({ - 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' }, - ], - loading: false, - inspect: false, - totalCount: 1, - }); + (useMatrixHistogram 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: 1, + }, + ]); wrapper.setProps({ endDate: 100 }); wrapper.update(); }); 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 e93ade7191f52..485ca4c93133a 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 @@ -9,54 +9,46 @@ import { Position } from '@elastic/charts'; import styled from 'styled-components'; import { EuiFlexGroup, EuiFlexItem, EuiProgress, EuiSelect, EuiSpacer } from '@elastic/eui'; -import { noop } from 'lodash/fp'; -import { compose } from 'redux'; -import { connect } from 'react-redux'; +import { useDispatch } from 'react-redux'; import * as i18n from './translations'; import { BarChart } from '../charts/barchart'; import { HeaderSection } from '../header_section'; import { MatrixLoader } from './matrix_loader'; import { Panel } from '../panel'; import { getBarchartConfigs, getCustomChartData } from './utils'; -import { useQuery } from '../../containers/matrix_histogram'; +import { useMatrixHistogram } from '../../containers/matrix_histogram'; import { MatrixHistogramProps, MatrixHistogramOption, MatrixHistogramQueryProps } from './types'; import { InspectButtonContainer } from '../inspect'; - -import { State, inputsSelectors } from '../../store'; -import { hostsModel } from '../../../hosts/store'; -import { networkModel } from '../../../network/store'; - +import { MatrixHistogramType } from '../../../../common/search_strategy/security_solution'; import { MatrixHistogramMappingTypes, GetTitle, GetSubTitle, } from '../../components/matrix_histogram/types'; import { GlobalTimeArgs } from '../../containers/use_global_time'; -import { QueryTemplateProps } from '../../containers/query_template'; import { setAbsoluteRangeDatePicker } from '../../store/inputs/actions'; import { InputsModelId } from '../../store/inputs/constants'; -import { HistogramType } from '../../../graphql/types'; -export interface OwnProps extends QueryTemplateProps { - defaultStackByOption: MatrixHistogramOption; - errorMessage: string; - headerChildren?: React.ReactNode; - hideHistogramIfEmpty?: boolean; - histogramType: HistogramType; - id: string; - indexToAdd?: string[] | null; - legendPosition?: Position; - mapping?: MatrixHistogramMappingTypes; - showSpacer?: boolean; - setQuery: GlobalTimeArgs['setQuery']; - setAbsoluteRangeDatePickerTarget?: InputsModelId; - showLegend?: boolean; - stackByOptions: MatrixHistogramOption[]; - subtitle?: string | GetSubTitle; - timelineId?: string; - title: string | GetTitle; - type: hostsModel.HostsType | networkModel.NetworkType; -} +export type MatrixHistogramComponentProps = MatrixHistogramProps & + Omit & { + defaultStackByOption: MatrixHistogramOption; + errorMessage: string; + headerChildren?: React.ReactNode; + hideHistogramIfEmpty?: boolean; + histogramType: MatrixHistogramType; + id: string; + indexToAdd?: string[] | null; + legendPosition?: Position; + mapping?: MatrixHistogramMappingTypes; + showSpacer?: boolean; + setQuery: GlobalTimeArgs['setQuery']; + setAbsoluteRangeDatePickerTarget?: InputsModelId; + showLegend?: boolean; + stackByOptions: MatrixHistogramOption[]; + subtitle?: string | GetSubTitle; + timelineId?: string; + title: string | GetTitle; + }; const DEFAULT_PANEL_HEIGHT = 300; @@ -70,9 +62,7 @@ const HistogramPanel = styled(Panel)<{ height?: number }>` ${({ height }) => (height != null ? `height: ${height}px;` : '')} `; -export const MatrixHistogramComponent: React.FC< - MatrixHistogramProps & MatrixHistogramQueryProps -> = ({ +export const MatrixHistogramComponent: React.FC = ({ chartHeight, defaultStackByOption, endDate, @@ -83,7 +73,6 @@ export const MatrixHistogramComponent: React.FC< hideHistogramIfEmpty = false, id, indexToAdd, - isInspected, legendPosition, mapping, panelHeight = DEFAULT_PANEL_HEIGHT, @@ -97,9 +86,25 @@ export const MatrixHistogramComponent: React.FC< timelineId, title, titleSize, - dispatchSetAbsoluteRangeDatePicker, yTickFormatter, }) => { + const dispatch = useDispatch(); + const handleBrushEnd = useCallback( + ({ x }) => { + if (!x) { + return; + } + const [min, max] = x; + dispatch( + setAbsoluteRangeDatePicker({ + id: setAbsoluteRangeDatePickerTarget, + from: new Date(min).toISOString(), + to: new Date(max).toISOString(), + }) + ); + }, + [dispatch, setAbsoluteRangeDatePickerTarget] + ); const barchartConfigs = useMemo( () => getBarchartConfigs({ @@ -107,30 +112,11 @@ export const MatrixHistogramComponent: React.FC< from: startDate, legendPosition, to: endDate, - onBrushEnd: ({ x }) => { - if (!x) { - return; - } - const [min, max] = x; - dispatchSetAbsoluteRangeDatePicker({ - id: setAbsoluteRangeDatePickerTarget, - from: new Date(min).toISOString(), - to: new Date(max).toISOString(), - }); - }, + onBrushEnd: handleBrushEnd, yTickFormatter, showLegend, }), - // eslint-disable-next-line react-hooks/exhaustive-deps - [ - chartHeight, - startDate, - legendPosition, - endDate, - dispatchSetAbsoluteRangeDatePicker, - yTickFormatter, - showLegend, - ] + [chartHeight, startDate, legendPosition, endDate, handleBrushEnd, yTickFormatter, showLegend] ); const [isInitialLoading, setIsInitialLoading] = useState(true); const [selectedStackByOption, setSelectedStackByOption] = useState( @@ -142,18 +128,16 @@ export const MatrixHistogramComponent: React.FC< stackByOptions.find((co) => co.value === event.target.value) ?? defaultStackByOption ); }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [] + [defaultStackByOption, stackByOptions] ); - const { data, loading, inspect, totalCount, refetch = noop } = useQuery({ + const [loading, { data, inspect, totalCount, refetch }] = useMatrixHistogram({ endDate, errorMessage, filterQuery, histogramType, indexToAdd, startDate, - isInspected, stackByField: selectedStackByOption.value, }); @@ -254,20 +238,3 @@ export const MatrixHistogramComponent: React.FC< }; export const MatrixHistogram = React.memo(MatrixHistogramComponent); - -const makeMapStateToProps = () => { - const getQuery = inputsSelectors.globalQueryByIdSelector(); - const mapStateToProps = (state: State, { id }: OwnProps) => { - const { isInspected } = getQuery(state, id); - return { - isInspected, - }; - }; - return mapStateToProps; -}; - -export const MatrixHistogramContainer = compose>( - connect(makeMapStateToProps, { - dispatchSetAbsoluteRangeDatePicker: setAbsoluteRangeDatePicker, - }) -)(MatrixHistogram); diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts index d471b5ae9bed1..fc1df4d8ca85f 100644 --- a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts @@ -9,7 +9,7 @@ import { ScaleType, Position, TickFormatter } from '@elastic/charts'; import { ActionCreator } from 'redux'; import { ESQuery } from '../../../../common/typed_json'; import { InputsModelId } from '../../store/inputs/constants'; -import { HistogramType } from '../../../graphql/types'; +import { MatrixHistogramType } from '../../../../common/search_strategy/security_solution'; import { UpdateDateRange } from '../charts/common'; import { GlobalTimeArgs } from '../../containers/use_global_time'; @@ -29,7 +29,7 @@ export interface MatrixHistogramConfigs { defaultStackByOption: MatrixHistogramOption; errorMessage: string; hideHistogramIfEmpty?: boolean; - histogramType: HistogramType; + histogramType: MatrixHistogramType; legendPosition?: Position; mapping?: MatrixHistogramMappingTypes; stackByOptions: MatrixHistogramOption[]; @@ -40,13 +40,7 @@ export interface MatrixHistogramConfigs { interface MatrixHistogramBasicProps { chartHeight?: number; - defaultIndex: string[]; defaultStackByOption: MatrixHistogramOption; - dispatchSetAbsoluteRangeDatePicker: ActionCreator<{ - id: InputsModelId; - from: string; - to: string; - }>; endDate: GlobalTimeArgs['to']; headerChildren?: React.ReactNode; hideHistogramIfEmpty?: boolean; @@ -75,8 +69,7 @@ export interface MatrixHistogramQueryProps { stackByField: string; startDate: string; indexToAdd?: string[] | null; - isInspected: boolean; - histogramType: HistogramType; + histogramType: MatrixHistogramType; } export interface MatrixHistogramProps extends MatrixHistogramBasicProps { diff --git a/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/histogram_configs.ts b/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/histogram_configs.ts index 6a05f97da2fef..b4893fa37571a 100644 --- a/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/histogram_configs.ts +++ b/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/histogram_configs.ts @@ -8,7 +8,7 @@ import { MatrixHistogramOption, MatrixHistogramConfigs, } from '../../../components/matrix_histogram/types'; -import { HistogramType } from '../../../../graphql/types'; +import { MatrixHistogramType } from '../../../../../common/search_strategy/security_solution/matrix_histogram'; export const anomaliesStackByOptions: MatrixHistogramOption[] = [ { @@ -24,7 +24,7 @@ export const histogramConfigs: MatrixHistogramConfigs = { anomaliesStackByOptions.find((o) => o.text === DEFAULT_STACK_BY) ?? anomaliesStackByOptions[0], errorMessage: i18n.ERROR_FETCHING_ANOMALIES_DATA, hideHistogramIfEmpty: true, - histogramType: HistogramType.anomalies, + histogramType: MatrixHistogramType.anomalies, stackByOptions: anomaliesStackByOptions, subtitle: undefined, title: i18n.ANOMALIES_TITLE, diff --git a/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/index.tsx b/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/index.tsx index 94019b26c180b..f6ebbb990f223 100644 --- a/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/index.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/index.tsx @@ -11,11 +11,12 @@ import { AnomaliesQueryTabBodyProps } from './types'; import { getAnomaliesFilterQuery } from './utils'; import { useInstalledSecurityJobs } from '../../../components/ml/hooks/use_installed_security_jobs'; import { useUiSetting$ } from '../../../lib/kibana'; -import { MatrixHistogramContainer } from '../../../components/matrix_histogram'; +import { MatrixHistogram } from '../../../components/matrix_histogram'; import { histogramConfigs } from './histogram_configs'; -const ID = 'anomaliesOverTimeQuery'; -export const AnomaliesQueryTabBody = ({ +const ID = 'anomaliesHistogramQuery'; + +const AnomaliesQueryTabBodyComponent: React.FC = ({ deleteQuery, endDate, setQuery, @@ -28,16 +29,7 @@ export const AnomaliesQueryTabBody = ({ AnomaliesTableComponent, flowTarget, ip, -}: AnomaliesQueryTabBodyProps) => { - useEffect(() => { - return () => { - if (deleteQuery) { - deleteQuery({ id: ID }); - } - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - +}) => { const { jobs } = useInstalledSecurityJobs(); const [anomalyScore] = useUiSetting$(DEFAULT_ANOMALY_SCORE); @@ -50,16 +42,23 @@ export const AnomaliesQueryTabBody = ({ ip ); + useEffect(() => { + return () => { + if (deleteQuery) { + deleteQuery({ id: ID }); + } + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + return ( <> - ({ - useApolloClient: jest.fn(), -})); - -jest.mock('../../lib/kibana', () => { - return { - useUiSetting$: jest.fn().mockReturnValue(['mockDefaultIndex']), - }; -}); - -jest.mock('./index.gql_query', () => { - return { - MatrixHistogramGqlQuery: 'mockGqlQuery', - }; -}); - -jest.mock('../../components/toasters/', () => ({ - useStateToaster: () => [jest.fn(), jest.fn()], - errorToToaster: jest.fn(), -})); - -describe('useQuery', () => { - let result: { - data: MatrixOverTimeHistogramData[] | null; - loading: boolean; - inspect: InspectQuery | null; - totalCount: number; - refetch: Refetch | undefined; - }; - describe('happy path', () => { - beforeAll(() => { - (useApolloClient as jest.Mock).mockReturnValue({ - query: mockQuery, - }); - const TestComponent = () => { - result = useQuery({ - endDate: '2020-07-07T08:20:00.000Z', - errorMessage: 'fakeErrorMsg', - filterQuery: '', - histogramType: HistogramType.alerts, - isInspected: false, - stackByField: 'fakeField', - startDate: '2020-07-07T08:08:00.000Z', - }); - - return
; - }; - - mount(); - }); - - test('should set variables', () => { - expect(mockQuery).toBeCalledWith({ - query: 'mockGqlQuery', - fetchPolicy: 'network-only', - variables: { - filterQuery: '', - sourceId: 'default', - timerange: { - interval: '12h', - from: '2020-07-07T08:08:00.000Z', - to: '2020-07-07T08:20:00.000Z', - }, - defaultIndex: 'mockDefaultIndex', - inspect: false, - stackByField: 'fakeField', - histogramType: 'alerts', - }, - context: { - fetchOptions: { - abortSignal: new AbortController().signal, - }, - }, - }); - }); - - test('should setData', () => { - expect(result.data).toEqual([{}]); - }); - - test('should set total count', () => { - expect(result.totalCount).toEqual(1); - }); - - test('should set inspect', () => { - expect(result.inspect).toEqual(false); - }); - }); - - describe('failure path', () => { - beforeAll(() => { - mockQuery.mockClear(); - (useApolloClient as jest.Mock).mockReset(); - (useApolloClient as jest.Mock).mockReturnValue({ - query: mockRejectQuery, - }); - const TestComponent = () => { - result = useQuery({ - endDate: '2020-07-07T08:20:18.966Z', - errorMessage: 'fakeErrorMsg', - filterQuery: '', - histogramType: HistogramType.alerts, - isInspected: false, - stackByField: 'fakeField', - startDate: '2020-07-08T08:20:18.966Z', - }); - - return
; - }; - - mount(); - }); - - test('should setData', () => { - expect(result.data).toEqual(null); - }); - - test('should set total count', () => { - expect(result.totalCount).toEqual(-1); - }); - - test('should set inspect', () => { - expect(result.inspect).toEqual(null); - }); - - test('should set error to toster', () => { - expect(errorToToaster).toHaveBeenCalled(); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts b/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts index c4702e915c076..65ad3cc994c67 100644 --- a/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts +++ b/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts @@ -5,29 +5,44 @@ */ import deepEqual from 'fast-deep-equal'; -import { isEmpty } from 'lodash/fp'; -import { useEffect, useMemo, useState, useRef } from 'react'; +import { isEmpty, noop } from 'lodash/fp'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; import { MatrixHistogramQueryProps } from '../../components/matrix_histogram/types'; -import { errorToToaster, useStateToaster } from '../../components/toasters'; -import { useUiSetting$ } from '../../lib/kibana'; -import { createFilter } from '../helpers'; -import { useApolloClient } from '../../utils/apollo_context'; -import { inputsModel } from '../../store'; -import { MatrixHistogramGqlQuery } from './index.gql_query'; -import { GetMatrixHistogramQuery, MatrixOverTimeHistogramData } from '../../../graphql/types'; +import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; +import { inputsModel } from '../../../common/store'; +import { createFilter } from '../../../common/containers/helpers'; +import { useKibana, useUiSetting$ } from '../../../common/lib/kibana'; +import { + MatrixHistogramQuery, + MatrixHistogramRequestOptions, + MatrixHistogramStrategyResponse, + MatrixHistogramData, +} from '../../../../common/search_strategy/security_solution'; +import { AbortError } from '../../../../../../../src/plugins/data/common'; +import { getInspectResponse } from '../../../helpers'; +import { InspectResponse } from '../../../types'; +import * as i18n from './translations'; -export const useQuery = ({ +export interface UseMatrixHistogramArgs { + data: MatrixHistogramData[]; + inspect: InspectResponse; + refetch: inputsModel.Refetch; + totalCount: number; +} + +export const useMatrixHistogram = ({ endDate, errorMessage, filterQuery, histogramType, indexToAdd, - isInspected, stackByField, startDate, -}: MatrixHistogramQueryProps) => { +}: MatrixHistogramQueryProps): [boolean, UseMatrixHistogramArgs] => { + const { data, notifications } = useKibana().services; + const refetch = useRef(noop); + const abortCtrl = useRef(new AbortController()); const [configIndex] = useUiSetting$(DEFAULT_INDEX_KEY); const defaultIndex = useMemo(() => { if (indexToAdd != null && !isEmpty(indexToAdd)) { @@ -35,108 +50,110 @@ export const useQuery = ({ } return configIndex; }, [configIndex, indexToAdd]); - const [, dispatchToaster] = useStateToaster(); - const refetch = useRef(); - const [loading, setLoading] = useState(false); - const [data, setData] = useState(null); - const [inspect, setInspect] = useState(null); - const [totalCount, setTotalCount] = useState(-1); - const apolloClient = useApolloClient(); - - const [matrixHistogramVariables, setMatrixHistogramVariables] = useState< - GetMatrixHistogramQuery.Variables + const [loading, setLoading] = useState(false); + const [matrixHistogramRequest, setMatrixHistogramRequest] = useState< + MatrixHistogramRequestOptions >({ + defaultIndex, + factoryQueryType: MatrixHistogramQuery, filterQuery: createFilter(filterQuery), - sourceId: 'default', + histogramType, timerange: { interval: '12h', - from: startDate!, - to: endDate!, + from: startDate, + to: endDate, }, - defaultIndex, - inspect: isInspected, stackByField, - histogramType, }); + const [matrixHistogramResponse, setMatrixHistogramResponse] = useState({ + data: [], + inspect: { + dsl: [], + response: [], + }, + refetch: refetch.current, + totalCount: -1, + }); + + const hostsSearch = useCallback( + (request: MatrixHistogramRequestOptions) => { + let didCancel = false; + const asyncSearch = async () => { + abortCtrl.current = new AbortController(); + setLoading(true); + + const searchSubscription$ = data.search + .search(request, { + strategy: 'securitySolutionSearchStrategy', + abortSignal: abortCtrl.current.signal, + }) + .subscribe({ + next: (response) => { + if (!response.isPartial && !response.isRunning) { + if (!didCancel) { + setLoading(false); + setMatrixHistogramResponse((prevResponse) => ({ + ...prevResponse, + data: response.matrixHistogramData, + inspect: getInspectResponse(response, prevResponse.inspect), + refetch: refetch.current, + totalCount: response.totalCount, + })); + } + searchSubscription$.unsubscribe(); + } else if (response.isPartial && !response.isRunning) { + if (!didCancel) { + setLoading(false); + } + // TODO: Make response error status clearer + notifications.toasts.addWarning(i18n.ERROR_MATRIX_HISTOGRAM); + searchSubscription$.unsubscribe(); + } + }, + error: (msg) => { + if (!(msg instanceof AbortError)) { + notifications.toasts.addDanger({ + title: errorMessage ?? i18n.FAIL_MATRIX_HISTOGRAM, + text: msg.message, + }); + } + }, + }); + }; + abortCtrl.current.abort(); + asyncSearch(); + refetch.current = asyncSearch; + return () => { + didCancel = true; + abortCtrl.current.abort(); + }; + }, + [data.search, errorMessage, notifications.toasts] + ); + useEffect(() => { - setMatrixHistogramVariables((prevVariables) => { - const localVariables = { + setMatrixHistogramRequest((prevRequest) => { + const myRequest = { + ...prevRequest, + defaultIndex, filterQuery: createFilter(filterQuery), - sourceId: 'default', timerange: { interval: '12h', - from: startDate!, - to: endDate!, + from: startDate, + to: endDate, }, - defaultIndex, - inspect: isInspected, - stackByField, - histogramType, }; - if (!deepEqual(prevVariables, localVariables)) { - return localVariables; + if (!deepEqual(prevRequest, myRequest)) { + return myRequest; } - return prevVariables; + return prevRequest; }); - }, [ - defaultIndex, - filterQuery, - histogramType, - indexToAdd, - isInspected, - stackByField, - startDate, - endDate, - ]); + }, [defaultIndex, endDate, filterQuery, startDate]); useEffect(() => { - let isSubscribed = true; - const abortCtrl = new AbortController(); - const abortSignal = abortCtrl.signal; - - async function fetchData() { - if (!apolloClient) return null; - setLoading(true); - return apolloClient - .query({ - query: MatrixHistogramGqlQuery, - fetchPolicy: 'network-only', - variables: matrixHistogramVariables, - context: { - fetchOptions: { - abortSignal, - }, - }, - }) - .then( - (result) => { - if (isSubscribed) { - const source = result?.data?.source?.MatrixHistogram ?? {}; - setData(source?.matrixHistogramData ?? []); - setTotalCount(source?.totalCount ?? -1); - setInspect(source?.inspect ?? null); - setLoading(false); - } - }, - (error) => { - if (isSubscribed) { - setData(null); - setTotalCount(-1); - setInspect(null); - setLoading(false); - errorToToaster({ title: errorMessage, error, dispatchToaster }); - } - } - ); - } - refetch.current = fetchData; - fetchData(); - return () => { - isSubscribed = false; - abortCtrl.abort(); - }; - }, [apolloClient, dispatchToaster, errorMessage, matrixHistogramVariables]); + hostsSearch(matrixHistogramRequest); + }, [matrixHistogramRequest, hostsSearch]); - return { data, loading, inspect, totalCount, refetch: refetch.current }; + return [loading, matrixHistogramResponse]; }; diff --git a/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/translations.ts b/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/translations.ts new file mode 100644 index 0000000000000..b15b28c6b49ae --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/translations.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const ERROR_MATRIX_HISTOGRAM = i18n.translate( + 'xpack.securitySolution.matrixHistogram.errorSearchDescription', + { + defaultMessage: `An error has occurred on matrix histogram search`, + } +); + +export const FAIL_MATRIX_HISTOGRAM = i18n.translate( + 'xpack.securitySolution.matrixHistogram.failSearchDescription', + { + defaultMessage: `Failed to run search on matrix histogram`, + } +); diff --git a/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx index 5436469409194..34f2385051f4c 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx @@ -54,6 +54,7 @@ interface UseAuthentications { endDate: string; startDate: string; type: hostsModel.HostsType; + skip: boolean; } export const useAuthentications = ({ @@ -62,6 +63,7 @@ export const useAuthentications = ({ endDate, startDate, type, + skip, }: UseAuthentications): [boolean, AuthenticationArgs] => { const getAuthenticationsSelector = hostsSelectors.authenticationsSelector(); const { activePage, limit } = useSelector( @@ -190,12 +192,12 @@ export const useAuthentications = ({ to: endDate, }, }; - if (!deepEqual(prevRequest, myRequest)) { + if (!skip && !deepEqual(prevRequest, myRequest)) { return myRequest; } return prevRequest; }); - }, [activePage, defaultIndex, docValueFields, endDate, filterQuery, limit, startDate]); + }, [activePage, defaultIndex, docValueFields, endDate, filterQuery, limit, skip, startDate]); useEffect(() => { authenticationsSearch(authenticationsRequest); diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.tsx b/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.tsx index 084d4b699e8eb..65ddb9305f607 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.tsx @@ -10,19 +10,20 @@ import { AuthenticationTable } from '../../components/authentications_table'; import { manageQuery } from '../../../common/components/page/manage_query'; import { useAuthentications } from '../../containers/authentications'; import { HostsComponentsQueryProps } from './types'; -import { hostsModel } from '../../store'; import { MatrixHistogramOption, MatrixHistogramMappingTypes, MatrixHistogramConfigs, } from '../../../common/components/matrix_histogram/types'; -import { MatrixHistogramContainer } from '../../../common/components/matrix_histogram'; +import { MatrixHistogram } from '../../../common/components/matrix_histogram'; import { KpiHostsChartColors } from '../../components/kpi_hosts/types'; import * as i18n from '../translations'; -import { HistogramType } from '../../../graphql/types'; +import { MatrixHistogramType } from '../../../../common/search_strategy/security_solution'; const AuthenticationTableManage = manageQuery(AuthenticationTable); -const ID = 'authenticationsOverTimeQuery'; + +const ID = 'authenticationsHistogramQuery'; + const authStackByOptions: MatrixHistogramOption[] = [ { text: 'event.outcome', @@ -53,13 +54,13 @@ const histogramConfigs: MatrixHistogramConfigs = { defaultStackByOption: authStackByOptions.find((o) => o.text === DEFAULT_STACK_BY) ?? authStackByOptions[0], errorMessage: i18n.ERROR_FETCHING_AUTHENTICATIONS_DATA, - histogramType: HistogramType.authentications, + histogramType: MatrixHistogramType.authentications, mapping: authMatrixDataMappingFields, stackByOptions: authStackByOptions, title: i18n.NAVIGATION_AUTHENTICATIONS_TITLE, }; -export const AuthenticationsQueryTabBody = ({ +const AuthenticationsQueryTabBodyComponent: React.FC = ({ deleteQuery, docValueFields, endDate, @@ -68,7 +69,12 @@ export const AuthenticationsQueryTabBody = ({ setQuery, startDate, type, -}: HostsComponentsQueryProps) => { +}) => { + const [ + loading, + { authentications, totalCount, pageInfo, loadPage, id, inspect, isInspected, refetch }, + ] = useAuthentications({ docValueFields, endDate, filterQuery, skip, startDate, type }); + useEffect(() => { return () => { if (deleteQuery) { @@ -77,21 +83,14 @@ export const AuthenticationsQueryTabBody = ({ }; }, [deleteQuery]); - const [ - loading, - { authentications, totalCount, pageInfo, loadPage, id, inspect, isInspected, refetch }, - ] = useAuthentications({ docValueFields, endDate, filterQuery, startDate, type }); - return ( <> - @@ -114,4 +113,8 @@ export const AuthenticationsQueryTabBody = ({ ); }; +AuthenticationsQueryTabBodyComponent.displayName = 'AuthenticationsQueryTabBodyComponent'; + +export const AuthenticationsQueryTabBody = React.memo(AuthenticationsQueryTabBodyComponent); + AuthenticationsQueryTabBody.displayName = 'AuthenticationsQueryTabBody'; diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx b/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx index f28c3dfa1ad77..be8412caf7732 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx @@ -10,19 +10,18 @@ import { useDispatch } from 'react-redux'; import { TimelineId } from '../../../../common/types/timeline'; import { StatefulEventsViewer } from '../../../common/components/events_viewer'; import { HostsComponentsQueryProps } from './types'; -import { hostsModel } from '../../store'; import { eventsDefaultModel } from '../../../common/components/events_viewer/default_model'; import { MatrixHistogramOption, MatrixHistogramConfigs, } from '../../../common/components/matrix_histogram/types'; -import { MatrixHistogramContainer } from '../../../common/components/matrix_histogram'; +import { MatrixHistogram } from '../../../common/components/matrix_histogram'; import { useFullScreen } from '../../../common/containers/use_full_screen'; import * as i18n from '../translations'; -import { HistogramType } from '../../../graphql/types'; +import { MatrixHistogramType } from '../../../../common/search_strategy/security_solution'; import { useManageTimeline } from '../../../timelines/components/manage_timeline'; -const EVENTS_HISTOGRAM_ID = 'eventsOverTimeQuery'; +const EVENTS_HISTOGRAM_ID = 'eventsHistogramQuery'; export const eventsStackByOptions: MatrixHistogramOption[] = [ { @@ -45,7 +44,7 @@ export const histogramConfigs: MatrixHistogramConfigs = { defaultStackByOption: eventsStackByOptions.find((o) => o.text === DEFAULT_STACK_BY) ?? eventsStackByOptions[0], errorMessage: i18n.ERROR_FETCHING_EVENTS_DATA, - histogramType: HistogramType.events, + histogramType: MatrixHistogramType.events, stackByOptions: eventsStackByOptions, subtitle: undefined, title: i18n.NAVIGATION_EVENTS_TITLE, @@ -59,8 +58,8 @@ const EventsQueryTabBodyComponent: React.FC = ({ setQuery, startDate, }) => { - const { initializeTimeline } = useManageTimeline(); const dispatch = useDispatch(); + const { initializeTimeline } = useManageTimeline(); const { globalFullScreen } = useFullScreen(); useEffect(() => { initializeTimeline({ @@ -80,13 +79,11 @@ const EventsQueryTabBodyComponent: React.FC = ({ return ( <> {!globalFullScreen && ( - diff --git a/x-pack/plugins/security_solution/public/network/containers/network_dns/histogram.ts b/x-pack/plugins/security_solution/public/network/containers/network_dns/histogram.ts new file mode 100644 index 0000000000000..dce0c3bd2b30d --- /dev/null +++ b/x-pack/plugins/security_solution/public/network/containers/network_dns/histogram.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { connect } from 'react-redux'; +import { compose } from 'redux'; +import { DocumentNode } from 'graphql'; +import { ScaleType } from '@elastic/charts'; + +import { MatrixHistogram } from '../../../common/components/matrix_histogram'; +import { + MatrixHistogramOption, + GetSubTitle, +} from '../../../common/components/matrix_histogram/types'; +import { UpdateDateRange } from '../../../common/components/charts/common'; +import { GlobalTimeArgs } from '../../../common/containers/use_global_time'; +import { withKibana } from '../../../common/lib/kibana'; +import { QueryTemplatePaginatedProps } from '../../../common/containers/query_template_paginated'; +import { DEFAULT_TABLE_ACTIVE_PAGE, DEFAULT_TABLE_LIMIT } from '../../../common/store/constants'; +import { networkModel, networkSelectors } from '../../store'; +import { State, inputsSelectors } from '../../../common/store'; + +export const HISTOGRAM_ID = 'networkDnsHistogramQuery'; + +interface DnsHistogramOwnProps extends QueryTemplatePaginatedProps { + dataKey: string | string[]; + defaultStackByOption: MatrixHistogramOption; + errorMessage: string; + isDnsHistogram?: boolean; + query: DocumentNode; + scaleType: ScaleType; + setQuery: GlobalTimeArgs['setQuery']; + showLegend?: boolean; + stackByOptions: MatrixHistogramOption[]; + subtitle?: string | GetSubTitle; + title: string; + type: networkModel.NetworkType; + updateDateRange: UpdateDateRange; + yTickFormatter?: (value: number) => string; +} + +const makeMapHistogramStateToProps = () => { + const getNetworkDnsSelector = networkSelectors.dnsSelector(); + const getQuery = inputsSelectors.globalQueryByIdSelector(); + const mapStateToProps = (state: State, { id = HISTOGRAM_ID }: DnsHistogramOwnProps) => { + const { isInspected } = getQuery(state, id); + return { + ...getNetworkDnsSelector(state), + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + limit: DEFAULT_TABLE_LIMIT, + isInspected, + id, + }; + }; + + return mapStateToProps; +}; + +export const NetworkDnsHistogramQuery = compose>( + connect(makeMapHistogramStateToProps), + withKibana +)(MatrixHistogram); diff --git a/x-pack/plugins/security_solution/public/network/containers/network_dns/index.tsx b/x-pack/plugins/security_solution/public/network/containers/network_dns/index.tsx index 72e3161de5373..53d9a303ab849 100644 --- a/x-pack/plugins/security_solution/public/network/containers/network_dns/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/network_dns/index.tsx @@ -4,48 +4,38 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getOr } from 'lodash/fp'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { connect } from 'react-redux'; -import { compose } from 'redux'; - -import { DocumentNode } from 'graphql'; -import { ScaleType } from '@elastic/charts'; +import { noop } from 'lodash/fp'; +import { useState, useEffect, useCallback, useRef } from 'react'; +import { shallowEqual, useSelector } from 'react-redux'; +import deepEqual from 'fast-deep-equal'; + +import { ESTermQuery } from '../../../../common/typed_json'; import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; -import { - GetNetworkDnsQuery, - NetworkDnsEdges, - NetworkDnsSortField, - PageInfoPaginated, - MatrixOverOrdinalHistogramData, -} from '../../../graphql/types'; -import { inputsModel, State, inputsSelectors } from '../../../common/store'; -import { withKibana, WithKibanaProps } from '../../../common/lib/kibana'; +import { inputsModel, State } from '../../../common/store'; +import { useKibana } from '../../../common/lib/kibana'; +import { createFilter } from '../../../common/containers/helpers'; +import { NetworkDnsEdges, PageInfoPaginated } from '../../../graphql/types'; import { generateTablePaginationOptions } from '../../../common/components/paginated_table/helpers'; -import { createFilter, getDefaultFetchPolicy } from '../../../common/containers/helpers'; -import { - QueryTemplatePaginated, - QueryTemplatePaginatedProps, -} from '../../../common/containers/query_template_paginated'; -import { networkDnsQuery } from './index.gql_query'; -import { DEFAULT_TABLE_ACTIVE_PAGE, DEFAULT_TABLE_LIMIT } from '../../../common/store/constants'; -import { MatrixHistogram } from '../../../common/components/matrix_histogram'; -import { - MatrixHistogramOption, - GetSubTitle, -} from '../../../common/components/matrix_histogram/types'; -import { UpdateDateRange } from '../../../common/components/charts/common'; -import { GlobalTimeArgs } from '../../../common/containers/use_global_time'; import { networkModel, networkSelectors } from '../../store'; +import { + NetworkQueries, + NetworkDnsRequestOptions, + NetworkDnsStrategyResponse, + MatrixOverOrdinalHistogramData, +} from '../../../../common/search_strategy/security_solution/network'; +import { AbortError } from '../../../../../../../src/plugins/data/common'; +import * as i18n from './translations'; +import { getInspectResponse } from '../../../helpers'; +import { InspectResponse } from '../../../types'; + +export * from './histogram'; const ID = 'networkDnsQuery'; -export const HISTOGRAM_ID = 'networkDnsHistogramQuery'; + export interface NetworkDnsArgs { id: string; - inspect: inputsModel.InspectQuery; + inspect: InspectResponse; isInspected: boolean; - loading: boolean; loadPage: (newActivePage: number) => void; networkDns: NetworkDnsEdges[]; pageInfo: PageInfoPaginated; @@ -55,162 +45,164 @@ export interface NetworkDnsArgs { histogram: MatrixOverOrdinalHistogramData[]; } -export interface OwnProps extends QueryTemplatePaginatedProps { - children: (args: NetworkDnsArgs) => React.ReactNode; +interface UseNetworkDns { + id?: string; type: networkModel.NetworkType; + filterQuery?: ESTermQuery | string; + endDate: string; + startDate: string; + skip: boolean; } -interface DnsHistogramOwnProps extends QueryTemplatePaginatedProps { - dataKey: string | string[]; - defaultStackByOption: MatrixHistogramOption; - errorMessage: string; - isDnsHistogram?: boolean; - query: DocumentNode; - scaleType: ScaleType; - setQuery: GlobalTimeArgs['setQuery']; - showLegend?: boolean; - stackByOptions: MatrixHistogramOption[]; - subtitle?: string | GetSubTitle; - title: string; - type: networkModel.NetworkType; - updateDateRange: UpdateDateRange; - yTickFormatter?: (value: number) => string; -} +export const useNetworkDns = ({ + endDate, + filterQuery, + id = ID, + skip, + startDate, + type, +}: UseNetworkDns): [boolean, NetworkDnsArgs] => { + const getNetworkDnsSelector = networkSelectors.dnsSelector(); + const { activePage, sort, isPtrIncluded, limit } = useSelector( + (state: State) => getNetworkDnsSelector(state), + shallowEqual + ); + const { data, notifications, uiSettings } = useKibana().services; + const refetch = useRef(noop); + const abortCtrl = useRef(new AbortController()); + const defaultIndex = uiSettings.get(DEFAULT_INDEX_KEY); + const [loading, setLoading] = useState(false); -export interface NetworkDnsComponentReduxProps { - activePage: number; - sort: NetworkDnsSortField; - isInspected: boolean; - isPtrIncluded: boolean; - limit: number; -} + const [networkDnsRequest, setNetworkDnsRequest] = useState({ + defaultIndex, + factoryQueryType: NetworkQueries.dns, + filterQuery: createFilter(filterQuery), + isPtrIncluded, + pagination: generateTablePaginationOptions(activePage, limit), + sort, + timerange: { + interval: '12h', + from: startDate ? startDate : '', + to: endDate ? endDate : new Date(Date.now()).toISOString(), + }, + }); + + const wrappedLoadMore = useCallback( + (newActivePage: number) => { + setNetworkDnsRequest((prevRequest) => ({ + ...prevRequest, + pagination: generateTablePaginationOptions(newActivePage, limit), + })); + }, + [limit] + ); -type NetworkDnsProps = OwnProps & NetworkDnsComponentReduxProps & WithKibanaProps; - -export class NetworkDnsComponentQuery extends QueryTemplatePaginated< - NetworkDnsProps, - GetNetworkDnsQuery.Query, - GetNetworkDnsQuery.Variables -> { - public render() { - const { - activePage, - children, - sort, - endDate, - filterQuery, - id = ID, - isInspected, - isPtrIncluded, - kibana, - limit, - skip, - sourceId, - startDate, - } = this.props; - const variables: GetNetworkDnsQuery.Variables = { - defaultIndex: kibana.services.uiSettings.get(DEFAULT_INDEX_KEY), - filterQuery: createFilter(filterQuery), - inspect: isInspected, - isPtrIncluded, - pagination: generateTablePaginationOptions(activePage, limit), - sort, - sourceId, - timerange: { - interval: '12h', - from: startDate!, - to: endDate!, - }, - }; - - return ( - - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - query={networkDnsQuery} - skip={skip} - variables={variables} - > - {({ data, loading, fetchMore, networkStatus, refetch }) => { - const networkDns = getOr([], `source.NetworkDns.edges`, data); - this.setFetchMore(fetchMore); - this.setFetchMoreOptions((newActivePage: number) => ({ - variables: { - pagination: generateTablePaginationOptions(newActivePage, limit), + const [networkDnsResponse, setNetworkDnsResponse] = useState({ + networkDns: [], + histogram: [], + id: ID, + inspect: { + dsl: [], + response: [], + }, + isInspected: false, + loadPage: wrappedLoadMore, + pageInfo: { + activePage: 0, + fakeTotalCount: 0, + showMorePagesIndicator: false, + }, + refetch: refetch.current, + totalCount: -1, + }); + + const networkDnsSearch = useCallback( + (request: NetworkDnsRequestOptions) => { + let didCancel = false; + const asyncSearch = async () => { + abortCtrl.current = new AbortController(); + setLoading(true); + + const searchSubscription$ = data.search + .search(request, { + strategy: 'securitySolutionSearchStrategy', + abortSignal: abortCtrl.current.signal, + }) + .subscribe({ + next: (response) => { + if (!response.isPartial && !response.isRunning) { + if (!didCancel) { + setLoading(false); + setNetworkDnsResponse((prevResponse) => ({ + ...prevResponse, + networkDns: response.edges, + inspect: getInspectResponse(response, prevResponse.inspect), + pageInfo: response.pageInfo, + refetch: refetch.current, + totalCount: response.totalCount, + histogram: response.histogram ?? prevResponse.histogram, + })); + } + searchSubscription$.unsubscribe(); + } else if (response.isPartial && !response.isRunning) { + if (!didCancel) { + setLoading(false); + } + // TODO: Make response error status clearer + notifications.toasts.addWarning(i18n.ERROR_NETWORK_DNS); + searchSubscription$.unsubscribe(); + } }, - updateQuery: (prev, { fetchMoreResult }) => { - if (!fetchMoreResult) { - return prev; + error: (msg) => { + if (!(msg instanceof AbortError)) { + notifications.toasts.addDanger({ + title: i18n.FAIL_NETWORK_DNS, + text: msg.message, + }); } - return { - ...fetchMoreResult, - source: { - ...fetchMoreResult.source, - NetworkDns: { - ...fetchMoreResult.source.NetworkDns, - edges: [...fetchMoreResult.source.NetworkDns.edges], - }, - }, - }; }, - })); - const isLoading = this.isItAValidLoading(loading, variables, networkStatus); - return children({ - id, - inspect: getOr(null, 'source.NetworkDns.inspect', data), - isInspected, - loading: isLoading, - loadPage: this.wrappedLoadMore, - networkDns, - pageInfo: getOr({}, 'source.NetworkDns.pageInfo', data), - refetch: this.memoizedRefetchQuery(variables, limit, refetch), - totalCount: getOr(-1, 'source.NetworkDns.totalCount', data), - histogram: getOr(null, 'source.NetworkDns.histogram', data), }); - }} - - ); - } -} + }; + abortCtrl.current.abort(); + asyncSearch(); + refetch.current = asyncSearch; + return () => { + didCancel = true; + abortCtrl.current.abort(); + }; + }, + [data.search, notifications.toasts] + ); -const makeMapStateToProps = () => { - const getNetworkDnsSelector = networkSelectors.dnsSelector(); - const getQuery = inputsSelectors.globalQueryByIdSelector(); - const mapStateToProps = (state: State, { id = ID }: OwnProps) => { - const { isInspected } = getQuery(state, id); - return { - ...getNetworkDnsSelector(state), - isInspected, - id, - }; - }; - - return mapStateToProps; -}; + useEffect(() => { + if (skip) { + return; + } -const makeMapHistogramStateToProps = () => { - const getNetworkDnsSelector = networkSelectors.dnsSelector(); - const getQuery = inputsSelectors.globalQueryByIdSelector(); - const mapStateToProps = (state: State, { id = HISTOGRAM_ID }: DnsHistogramOwnProps) => { - const { isInspected } = getQuery(state, id); - return { - ...getNetworkDnsSelector(state), - activePage: DEFAULT_TABLE_ACTIVE_PAGE, - limit: DEFAULT_TABLE_LIMIT, - isInspected, - id, - }; - }; - - return mapStateToProps; -}; + setNetworkDnsRequest((prevRequest) => { + const myRequest = { + ...prevRequest, + defaultIndex, + isPtrIncluded, + filterQuery: createFilter(filterQuery), + pagination: generateTablePaginationOptions(activePage, limit), + sort, + timerange: { + interval: '12h', + from: startDate, + to: endDate, + }, + }; + if (!deepEqual(prevRequest, myRequest)) { + return myRequest; + } + return prevRequest; + }); + }, [activePage, defaultIndex, endDate, filterQuery, limit, startDate, sort, skip, isPtrIncluded]); -export const NetworkDnsQuery = compose>( - connect(makeMapStateToProps), - withKibana -)(NetworkDnsComponentQuery); + useEffect(() => { + networkDnsSearch(networkDnsRequest); + }, [networkDnsRequest, networkDnsSearch]); -export const NetworkDnsHistogramQuery = compose>( - connect(makeMapHistogramStateToProps), - withKibana -)(MatrixHistogram); + return [loading, networkDnsResponse]; +}; diff --git a/x-pack/plugins/security_solution/public/network/containers/network_dns/translations.ts b/x-pack/plugins/security_solution/public/network/containers/network_dns/translations.ts new file mode 100644 index 0000000000000..54c36dd1536f1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/network/containers/network_dns/translations.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const ERROR_NETWORK_DNS = i18n.translate( + 'xpack.securitySolution.networkDns.errorSearchDescription', + { + defaultMessage: `An error has occurred on network dns search`, + } +); + +export const FAIL_NETWORK_DNS = i18n.translate( + 'xpack.securitySolution.networkDns.failSearchDescription', + { + defaultMessage: `Failed to run search on network dns`, + } +); diff --git a/x-pack/plugins/security_solution/public/network/pages/ip_details/types.ts b/x-pack/plugins/security_solution/public/network/pages/ip_details/types.ts index d1ee48a9a5d9e..cab6e8e09b200 100644 --- a/x-pack/plugins/security_solution/public/network/pages/ip_details/types.ts +++ b/x-pack/plugins/security_solution/public/network/pages/ip_details/types.ts @@ -11,7 +11,7 @@ import { NetworkType } from '../../store/model'; import { FlowTarget, FlowTargetSourceDest, -} from '../../../../common/search_strategy/security_solution/network'; +} from '../../../../common/search_strategy/security_solution'; import { GlobalTimeArgs } from '../../../common/containers/use_global_time'; export const type = NetworkType.details; diff --git a/x-pack/plugins/security_solution/public/network/pages/navigation/dns_query_tab_body.tsx b/x-pack/plugins/security_solution/public/network/pages/navigation/dns_query_tab_body.tsx index 2886089a1eb99..5adb78edbec8e 100644 --- a/x-pack/plugins/security_solution/public/network/pages/navigation/dns_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/navigation/dns_query_tab_body.tsx @@ -8,19 +8,18 @@ import React, { useEffect, useCallback, useMemo } from 'react'; import { getOr } from 'lodash/fp'; import { NetworkDnsTable } from '../../components/network_dns_table'; -import { NetworkDnsQuery, HISTOGRAM_ID } from '../../containers/network_dns'; +import { useNetworkDns, HISTOGRAM_ID } from '../../containers/network_dns'; import { manageQuery } from '../../../common/components/page/manage_query'; import { NetworkComponentQueryProps } from './types'; -import { networkModel } from '../../store'; import { MatrixHistogramOption, MatrixHistogramConfigs, } from '../../../common/components/matrix_histogram/types'; import * as i18n from '../translations'; -import { MatrixHistogramContainer } from '../../../common/components/matrix_histogram'; -import { HistogramType } from '../../../graphql/types'; +import { MatrixHistogram } from '../../../common/components/matrix_histogram'; +import { MatrixHistogramType } from '../../../../common/search_strategy/security_solution'; const NetworkDnsTableManage = manageQuery(NetworkDnsTable); @@ -37,12 +36,12 @@ export const histogramConfigs: Omit = { defaultStackByOption: dnsStackByOptions.find((o) => o.text === DEFAULT_STACK_BY) ?? dnsStackByOptions[0], errorMessage: i18n.ERROR_FETCHING_DNS_DATA, - histogramType: HistogramType.dns, + histogramType: MatrixHistogramType.dns, stackByOptions: dnsStackByOptions, subtitle: undefined, }; -export const DnsQueryTabBody = ({ +const DnsQueryTabBodyComponent: React.FC = ({ deleteQuery, endDate, filterQuery, @@ -50,7 +49,7 @@ export const DnsQueryTabBody = ({ startDate, setQuery, type, -}: NetworkComponentQueryProps) => { +}) => { useEffect(() => { return () => { if (deleteQuery) { @@ -59,6 +58,17 @@ export const DnsQueryTabBody = ({ }; }, [deleteQuery]); + const [ + loading, + { totalCount, networkDns, pageInfo, loadPage, id, inspect, isInspected, refetch }, + ] = useNetworkDns({ + endDate, + filterQuery, + skip, + startDate, + type, + }); + const getTitle = useCallback( (option: MatrixHistogramOption) => i18n.DOMAINS_COUNT_BY(option.text), [] @@ -74,54 +84,33 @@ export const DnsQueryTabBody = ({ return ( <> - - - {({ - totalCount, - loading, - networkDns, - pageInfo, - loadPage, - id, - inspect, - isInspected, - refetch, - }) => ( - - )} - + /> ); }; -DnsQueryTabBody.displayName = 'DNSQueryTabBody'; +DnsQueryTabBodyComponent.displayName = 'DnsQueryTabBodyComponent'; + +export const DnsQueryTabBody = React.memo(DnsQueryTabBodyComponent); 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 index a35d85d1321f5..e365ac38d31df 100644 --- 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 @@ -10,7 +10,7 @@ import React from 'react'; import { ThemeProvider } from 'styled-components'; import '../../../common/mock/match_media'; -import { useQuery } from '../../../common/containers/matrix_histogram'; +import { useMatrixHistogram } from '../../../common/containers/matrix_histogram'; // we don't have the types for waitFor just yet, so using "as waitFor" until when we do import { wait as waitFor } from '@testing-library/react'; import { mockIndexPattern, TestProviders } from '../../../common/mock'; @@ -19,11 +19,9 @@ import { AlertsByCategory } from '.'; jest.mock('../../../common/components/link_to'); jest.mock('../../../common/lib/kibana'); -jest.mock('../../../common/containers/matrix_histogram', () => { - return { - useQuery: jest.fn(), - }; -}); +jest.mock('../../../common/containers/matrix_histogram', () => ({ + useMatrixHistogram: jest.fn(), +})); const theme = () => ({ eui: { ...euiDarkVars, euiSizeL: '24px' }, darkMode: true }); const from = '2020-03-31T06:00:00.000Z'; @@ -34,12 +32,14 @@ describe('Alerts by category', () => { describe('before loading data', () => { beforeAll(async () => { - (useQuery as jest.Mock).mockReturnValue({ - data: null, - loading: false, - inspect: false, - totalCount: null, - }); + (useMatrixHistogram as jest.Mock).mockReturnValue([ + false, + { + data: null, + inspect: false, + totalCount: null, + }, + ]); wrapper = mount( @@ -100,19 +100,21 @@ describe('Alerts by category', () => { describe('after loading data', () => { beforeAll(async () => { - (useQuery as jest.Mock).mockReturnValue({ - 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' }, - ], - loading: false, - inspect: false, - totalCount: 6, - }); + (useMatrixHistogram 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( 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 index 111935782949b..1a2238c763bda 100644 --- 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 @@ -10,7 +10,7 @@ import { Position } from '@elastic/charts'; import { DEFAULT_NUMBER_FORMAT, APP_ID } from '../../../../common/constants'; import { SHOWING, UNIT } from '../../../common/components/alerts_viewer/translations'; -import { MatrixHistogramContainer } from '../../../common/components/matrix_histogram'; +import { MatrixHistogram } from '../../../common/components/matrix_histogram'; import { useKibana, useUiSetting$ } from '../../../common/lib/kibana'; import { convertToBuildEsQuery } from '../../../common/lib/keury'; import { @@ -19,7 +19,7 @@ import { IIndexPattern, Query, } from '../../../../../../../src/plugins/data/public'; -import { HostsTableType, HostsType } from '../../../hosts/store/model'; +import { HostsTableType } from '../../../hosts/store/model'; import * as i18n from '../../pages/translations'; import { @@ -107,7 +107,7 @@ const AlertsByCategoryComponent: React.FC = ({ ); return ( - = ({ headerChildren={hideHeaderChildren ? null : alertsCountViewAlertsButton} id={ID} setQuery={setQuery} - sourceId="default" startDate={from} - type={HostsType.page} {...alertsByCategoryHistogramConfigs} /> ); 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 2e9c25f01b3c1..7025afde963f1 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 @@ -12,7 +12,7 @@ import uuid from 'uuid'; import { DEFAULT_NUMBER_FORMAT, APP_ID } from '../../../../common/constants'; import { SHOWING, UNIT } from '../../../common/components/events_viewer/translations'; import { getTabsOnHostsUrl } from '../../../common/components/link_to/redirect_to_hosts'; -import { MatrixHistogramContainer } from '../../../common/components/matrix_histogram'; +import { MatrixHistogram } from '../../../common/components/matrix_histogram'; import { MatrixHistogramConfigs, MatrixHistogramOption, @@ -27,7 +27,7 @@ import { IIndexPattern, Query, } from '../../../../../../../src/plugins/data/public'; -import { HostsTableType, HostsType } from '../../../hosts/store/model'; +import { HostsTableType } from '../../../hosts/store/model'; import { InputsModelId } from '../../../common/store/inputs/constants'; import { GlobalTimeArgs } from '../../../common/containers/use_global_time'; @@ -159,7 +159,7 @@ const EventsByDatasetComponent: React.FC = ({ }, [onlyField, headerChildren, eventsCountViewEventsButton]); return ( - = ({ setAbsoluteRangeDatePickerTarget={setAbsoluteRangeDatePickerTarget} setQuery={setQuery} showSpacer={showSpacer} - sourceId="default" startDate={from} timelineId={timelineId} - type={HostsType.page} {...eventsByDatasetHistogramConfigs} title={onlyField != null ? i18n.TOP(onlyField) : eventsByDatasetHistogramConfigs.title} /> diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/__mocks__/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/__mocks__/index.ts new file mode 100644 index 0000000000000..b9f04502286e5 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/__mocks__/index.ts @@ -0,0 +1,777 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IEsSearchResponse } from '../../../../../../../../../../src/plugins/data/common'; + +import { + Direction, + HostAggEsItem, + HostsFields, + HostsQueries, + HostsRequestOptions, +} from '../../../../../../../common/search_strategy'; + +export const mockOptions: HostsRequestOptions = { + defaultIndex: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'winlogbeat-*', + ], + docValueFields: [ + { field: '@timestamp', format: 'date_time' }, + { field: 'event.created', format: 'date_time' }, + { field: 'event.end', format: 'date_time' }, + { field: 'event.ingested', format: 'date_time' }, + { field: 'event.start', format: 'date_time' }, + { field: 'file.accessed', format: 'date_time' }, + { field: 'file.created', format: 'date_time' }, + { field: 'file.ctime', format: 'date_time' }, + { field: 'file.mtime', format: 'date_time' }, + { field: 'package.installed', format: 'date_time' }, + { field: 'process.parent.start', format: 'date_time' }, + { field: 'process.start', format: 'date_time' }, + { field: 'system.audit.host.boottime', format: 'date_time' }, + { field: 'system.audit.package.installtime', format: 'date_time' }, + { field: 'system.audit.user.password.last_changed', format: 'date_time' }, + { field: 'tls.client.not_after', format: 'date_time' }, + { field: 'tls.client.not_before', format: 'date_time' }, + { field: 'tls.server.not_after', format: 'date_time' }, + { field: 'tls.server.not_before', format: 'date_time' }, + { field: 'aws.cloudtrail.user_identity.session_context.creation_date', format: 'date_time' }, + { field: 'azure.auditlogs.properties.activity_datetime', format: 'date_time' }, + { field: 'azure.enqueued_time', format: 'date_time' }, + { field: 'azure.signinlogs.properties.created_at', format: 'date_time' }, + { field: 'cef.extensions.agentReceiptTime', format: 'date_time' }, + { field: 'cef.extensions.deviceCustomDate1', format: 'date_time' }, + { field: 'cef.extensions.deviceCustomDate2', format: 'date_time' }, + { field: 'cef.extensions.deviceReceiptTime', format: 'date_time' }, + { field: 'cef.extensions.endTime', format: 'date_time' }, + { field: 'cef.extensions.fileCreateTime', format: 'date_time' }, + { field: 'cef.extensions.fileModificationTime', format: 'date_time' }, + { field: 'cef.extensions.flexDate1', format: 'date_time' }, + { field: 'cef.extensions.managerReceiptTime', format: 'date_time' }, + { field: 'cef.extensions.oldFileCreateTime', format: 'date_time' }, + { field: 'cef.extensions.oldFileModificationTime', format: 'date_time' }, + { field: 'cef.extensions.startTime', format: 'date_time' }, + { field: 'checkpoint.subs_exp', format: 'date_time' }, + { field: 'crowdstrike.event.EndTimestamp', format: 'date_time' }, + { field: 'crowdstrike.event.IncidentEndTime', format: 'date_time' }, + { field: 'crowdstrike.event.IncidentStartTime', format: 'date_time' }, + { field: 'crowdstrike.event.ProcessEndTime', format: 'date_time' }, + { field: 'crowdstrike.event.ProcessStartTime', format: 'date_time' }, + { field: 'crowdstrike.event.StartTimestamp', format: 'date_time' }, + { field: 'crowdstrike.event.Timestamp', format: 'date_time' }, + { field: 'crowdstrike.event.UTCTimestamp', format: 'date_time' }, + { field: 'crowdstrike.metadata.eventCreationTime', format: 'date_time' }, + { field: 'gsuite.admin.email.log_search_filter.end_date', format: 'date_time' }, + { field: 'gsuite.admin.email.log_search_filter.start_date', format: 'date_time' }, + { field: 'gsuite.admin.user.birthdate', format: 'date_time' }, + { field: 'kafka.block_timestamp', format: 'date_time' }, + { field: 'microsoft.defender_atp.lastUpdateTime', format: 'date_time' }, + { field: 'microsoft.defender_atp.resolvedTime', format: 'date_time' }, + { field: 'misp.campaign.first_seen', format: 'date_time' }, + { field: 'misp.campaign.last_seen', format: 'date_time' }, + { field: 'misp.intrusion_set.first_seen', format: 'date_time' }, + { field: 'misp.intrusion_set.last_seen', format: 'date_time' }, + { field: 'misp.observed_data.first_observed', format: 'date_time' }, + { field: 'misp.observed_data.last_observed', format: 'date_time' }, + { field: 'misp.report.published', format: 'date_time' }, + { field: 'misp.threat_indicator.valid_from', format: 'date_time' }, + { field: 'misp.threat_indicator.valid_until', format: 'date_time' }, + { field: 'netflow.collection_time_milliseconds', format: 'date_time' }, + { field: 'netflow.exporter.timestamp', format: 'date_time' }, + { field: 'netflow.flow_end_microseconds', format: 'date_time' }, + { field: 'netflow.flow_end_milliseconds', format: 'date_time' }, + { field: 'netflow.flow_end_nanoseconds', format: 'date_time' }, + { field: 'netflow.flow_end_seconds', format: 'date_time' }, + { field: 'netflow.flow_start_microseconds', format: 'date_time' }, + { field: 'netflow.flow_start_milliseconds', format: 'date_time' }, + { field: 'netflow.flow_start_nanoseconds', format: 'date_time' }, + { field: 'netflow.flow_start_seconds', format: 'date_time' }, + { field: 'netflow.max_export_seconds', format: 'date_time' }, + { field: 'netflow.max_flow_end_microseconds', format: 'date_time' }, + { field: 'netflow.max_flow_end_milliseconds', format: 'date_time' }, + { field: 'netflow.max_flow_end_nanoseconds', format: 'date_time' }, + { field: 'netflow.max_flow_end_seconds', format: 'date_time' }, + { field: 'netflow.min_export_seconds', format: 'date_time' }, + { field: 'netflow.min_flow_start_microseconds', format: 'date_time' }, + { field: 'netflow.min_flow_start_milliseconds', format: 'date_time' }, + { field: 'netflow.min_flow_start_nanoseconds', format: 'date_time' }, + { field: 'netflow.min_flow_start_seconds', format: 'date_time' }, + { field: 'netflow.monitoring_interval_end_milli_seconds', format: 'date_time' }, + { field: 'netflow.monitoring_interval_start_milli_seconds', format: 'date_time' }, + { field: 'netflow.observation_time_microseconds', format: 'date_time' }, + { field: 'netflow.observation_time_milliseconds', format: 'date_time' }, + { field: 'netflow.observation_time_nanoseconds', format: 'date_time' }, + { field: 'netflow.observation_time_seconds', format: 'date_time' }, + { field: 'netflow.system_init_time_milliseconds', format: 'date_time' }, + { field: 'rsa.internal.lc_ctime', format: 'date_time' }, + { field: 'rsa.internal.time', format: 'date_time' }, + { field: 'rsa.time.effective_time', format: 'date_time' }, + { field: 'rsa.time.endtime', format: 'date_time' }, + { field: 'rsa.time.event_queue_time', format: 'date_time' }, + { field: 'rsa.time.event_time', format: 'date_time' }, + { field: 'rsa.time.expire_time', format: 'date_time' }, + { field: 'rsa.time.recorded_time', format: 'date_time' }, + { field: 'rsa.time.stamp', format: 'date_time' }, + { field: 'rsa.time.starttime', format: 'date_time' }, + { field: 'sophos.xg.date', format: 'date_time' }, + { field: 'sophos.xg.eventtime', format: 'date_time' }, + { field: 'sophos.xg.start_time', format: 'date_time' }, + ], + factoryQueryType: HostsQueries.hosts, + filterQuery: '{"bool":{"must":[],"filter":[{"match_all":{}}],"should":[],"must_not":[]}}', + pagination: { activePage: 0, cursorStart: 0, fakePossibleCount: 50, querySize: 10 }, + timerange: { interval: '12h', from: '2020-09-03T09:15:21.415Z', to: '2020-09-04T09:15:21.415Z' }, + sort: { direction: Direction.desc, field: HostsFields.lastSeen }, +}; + +export const mockSearchStrategyResponse: IEsSearchResponse = { + isPartial: false, + isRunning: false, + rawResponse: { + took: 169, + timed_out: false, + _shards: { total: 21, successful: 21, skipped: 0, failed: 0 }, + hits: { total: -1, max_score: 0, hits: [] }, + aggregations: { + host_data: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'bastion00.siem.estc.dev', + doc_count: 774875, + lastSeen: { value: 1599210921410, value_as_string: '2020-09-04T09:15:21.410Z' }, + os: { + hits: { + total: 774875, + max_score: 0, + hits: [ + { + _index: 'filebeat-8.0.0-2020.09.02-000001', + _id: 'f6NmWHQBA6bGZw2uJepK', + _score: null, + _source: {}, + sort: [1599210921410], + }, + ], + }, + }, + }, + { + key: 'es02.siem.estc.dev', + doc_count: 10496, + lastSeen: { value: 1599210907990, value_as_string: '2020-09-04T09:15:07.990Z' }, + os: { + hits: { + total: 10496, + max_score: 0, + hits: [ + { + _index: 'filebeat-8.0.0-2020.09.02-000001', + _id: '4_lmWHQBc39KFIJbFdYv', + _score: null, + _source: {}, + sort: [1599210907990], + }, + ], + }, + }, + }, + { + key: 'es00.siem.estc.dev', + doc_count: 19722, + lastSeen: { value: 1599210906783, value_as_string: '2020-09-04T09:15:06.783Z' }, + os: { + hits: { + total: 19722, + max_score: 0, + hits: [ + { + _index: 'filebeat-8.0.0-2020.09.02-000001', + _id: 'z_lmWHQBc39KFIJbAdZP', + _score: null, + _source: {}, + sort: [1599210906783], + }, + ], + }, + }, + }, + { + key: 'es01.siem.estc.dev', + doc_count: 16770, + lastSeen: { value: 1599210900781, value_as_string: '2020-09-04T09:15:00.781Z' }, + os: { + hits: { + total: 16770, + max_score: 0, + hits: [ + { + _index: 'filebeat-8.0.0-2020.09.02-000001', + _id: 'uPllWHQBc39KFIJb6tbR', + _score: null, + _source: {}, + sort: [1599210900781], + }, + ], + }, + }, + }, + { + key: 'siem-windows', + doc_count: 1941, + lastSeen: { value: 1599210880354, value_as_string: '2020-09-04T09:14:40.354Z' }, + os: { + hits: { + total: 1941, + max_score: 0, + hits: [ + { + _index: 'winlogbeat-8.0.0-2020.09.02-000001', + _id: '56NlWHQBA6bGZw2uiOfb', + _score: null, + _source: { + host: { + os: { + build: '17763.1397', + kernel: '10.0.17763.1397 (WinBuild.160101.0800)', + name: 'Windows Server 2019 Datacenter', + family: 'windows', + version: '10.0', + platform: 'windows', + }, + }, + }, + sort: [1599210880354], + }, + ], + }, + }, + }, + { + key: 'filebeat-cloud', + doc_count: 50, + lastSeen: { value: 1599207421000, value_as_string: '2020-09-04T08:17:01.000Z' }, + os: { + hits: { + total: 50, + max_score: 0, + hits: [ + { + _index: 'filebeat-8.0.0-2020.09.02-000001', + _id: 'FKMwWHQBA6bGZw2uw5Z3', + _score: null, + _source: {}, + sort: [1599207421000], + }, + ], + }, + }, + }, + { + key: 'kibana00.siem.estc.dev', + doc_count: 50, + lastSeen: { value: 1599207421000, value_as_string: '2020-09-04T08:17:01.000Z' }, + os: { + hits: { + total: 50, + max_score: 0, + hits: [ + { + _index: 'filebeat-8.0.0-2020.09.02-000001', + _id: 'MKMwWHQBA6bGZw2u0ZZw', + _score: null, + _source: {}, + sort: [1599207421000], + }, + ], + }, + }, + }, + { + key: 'DESKTOP-QBBSCUT', + doc_count: 128973, + lastSeen: { value: 1599150487957, value_as_string: '2020-09-03T16:28:07.957Z' }, + os: { + hits: { + total: 128973, + max_score: 0, + hits: [ + { + _index: '.ds-logs-elastic.agent-default-000001', + _id: 'tvTLVHQBc39KFIJb_ykQ', + _score: null, + _source: { + host: { + os: { + build: '18362.1016', + kernel: '10.0.18362.1016 (WinBuild.160101.0800)', + name: 'Windows 10 Pro', + family: 'windows', + version: '10.0', + platform: 'windows', + }, + }, + }, + sort: [1599150487957], + }, + ], + }, + }, + }, + { + key: 'mainqa-atlcolo-10-0-7-195.eng.endgames.local', + doc_count: 21213, + lastSeen: { value: 1599150457515, value_as_string: '2020-09-03T16:27:37.515Z' }, + os: { + hits: { + total: 21213, + max_score: 0, + hits: [ + { + _index: '.ds-logs-endpoint.events.network-default-000001', + _id: 'efTLVHQBc39KFIJbiCgD', + _score: null, + _source: { + host: { + os: { + Ext: { variant: 'macOS' }, + kernel: + 'Darwin Kernel Version 18.2.0: Fri Oct 5 19:40:55 PDT 2018; root:xnu-4903.221.2~1/RELEASE_X86_64', + name: 'macOS', + family: 'macos', + version: '10.14.1', + platform: 'macos', + full: 'macOS 10.14.1', + }, + }, + }, + sort: [1599150457515], + }, + ], + }, + }, + }, + ], + }, + host_count: { value: 9 }, + }, + }, + total: 21, + loaded: 21, +}; + +export const formattedSearchStrategyResponse = { + isPartial: false, + isRunning: false, + rawResponse: { + took: 169, + timed_out: false, + _shards: { total: 21, successful: 21, skipped: 0, failed: 0 }, + hits: { total: -1, max_score: 0, hits: [] }, + aggregations: { + host_data: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'bastion00.siem.estc.dev', + doc_count: 774875, + lastSeen: { value: 1599210921410, value_as_string: '2020-09-04T09:15:21.410Z' }, + os: { + hits: { + total: 774875, + max_score: 0, + hits: [ + { + _index: 'filebeat-8.0.0-2020.09.02-000001', + _id: 'f6NmWHQBA6bGZw2uJepK', + _score: null, + _source: {}, + sort: [1599210921410], + }, + ], + }, + }, + }, + { + key: 'es02.siem.estc.dev', + doc_count: 10496, + lastSeen: { value: 1599210907990, value_as_string: '2020-09-04T09:15:07.990Z' }, + os: { + hits: { + total: 10496, + max_score: 0, + hits: [ + { + _index: 'filebeat-8.0.0-2020.09.02-000001', + _id: '4_lmWHQBc39KFIJbFdYv', + _score: null, + _source: {}, + sort: [1599210907990], + }, + ], + }, + }, + }, + { + key: 'es00.siem.estc.dev', + doc_count: 19722, + lastSeen: { value: 1599210906783, value_as_string: '2020-09-04T09:15:06.783Z' }, + os: { + hits: { + total: 19722, + max_score: 0, + hits: [ + { + _index: 'filebeat-8.0.0-2020.09.02-000001', + _id: 'z_lmWHQBc39KFIJbAdZP', + _score: null, + _source: {}, + sort: [1599210906783], + }, + ], + }, + }, + }, + { + key: 'es01.siem.estc.dev', + doc_count: 16770, + lastSeen: { value: 1599210900781, value_as_string: '2020-09-04T09:15:00.781Z' }, + os: { + hits: { + total: 16770, + max_score: 0, + hits: [ + { + _index: 'filebeat-8.0.0-2020.09.02-000001', + _id: 'uPllWHQBc39KFIJb6tbR', + _score: null, + _source: {}, + sort: [1599210900781], + }, + ], + }, + }, + }, + { + key: 'siem-windows', + doc_count: 1941, + lastSeen: { value: 1599210880354, value_as_string: '2020-09-04T09:14:40.354Z' }, + os: { + hits: { + total: 1941, + max_score: 0, + hits: [ + { + _index: 'winlogbeat-8.0.0-2020.09.02-000001', + _id: '56NlWHQBA6bGZw2uiOfb', + _score: null, + _source: { + host: { + os: { + build: '17763.1397', + kernel: '10.0.17763.1397 (WinBuild.160101.0800)', + name: 'Windows Server 2019 Datacenter', + family: 'windows', + version: '10.0', + platform: 'windows', + }, + }, + }, + sort: [1599210880354], + }, + ], + }, + }, + }, + { + key: 'filebeat-cloud', + doc_count: 50, + lastSeen: { value: 1599207421000, value_as_string: '2020-09-04T08:17:01.000Z' }, + os: { + hits: { + total: 50, + max_score: 0, + hits: [ + { + _index: 'filebeat-8.0.0-2020.09.02-000001', + _id: 'FKMwWHQBA6bGZw2uw5Z3', + _score: null, + _source: {}, + sort: [1599207421000], + }, + ], + }, + }, + }, + { + key: 'kibana00.siem.estc.dev', + doc_count: 50, + lastSeen: { value: 1599207421000, value_as_string: '2020-09-04T08:17:01.000Z' }, + os: { + hits: { + total: 50, + max_score: 0, + hits: [ + { + _index: 'filebeat-8.0.0-2020.09.02-000001', + _id: 'MKMwWHQBA6bGZw2u0ZZw', + _score: null, + _source: {}, + sort: [1599207421000], + }, + ], + }, + }, + }, + { + key: 'DESKTOP-QBBSCUT', + doc_count: 128973, + lastSeen: { value: 1599150487957, value_as_string: '2020-09-03T16:28:07.957Z' }, + os: { + hits: { + total: 128973, + max_score: 0, + hits: [ + { + _index: '.ds-logs-elastic.agent-default-000001', + _id: 'tvTLVHQBc39KFIJb_ykQ', + _score: null, + _source: { + host: { + os: { + build: '18362.1016', + kernel: '10.0.18362.1016 (WinBuild.160101.0800)', + name: 'Windows 10 Pro', + family: 'windows', + version: '10.0', + platform: 'windows', + }, + }, + }, + sort: [1599150487957], + }, + ], + }, + }, + }, + { + key: 'mainqa-atlcolo-10-0-7-195.eng.endgames.local', + doc_count: 21213, + lastSeen: { value: 1599150457515, value_as_string: '2020-09-03T16:27:37.515Z' }, + os: { + hits: { + total: 21213, + max_score: 0, + hits: [ + { + _index: '.ds-logs-endpoint.events.network-default-000001', + _id: 'efTLVHQBc39KFIJbiCgD', + _score: null, + _source: { + host: { + os: { + Ext: { variant: 'macOS' }, + kernel: + 'Darwin Kernel Version 18.2.0: Fri Oct 5 19:40:55 PDT 2018; root:xnu-4903.221.2~1/RELEASE_X86_64', + name: 'macOS', + family: 'macos', + version: '10.14.1', + platform: 'macos', + full: 'macOS 10.14.1', + }, + }, + }, + sort: [1599150457515], + }, + ], + }, + }, + }, + ], + }, + host_count: { value: 9 }, + }, + }, + total: 21, + loaded: 21, + inspect: { + dsl: [ + '{\n "allowNoIndices": true,\n "index": [\n "apm-*-transaction*",\n "auditbeat-*",\n "endgame-*",\n "filebeat-*",\n "logs-*",\n "packetbeat-*",\n "winlogbeat-*"\n ],\n "ignoreUnavailable": true,\n "body": {\n "aggregations": {\n "host_count": {\n "cardinality": {\n "field": "host.name"\n }\n },\n "host_data": {\n "terms": {\n "size": 10,\n "field": "host.name",\n "order": {\n "lastSeen": "desc"\n }\n },\n "aggs": {\n "lastSeen": {\n "max": {\n "field": "@timestamp"\n }\n },\n "os": {\n "top_hits": {\n "size": 1,\n "sort": [\n {\n "@timestamp": {\n "order": "desc"\n }\n }\n ],\n "_source": {\n "includes": [\n "host.os.*"\n ]\n }\n }\n }\n }\n }\n },\n "query": {\n "bool": {\n "filter": [\n {\n "bool": {\n "must": [],\n "filter": [\n {\n "match_all": {}\n }\n ],\n "should": [],\n "must_not": []\n }\n },\n {\n "range": {\n "@timestamp": {\n "gte": "2020-09-03T09:15:21.415Z",\n "lte": "2020-09-04T09:15:21.415Z",\n "format": "strict_date_optional_time"\n }\n }\n }\n ]\n }\n },\n "size": 0,\n "track_total_hits": false\n }\n}', + ], + }, + edges: [ + { + node: { + _id: 'bastion00.siem.estc.dev', + lastSeen: ['2020-09-04T09:15:21.410Z'], + host: { name: ['bastion00.siem.estc.dev'] }, + }, + cursor: { value: 'bastion00.siem.estc.dev', tiebreaker: null }, + }, + { + node: { + _id: 'es02.siem.estc.dev', + lastSeen: ['2020-09-04T09:15:07.990Z'], + host: { name: ['es02.siem.estc.dev'] }, + }, + cursor: { value: 'es02.siem.estc.dev', tiebreaker: null }, + }, + { + node: { + _id: 'es00.siem.estc.dev', + lastSeen: ['2020-09-04T09:15:06.783Z'], + host: { name: ['es00.siem.estc.dev'] }, + }, + cursor: { value: 'es00.siem.estc.dev', tiebreaker: null }, + }, + { + node: { + _id: 'es01.siem.estc.dev', + lastSeen: ['2020-09-04T09:15:00.781Z'], + host: { name: ['es01.siem.estc.dev'] }, + }, + cursor: { value: 'es01.siem.estc.dev', tiebreaker: null }, + }, + { + node: { + _id: 'siem-windows', + lastSeen: ['2020-09-04T09:14:40.354Z'], + host: { + name: ['siem-windows'], + os: { name: ['Windows Server 2019 Datacenter'], version: ['10.0'] }, + }, + }, + cursor: { value: 'siem-windows', tiebreaker: null }, + }, + { + node: { + _id: 'filebeat-cloud', + lastSeen: ['2020-09-04T08:17:01.000Z'], + host: { name: ['filebeat-cloud'] }, + }, + cursor: { value: 'filebeat-cloud', tiebreaker: null }, + }, + { + node: { + _id: 'kibana00.siem.estc.dev', + lastSeen: ['2020-09-04T08:17:01.000Z'], + host: { name: ['kibana00.siem.estc.dev'] }, + }, + cursor: { value: 'kibana00.siem.estc.dev', tiebreaker: null }, + }, + { + node: { + _id: 'DESKTOP-QBBSCUT', + lastSeen: ['2020-09-03T16:28:07.957Z'], + host: { name: ['DESKTOP-QBBSCUT'], os: { name: ['Windows 10 Pro'], version: ['10.0'] } }, + }, + cursor: { value: 'DESKTOP-QBBSCUT', tiebreaker: null }, + }, + { + node: { + _id: 'mainqa-atlcolo-10-0-7-195.eng.endgames.local', + lastSeen: ['2020-09-03T16:27:37.515Z'], + host: { + name: ['mainqa-atlcolo-10-0-7-195.eng.endgames.local'], + os: { name: ['macOS'], version: ['10.14.1'] }, + }, + }, + cursor: { value: 'mainqa-atlcolo-10-0-7-195.eng.endgames.local', tiebreaker: null }, + }, + ], + totalCount: 9, + pageInfo: { activePage: 0, fakeTotalCount: 9, showMorePagesIndicator: false }, +}; + +export const mockBuckets: HostAggEsItem = { + key: 'zeek-london', + os: { + hits: { + total: { + value: 242338, + relation: 'eq', + }, + max_score: null, + hits: [ + { + _index: 'auditbeat-8.0.0-2019.09.06-000022', + _id: 'dl0T_m0BHe9nqdOiF2A8', + _score: null, + _source: { + host: { + os: { + kernel: ['5.0.0-1013-gcp'], + name: ['Ubuntu'], + family: ['debian'], + version: ['18.04.2 LTS (Bionic Beaver)'], + platform: ['ubuntu'], + }, + }, + }, + sort: [1571925726017], + }, + ], + }, + }, +}; + +export const expectedDsl = { + allowNoIndices: true, + body: { + aggregations: { + host_count: { cardinality: { field: 'host.name' } }, + host_data: { + aggs: { + lastSeen: { max: { field: '@timestamp' } }, + os: { + top_hits: { + _source: { includes: ['host.os.*'] }, + size: 1, + sort: [{ '@timestamp': { order: 'desc' } }], + }, + }, + }, + terms: { field: 'host.name', order: { lastSeen: 'desc' }, size: 10 }, + }, + }, + query: { + bool: { + filter: [ + { bool: { filter: [{ match_all: {} }], must: [], must_not: [], should: [] } }, + { + range: { + '@timestamp': { + format: 'strict_date_optional_time', + gte: '2020-09-03T09:15:21.415Z', + lte: '2020-09-04T09:15:21.415Z', + }, + }, + }, + ], + }, + }, + size: 0, + track_total_hits: false, + }, + ignoreUnavailable: true, + index: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'winlogbeat-*', + ], +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/helpers.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/helpers.test.ts new file mode 100644 index 0000000000000..78f214c69f14f --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/helpers.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { HostsEdges } from '../../../../../../common/search_strategy/security_solution'; + +import { formatHostEdgesData } from './helpers'; +import { mockBuckets } from './__mocks__'; + +describe('#formatHostsData', () => { + test('it formats a host with a source of name correctly', () => { + const mockFields: readonly string[] = ['host.name']; + const data = formatHostEdgesData(mockFields, mockBuckets); + const expected: HostsEdges = { + cursor: { tiebreaker: null, value: 'zeek-london' }, + node: { host: { name: ['zeek-london'] }, _id: 'zeek-london' }, + }; + + expect(data).toEqual(expected); + }); + + test('it formats a host with a source of os correctly', () => { + const mockFields: readonly string[] = ['host.os.name']; + const data = formatHostEdgesData(mockFields, mockBuckets); + const expected: HostsEdges = { + cursor: { tiebreaker: null, value: 'zeek-london' }, + node: { host: { os: { name: ['Ubuntu'] } }, _id: 'zeek-london' }, + }; + + expect(data).toEqual(expected); + }); + + test('it formats a host with a source of version correctly', () => { + const mockFields: readonly string[] = ['host.os.version']; + const data = formatHostEdgesData(mockFields, mockBuckets); + const expected: HostsEdges = { + cursor: { tiebreaker: null, value: 'zeek-london' }, + node: { host: { os: { version: ['18.04.2 LTS (Bionic Beaver)'] } }, _id: 'zeek-london' }, + }; + + expect(data).toEqual(expected); + }); + + test('it formats a host with a source of id correctly', () => { + const mockFields: readonly string[] = ['host.name']; + const data = formatHostEdgesData(mockFields, mockBuckets); + const expected: HostsEdges = { + cursor: { tiebreaker: null, value: 'zeek-london' }, + node: { _id: 'zeek-london', host: { name: ['zeek-london'] } }, + }; + + expect(data).toEqual(expected); + }); + + test('it formats a host with a source of name, lastBeat, os, and version correctly', () => { + const mockFields: readonly string[] = ['host.name', 'host.os.name', 'host.os.version']; + const data = formatHostEdgesData(mockFields, mockBuckets); + const expected: HostsEdges = { + cursor: { tiebreaker: null, value: 'zeek-london' }, + node: { + _id: 'zeek-london', + host: { + name: ['zeek-london'], + os: { name: ['Ubuntu'], version: ['18.04.2 LTS (Bionic Beaver)'] }, + }, + }, + }; + + expect(data).toEqual(expected); + }); + + test('it formats a host without any data if mockFields are empty', () => { + const mockFields: readonly string[] = []; + const data = formatHostEdgesData(mockFields, mockBuckets); + const expected: HostsEdges = { + cursor: { + tiebreaker: null, + value: '', + }, + node: {}, + }; + + expect(data).toEqual(expected); + }); +}); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/helpers.ts index 3550824028478..b06c36fd24e1a 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/helpers.ts @@ -9,22 +9,29 @@ import { hostFieldsMap } from '../../../../../../common/ecs/ecs_fields'; import { HostsEdges } from '../../../../../../common/search_strategy/security_solution/hosts'; import { HostAggEsItem, HostBuckets, HostValue } from '../../../../../lib/hosts/types'; +import { toArray } from '../../../../helpers/to_array'; -const HOSTS_FIELDS = ['_id', 'lastSeen', 'host.id', 'host.name', 'host.os.name', 'host.os.version']; +export const HOSTS_FIELDS: readonly string[] = [ + '_id', + 'lastSeen', + 'host.id', + 'host.name', + 'host.os.name', + 'host.os.version', +]; -export const formatHostEdgesData = (bucket: HostAggEsItem): HostsEdges => - HOSTS_FIELDS.reduce( +export const formatHostEdgesData = ( + fields: readonly string[] = HOSTS_FIELDS, + bucket: HostAggEsItem +): HostsEdges => + fields.reduce( (flattenedFields, fieldName) => { const hostId = get('key', bucket); flattenedFields.node._id = hostId || null; flattenedFields.cursor.value = hostId || ''; const fieldValue = getHostFieldValue(fieldName, bucket); if (fieldValue != null) { - return set( - `node.${fieldName}`, - Array.isArray(fieldValue) ? fieldValue : [fieldValue], - flattenedFields - ); + return set(`node.${fieldName}`, toArray(fieldValue), flattenedFields); } return flattenedFields; }, diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/index.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/index.test.ts new file mode 100644 index 0000000000000..b57112b02fffe --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/index.test.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../../../common/constants'; + +import { HostsRequestOptions } from '../../../../../../common/search_strategy/security_solution'; +import * as buildQuery from './query.all_hosts.dsl'; +import { allHosts } from '.'; +import { + mockOptions, + mockSearchStrategyResponse, + formattedSearchStrategyResponse, +} from './__mocks__'; + +describe('allHosts search strategy', () => { + const buildAllHostsQuery = jest.spyOn(buildQuery, 'buildHostsQuery'); + + afterEach(() => { + buildAllHostsQuery.mockClear(); + }); + + describe('buildDsl', () => { + test('should build dsl query', () => { + allHosts.buildDsl(mockOptions); + expect(buildAllHostsQuery).toHaveBeenCalledWith(mockOptions); + }); + + test('should throw error if query size is greater equal than DEFAULT_MAX_TABLE_QUERY_SIZE ', () => { + const overSizeOptions = { + ...mockOptions, + pagination: { + ...mockOptions.pagination, + querySize: DEFAULT_MAX_TABLE_QUERY_SIZE, + }, + } as HostsRequestOptions; + + expect(() => { + allHosts.buildDsl(overSizeOptions); + }).toThrowError(`No query size above ${DEFAULT_MAX_TABLE_QUERY_SIZE}`); + }); + }); + + describe('parse', () => { + test('should parse data correctly', async () => { + const result = await allHosts.parse(mockOptions, mockSearchStrategyResponse); + expect(result).toMatchObject(formattedSearchStrategyResponse); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/index.ts index d4c2214b98645..aacfc227a36ad 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/index.ts @@ -18,7 +18,7 @@ import { import { inspectStringifyObject } from '../../../../../utils/build_query'; import { SecuritySolutionFactory } from '../../types'; import { buildHostsQuery } from './query.all_hosts.dsl'; -import { formatHostEdgesData } from './helpers'; +import { formatHostEdgesData, HOSTS_FIELDS } from './helpers'; export const allHosts: SecuritySolutionFactory = { buildDsl: (options: HostsRequestOptions) => { @@ -38,12 +38,11 @@ export const allHosts: SecuritySolutionFactory = { 'aggregations.host_data.buckets', response.rawResponse ); - const hostsEdges = buckets.map((bucket) => formatHostEdgesData(bucket)); + const hostsEdges = buckets.map((bucket) => formatHostEdgesData(HOSTS_FIELDS, bucket)); const fakeTotalCount = fakePossibleCount <= totalCount ? fakePossibleCount : totalCount; const edges = hostsEdges.splice(cursorStart, querySize - cursorStart); const inspect = { dsl: [inspectStringifyObject(buildHostsQuery(options))], - response: [inspectStringifyObject(response)], }; const showMorePagesIndicator = totalCount > fakeTotalCount; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/query.all_hosts.dsl.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/query.all_hosts.dsl.test.ts new file mode 100644 index 0000000000000..f5999d15e8950 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/all/query.all_hosts.dsl.test.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; + * you may not use this file except in compliance with the Elastic License. + */ +import { buildHostsQuery } from './query.all_hosts.dsl'; +import { mockOptions, expectedDsl } from './__mocks__/'; + +describe('buildHostsQuery', () => { + test('build query from options correctly', () => { + expect(buildHostsQuery(mockOptions)).toEqual(expectedDsl); + }); +}); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/__mocks__/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/__mocks__/index.ts new file mode 100644 index 0000000000000..65343dc721fd7 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/__mocks__/index.ts @@ -0,0 +1,2370 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { IEsSearchResponse } from '../../../../../../../../../../src/plugins/data/common'; +import { + AuthenticationHit, + Direction, + HostsQueries, + HostAuthenticationsRequestOptions, +} from '../../../../../../../common/search_strategy'; + +export const mockOptions: HostAuthenticationsRequestOptions = { + defaultIndex: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'winlogbeat-*', + ], + docValueFields: [ + { + field: '@timestamp', + format: 'date_time', + }, + { + field: 'event.created', + format: 'date_time', + }, + { + field: 'event.end', + format: 'date_time', + }, + { + field: 'event.ingested', + format: 'date_time', + }, + { + field: 'event.start', + format: 'date_time', + }, + { + field: 'file.accessed', + format: 'date_time', + }, + { + field: 'file.created', + format: 'date_time', + }, + { + field: 'file.ctime', + format: 'date_time', + }, + { + field: 'file.mtime', + format: 'date_time', + }, + { + field: 'package.installed', + format: 'date_time', + }, + { + field: 'process.parent.start', + format: 'date_time', + }, + { + field: 'process.start', + format: 'date_time', + }, + { + field: 'system.audit.host.boottime', + format: 'date_time', + }, + { + field: 'system.audit.package.installtime', + format: 'date_time', + }, + { + field: 'system.audit.user.password.last_changed', + format: 'date_time', + }, + { + field: 'tls.client.not_after', + format: 'date_time', + }, + { + field: 'tls.client.not_before', + format: 'date_time', + }, + { + field: 'tls.server.not_after', + format: 'date_time', + }, + { + field: 'tls.server.not_before', + format: 'date_time', + }, + { + field: 'aws.cloudtrail.user_identity.session_context.creation_date', + format: 'date_time', + }, + { + field: 'azure.auditlogs.properties.activity_datetime', + format: 'date_time', + }, + { + field: 'azure.enqueued_time', + format: 'date_time', + }, + { + field: 'azure.signinlogs.properties.created_at', + format: 'date_time', + }, + { + field: 'cef.extensions.agentReceiptTime', + format: 'date_time', + }, + { + field: 'cef.extensions.deviceCustomDate1', + format: 'date_time', + }, + { + field: 'cef.extensions.deviceCustomDate2', + format: 'date_time', + }, + { + field: 'cef.extensions.deviceReceiptTime', + format: 'date_time', + }, + { + field: 'cef.extensions.endTime', + format: 'date_time', + }, + { + field: 'cef.extensions.fileCreateTime', + format: 'date_time', + }, + { + field: 'cef.extensions.fileModificationTime', + format: 'date_time', + }, + { + field: 'cef.extensions.flexDate1', + format: 'date_time', + }, + { + field: 'cef.extensions.managerReceiptTime', + format: 'date_time', + }, + { + field: 'cef.extensions.oldFileCreateTime', + format: 'date_time', + }, + { + field: 'cef.extensions.oldFileModificationTime', + format: 'date_time', + }, + { + field: 'cef.extensions.startTime', + format: 'date_time', + }, + { + field: 'checkpoint.subs_exp', + format: 'date_time', + }, + { + field: 'crowdstrike.event.EndTimestamp', + format: 'date_time', + }, + { + field: 'crowdstrike.event.IncidentEndTime', + format: 'date_time', + }, + { + field: 'crowdstrike.event.IncidentStartTime', + format: 'date_time', + }, + { + field: 'crowdstrike.event.ProcessEndTime', + format: 'date_time', + }, + { + field: 'crowdstrike.event.ProcessStartTime', + format: 'date_time', + }, + { + field: 'crowdstrike.event.StartTimestamp', + format: 'date_time', + }, + { + field: 'crowdstrike.event.Timestamp', + format: 'date_time', + }, + { + field: 'crowdstrike.event.UTCTimestamp', + format: 'date_time', + }, + { + field: 'crowdstrike.metadata.eventCreationTime', + format: 'date_time', + }, + { + field: 'gsuite.admin.email.log_search_filter.end_date', + format: 'date_time', + }, + { + field: 'gsuite.admin.email.log_search_filter.start_date', + format: 'date_time', + }, + { + field: 'gsuite.admin.user.birthdate', + format: 'date_time', + }, + { + field: 'kafka.block_timestamp', + format: 'date_time', + }, + { + field: 'microsoft.defender_atp.lastUpdateTime', + format: 'date_time', + }, + { + field: 'microsoft.defender_atp.resolvedTime', + format: 'date_time', + }, + { + field: 'misp.campaign.first_seen', + format: 'date_time', + }, + { + field: 'misp.campaign.last_seen', + format: 'date_time', + }, + { + field: 'misp.intrusion_set.first_seen', + format: 'date_time', + }, + { + field: 'misp.intrusion_set.last_seen', + format: 'date_time', + }, + { + field: 'misp.observed_data.first_observed', + format: 'date_time', + }, + { + field: 'misp.observed_data.last_observed', + format: 'date_time', + }, + { + field: 'misp.report.published', + format: 'date_time', + }, + { + field: 'misp.threat_indicator.valid_from', + format: 'date_time', + }, + { + field: 'misp.threat_indicator.valid_until', + format: 'date_time', + }, + { + field: 'netflow.collection_time_milliseconds', + format: 'date_time', + }, + { + field: 'netflow.exporter.timestamp', + format: 'date_time', + }, + { + field: 'netflow.flow_end_microseconds', + format: 'date_time', + }, + { + field: 'netflow.flow_end_milliseconds', + format: 'date_time', + }, + { + field: 'netflow.flow_end_nanoseconds', + format: 'date_time', + }, + { + field: 'netflow.flow_end_seconds', + format: 'date_time', + }, + { + field: 'netflow.flow_start_microseconds', + format: 'date_time', + }, + { + field: 'netflow.flow_start_milliseconds', + format: 'date_time', + }, + { + field: 'netflow.flow_start_nanoseconds', + format: 'date_time', + }, + { + field: 'netflow.flow_start_seconds', + format: 'date_time', + }, + { + field: 'netflow.max_export_seconds', + format: 'date_time', + }, + { + field: 'netflow.max_flow_end_microseconds', + format: 'date_time', + }, + { + field: 'netflow.max_flow_end_milliseconds', + format: 'date_time', + }, + { + field: 'netflow.max_flow_end_nanoseconds', + format: 'date_time', + }, + { + field: 'netflow.max_flow_end_seconds', + format: 'date_time', + }, + { + field: 'netflow.min_export_seconds', + format: 'date_time', + }, + { + field: 'netflow.min_flow_start_microseconds', + format: 'date_time', + }, + { + field: 'netflow.min_flow_start_milliseconds', + format: 'date_time', + }, + { + field: 'netflow.min_flow_start_nanoseconds', + format: 'date_time', + }, + { + field: 'netflow.min_flow_start_seconds', + format: 'date_time', + }, + { + field: 'netflow.monitoring_interval_end_milli_seconds', + format: 'date_time', + }, + { + field: 'netflow.monitoring_interval_start_milli_seconds', + format: 'date_time', + }, + { + field: 'netflow.observation_time_microseconds', + format: 'date_time', + }, + { + field: 'netflow.observation_time_milliseconds', + format: 'date_time', + }, + { + field: 'netflow.observation_time_nanoseconds', + format: 'date_time', + }, + { + field: 'netflow.observation_time_seconds', + format: 'date_time', + }, + { + field: 'netflow.system_init_time_milliseconds', + format: 'date_time', + }, + { + field: 'rsa.internal.lc_ctime', + format: 'date_time', + }, + { + field: 'rsa.internal.time', + format: 'date_time', + }, + { + field: 'rsa.time.effective_time', + format: 'date_time', + }, + { + field: 'rsa.time.endtime', + format: 'date_time', + }, + { + field: 'rsa.time.event_queue_time', + format: 'date_time', + }, + { + field: 'rsa.time.event_time', + format: 'date_time', + }, + { + field: 'rsa.time.expire_time', + format: 'date_time', + }, + { + field: 'rsa.time.recorded_time', + format: 'date_time', + }, + { + field: 'rsa.time.stamp', + format: 'date_time', + }, + { + field: 'rsa.time.starttime', + format: 'date_time', + }, + { + field: 'sophos.xg.date', + format: 'date_time', + }, + { + field: 'sophos.xg.eventtime', + format: 'date_time', + }, + { + field: 'sophos.xg.start_time', + format: 'date_time', + }, + ], + factoryQueryType: HostsQueries.authentications, + filterQuery: '{"bool":{"must":[],"filter":[{"match_all":{}}],"should":[],"must_not":[]}}', + pagination: { + activePage: 0, + cursorStart: 0, + fakePossibleCount: 50, + querySize: 10, + }, + timerange: { + interval: '12h', + from: '2020-09-02T15:17:13.678Z', + to: '2020-09-03T15:17:13.678Z', + }, + sort: { + direction: Direction.desc, + field: 'success', + }, + params: {}, +}; + +export const mockSearchStrategyResponse: IEsSearchResponse = { + isPartial: false, + isRunning: false, + rawResponse: { + took: 14, + timed_out: false, + _shards: { total: 21, successful: 21, skipped: 0, failed: 0 }, + hits: { total: -1, max_score: 0, hits: [] }, + aggregations: { + group_by_users: { + doc_count_error_upper_bound: -1, + sum_other_doc_count: 408, + buckets: [ + { + key: 'SYSTEM', + doc_count: 281, + failures: { + meta: {}, + doc_count: 0, + lastFailure: { hits: { total: 0, max_score: 0, hits: [] } }, + }, + successes: { + meta: {}, + doc_count: 4, + lastSuccess: { + hits: { + total: 4, + max_score: 0, + hits: [ + { + _index: 'winlogbeat-8.0.0-2020.09.02-000001', + _id: 'zqY7WXQBA6bGZw2uLeKI', + _score: null, + _source: { + process: { + name: 'services.exe', + pid: 564, + executable: 'C:\\Windows\\System32\\services.exe', + }, + agent: { + build_date: '2020-07-16 09:16:27 +0000 UTC ', + name: 'siem-windows', + commit: '4dcbde39492bdc3843034bba8db811c68cb44b97 ', + id: '05e1bff7-d7a8-416a-8554-aa10288fa07d', + type: 'winlogbeat', + ephemeral_id: '655abd6c-6c33-435d-a2eb-79b2a01e6d61', + version: '8.0.0', + user: { name: 'inside_winlogbeat_user' }, + }, + winlog: { + computer_name: 'siem-windows', + process: { pid: 576, thread: { id: 880 } }, + keywords: ['Audit Success'], + logon: { id: '0x3e7', type: 'Service' }, + channel: 'Security', + event_data: { + LogonGuid: '{00000000-0000-0000-0000-000000000000}', + TargetOutboundDomainName: '-', + VirtualAccount: '%%1843', + LogonType: '5', + IpPort: '-', + TransmittedServices: '-', + SubjectLogonId: '0x3e7', + LmPackageName: '-', + TargetOutboundUserName: '-', + KeyLength: '0', + TargetLogonId: '0x3e7', + RestrictedAdminMode: '-', + SubjectUserName: 'SIEM-WINDOWS$', + TargetLinkedLogonId: '0x0', + ElevatedToken: '%%1842', + SubjectDomainName: 'WORKGROUP', + IpAddress: '-', + ImpersonationLevel: '%%1833', + TargetUserName: 'SYSTEM', + LogonProcessName: 'Advapi ', + TargetDomainName: 'NT AUTHORITY', + SubjectUserSid: 'S-1-5-18', + TargetUserSid: 'S-1-5-18', + AuthenticationPackageName: 'Negotiate', + }, + opcode: 'Info', + version: 2, + record_id: 57818, + task: 'Logon', + event_id: 4624, + provider_guid: '{54849625-5478-4994-a5ba-3e3b0328c30d}', + activity_id: '{d2485217-6bac-0000-8fbb-3f7e2571d601}', + api: 'wineventlog', + provider_name: 'Microsoft-Windows-Security-Auditing', + }, + log: { level: 'information' }, + source: { domain: '-' }, + message: + 'An account was successfully logged on.\n\nSubject:\n\tSecurity ID:\t\tS-1-5-18\n\tAccount Name:\t\tSIEM-WINDOWS$\n\tAccount Domain:\t\tWORKGROUP\n\tLogon ID:\t\t0x3E7\n\nLogon Information:\n\tLogon Type:\t\t5\n\tRestricted Admin Mode:\t-\n\tVirtual Account:\t\tNo\n\tElevated Token:\t\tYes\n\nImpersonation Level:\t\tImpersonation\n\nNew Logon:\n\tSecurity ID:\t\tS-1-5-18\n\tAccount Name:\t\tSYSTEM\n\tAccount Domain:\t\tNT AUTHORITY\n\tLogon ID:\t\t0x3E7\n\tLinked Logon ID:\t\t0x0\n\tNetwork Account Name:\t-\n\tNetwork Account Domain:\t-\n\tLogon GUID:\t\t{00000000-0000-0000-0000-000000000000}\n\nProcess Information:\n\tProcess ID:\t\t0x234\n\tProcess Name:\t\tC:\\Windows\\System32\\services.exe\n\nNetwork Information:\n\tWorkstation Name:\t-\n\tSource Network Address:\t-\n\tSource Port:\t\t-\n\nDetailed Authentication Information:\n\tLogon Process:\t\tAdvapi \n\tAuthentication Package:\tNegotiate\n\tTransited Services:\t-\n\tPackage Name (NTLM only):\t-\n\tKey Length:\t\t0\n\nThis event is generated when a logon session is created. It is generated on the computer that was accessed.\n\nThe subject fields indicate the account on the local system which requested the logon. This is most commonly a service such as the Server service, or a local process such as Winlogon.exe or Services.exe.\n\nThe logon type field indicates the kind of logon that occurred. The most common types are 2 (interactive) and 3 (network).\n\nThe New Logon fields indicate the account for whom the new logon was created, i.e. the account that was logged on.\n\nThe network fields indicate where a remote logon request originated. Workstation name is not always available and may be left blank in some cases.\n\nThe impersonation level field indicates the extent to which a process in the logon session can impersonate.\n\nThe authentication information fields provide detailed information about this specific logon request.\n\t- Logon GUID is a unique identifier that can be used to correlate this event with a KDC event.\n\t- Transited services indicate which intermediate services have participated in this logon request.\n\t- Package name indicates which sub-protocol was used among the NTLM protocols.\n\t- Key length indicates the length of the generated session key. This will be 0 if no session key was requested.', + cloud: { + availability_zone: 'us-central1-c', + instance: { name: 'siem-windows', id: '9156726559029788564' }, + provider: 'gcp', + machine: { type: 'g1-small' }, + project: { id: 'elastic-siem' }, + }, + '@timestamp': '2020-09-04T13:08:02.532Z', + related: { user: ['SYSTEM', 'SIEM-WINDOWS$'] }, + ecs: { version: '1.5.0' }, + host: { + hostname: 'siem-windows', + os: { + build: '17763.1397', + kernel: '10.0.17763.1397 (WinBuild.160101.0800)', + name: 'Windows Server 2019 Datacenter', + family: 'windows', + version: '10.0', + platform: 'windows', + }, + ip: ['fe80::ecf5:decc:3ec3:767e', '10.200.0.15'], + name: 'siem-windows', + id: 'ce1d3c9b-a815-4643-9641-ada0f2c00609', + mac: ['42:01:0a:c8:00:0f'], + architecture: 'x86_64', + }, + event: { + code: 4624, + provider: 'Microsoft-Windows-Security-Auditing', + created: '2020-09-04T13:08:03.638Z', + kind: 'event', + module: 'security', + action: 'logged-in', + category: 'authentication', + type: 'start', + outcome: 'success', + }, + user: { domain: 'NT AUTHORITY', name: 'SYSTEM', id: 'S-1-5-18' }, + }, + sort: [1599224882532], + }, + ], + }, + }, + }, + }, + { + key: 'tsg', + doc_count: 1, + failures: { + doc_count: 0, + lastFailure: { hits: { total: 0, max_score: 0, hits: [] } }, + }, + successes: { + doc_count: 1, + lastSuccess: { + hits: { + total: 1, + max_score: 0, + hits: [ + { + _index: '.ds-logs-system.auth-default-000001', + _id: '9_sfWXQBc39KFIJbIsDh', + _score: null, + _source: { + agent: { + hostname: 'siem-kibana', + name: 'siem-kibana', + id: 'aa3d9dc7-fef1-4c2f-a68d-25785d624e35', + ephemeral_id: 'e503bd85-11c7-4bc9-ae7d-70be1d919fb7', + type: 'filebeat', + version: '7.9.1', + }, + process: { name: 'sshd', pid: 20764 }, + log: { file: { path: '/var/log/auth.log' }, offset: 552463 }, + source: { + geo: { + continent_name: 'Europe', + region_iso_code: 'DE-BE', + city_name: 'Berlin', + country_iso_code: 'DE', + region_name: 'Land Berlin', + location: { lon: 13.3512, lat: 52.5727 }, + }, + as: { number: 6805, organization: { name: 'Telefonica Germany' } }, + port: 57457, + ip: '77.183.42.188', + }, + cloud: { + availability_zone: 'us-east1-b', + instance: { name: 'siem-kibana', id: '5412578377715150143' }, + provider: 'gcp', + machine: { type: 'n1-standard-2' }, + project: { id: 'elastic-beats' }, + }, + input: { type: 'log' }, + '@timestamp': '2020-09-04T11:49:21.000Z', + system: { + auth: { + ssh: { + method: 'publickey', + signature: 'RSA SHA256:vv64JNLzKZWYA9vonnGWuW7zxWhyZrL/BFxyIGbISx8', + event: 'Accepted', + }, + }, + }, + ecs: { version: '1.5.0' }, + data_stream: { namespace: 'default', type: 'logs', dataset: 'system.auth' }, + host: { + hostname: 'siem-kibana', + os: { + kernel: '4.9.0-8-amd64', + codename: 'stretch', + name: 'Debian GNU/Linux', + family: 'debian', + version: '9 (stretch)', + platform: 'debian', + }, + containerized: false, + ip: ['10.142.0.7', 'fe80::4001:aff:fe8e:7'], + name: 'siem-kibana', + id: 'aa7ca589f1b8220002f2fc61c64cfbf1', + mac: ['42:01:0a:8e:00:07'], + architecture: 'x86_64', + }, + event: { + timezone: '+00:00', + action: 'ssh_login', + type: 'authentication_success', + category: 'authentication', + dataset: 'system.auth', + outcome: 'success', + }, + user: { name: 'tsg' }, + }, + sort: [1599220161000], + }, + ], + }, + }, + }, + }, + { + key: 'admin', + doc_count: 23, + failures: { + doc_count: 23, + lastFailure: { + hits: { + total: 23, + max_score: 0, + hits: [ + { + _index: '.ds-logs-system.auth-default-000001', + _id: 'ZfxZWXQBc39KFIJbLN5U', + _score: null, + _source: { + agent: { + hostname: 'siem-kibana', + name: 'siem-kibana', + id: 'aa3d9dc7-fef1-4c2f-a68d-25785d624e35', + ephemeral_id: 'e503bd85-11c7-4bc9-ae7d-70be1d919fb7', + type: 'filebeat', + version: '7.9.1', + }, + process: { name: 'sshd', pid: 22913 }, + log: { file: { path: '/var/log/auth.log' }, offset: 562910 }, + source: { + geo: { + continent_name: 'Asia', + region_iso_code: 'KR-28', + city_name: 'Incheon', + country_iso_code: 'KR', + region_name: 'Incheon', + location: { lon: 126.7288, lat: 37.4562 }, + }, + as: { number: 4766, organization: { name: 'Korea Telecom' } }, + ip: '59.15.3.197', + }, + cloud: { + availability_zone: 'us-east1-b', + instance: { name: 'siem-kibana', id: '5412578377715150143' }, + provider: 'gcp', + machine: { type: 'n1-standard-2' }, + project: { id: 'elastic-beats' }, + }, + input: { type: 'log' }, + '@timestamp': '2020-09-04T13:40:46.000Z', + system: { auth: { ssh: { event: 'Invalid' } } }, + ecs: { version: '1.5.0' }, + data_stream: { namespace: 'default', type: 'logs', dataset: 'system.auth' }, + host: { + hostname: 'siem-kibana', + os: { + kernel: '4.9.0-8-amd64', + codename: 'stretch', + name: 'Debian GNU/Linux', + family: 'debian', + version: '9 (stretch)', + platform: 'debian', + }, + containerized: false, + ip: ['10.142.0.7', 'fe80::4001:aff:fe8e:7'], + name: 'siem-kibana', + id: 'aa7ca589f1b8220002f2fc61c64cfbf1', + mac: ['42:01:0a:8e:00:07'], + architecture: 'x86_64', + }, + event: { + timezone: '+00:00', + action: 'ssh_login', + type: 'authentication_failure', + category: 'authentication', + dataset: 'system.auth', + outcome: 'failure', + }, + user: { name: 'admin' }, + }, + sort: [1599226846000], + }, + ], + }, + }, + }, + successes: { + doc_count: 0, + lastSuccess: { hits: { total: 0, max_score: 0, hits: [] } }, + }, + }, + { + key: 'user', + doc_count: 21, + failures: { + doc_count: 21, + lastFailure: { + hits: { + total: 21, + max_score: 0, + hits: [ + { + _index: 'filebeat-8.0.0-2020.09.02-000001', + _id: 'M_xLWXQBc39KFIJbY7Cb', + _score: null, + _source: { + agent: { + name: 'bastion00.siem.estc.dev', + id: 'f9a321c1-ec27-49fa-aacf-6a50ef6d836f', + type: 'filebeat', + ephemeral_id: '734ee3da-1a4f-4bc9-b400-e0cf0e5eeebc', + version: '8.0.0', + }, + process: { name: 'sshd', pid: 20671 }, + log: { file: { path: '/var/log/auth.log' }, offset: 1028103 }, + source: { + geo: { + continent_name: 'North America', + region_iso_code: 'US-NY', + city_name: 'New York', + country_iso_code: 'US', + region_name: 'New York', + location: { lon: -74, lat: 40.7157 }, + }, + ip: '64.227.88.245', + }, + fileset: { name: 'auth' }, + input: { type: 'log' }, + '@timestamp': '2020-09-04T13:25:43.000Z', + system: { auth: { ssh: { event: 'Invalid' } } }, + ecs: { version: '1.5.0' }, + related: { ip: ['64.227.88.245'], user: ['user'] }, + service: { type: 'system' }, + host: { hostname: 'bastion00', name: 'bastion00.siem.estc.dev' }, + event: { + ingested: '2020-09-04T13:25:47.034172Z', + timezone: '+00:00', + kind: 'event', + module: 'system', + action: 'ssh_login', + type: ['authentication_failure', 'info'], + category: ['authentication'], + dataset: 'system.auth', + outcome: 'failure', + }, + user: { name: 'user' }, + }, + sort: [1599225943000], + }, + ], + }, + }, + }, + successes: { + doc_count: 0, + lastSuccess: { hits: { total: 0, max_score: 0, hits: [] } }, + }, + }, + { + key: 'ubuntu', + doc_count: 18, + failures: { + doc_count: 18, + lastFailure: { + hits: { + total: 18, + max_score: 0, + hits: [ + { + _index: 'filebeat-8.0.0-2020.09.02-000001', + _id: 'nPxKWXQBc39KFIJb7q4w', + _score: null, + _source: { + agent: { + name: 'bastion00.siem.estc.dev', + id: 'f9a321c1-ec27-49fa-aacf-6a50ef6d836f', + ephemeral_id: '734ee3da-1a4f-4bc9-b400-e0cf0e5eeebc', + type: 'filebeat', + version: '8.0.0', + }, + process: { name: 'sshd', pid: 20665 }, + log: { file: { path: '/var/log/auth.log' }, offset: 1027372 }, + source: { + geo: { + continent_name: 'North America', + region_iso_code: 'US-NY', + city_name: 'New York', + country_iso_code: 'US', + region_name: 'New York', + location: { lon: -74, lat: 40.7157 }, + }, + ip: '64.227.88.245', + }, + fileset: { name: 'auth' }, + input: { type: 'log' }, + '@timestamp': '2020-09-04T13:25:07.000Z', + system: { auth: { ssh: { event: 'Invalid' } } }, + ecs: { version: '1.5.0' }, + related: { ip: ['64.227.88.245'], user: ['ubuntu'] }, + service: { type: 'system' }, + host: { hostname: 'bastion00', name: 'bastion00.siem.estc.dev' }, + event: { + ingested: '2020-09-04T13:25:16.974606Z', + timezone: '+00:00', + kind: 'event', + module: 'system', + action: 'ssh_login', + type: ['authentication_failure', 'info'], + category: ['authentication'], + dataset: 'system.auth', + outcome: 'failure', + }, + user: { name: 'ubuntu' }, + }, + sort: [1599225907000], + }, + ], + }, + }, + }, + successes: { + doc_count: 0, + lastSuccess: { hits: { total: 0, max_score: 0, hits: [] } }, + }, + }, + { + key: 'odoo', + doc_count: 17, + failures: { + doc_count: 17, + lastFailure: { + hits: { + total: 17, + max_score: 0, + hits: [ + { + _index: '.ds-logs-system.auth-default-000001', + _id: 'mPsfWXQBc39KFIJbI8HI', + _score: null, + _source: { + agent: { + hostname: 'siem-kibana', + name: 'siem-kibana', + id: 'aa3d9dc7-fef1-4c2f-a68d-25785d624e35', + type: 'filebeat', + ephemeral_id: 'e503bd85-11c7-4bc9-ae7d-70be1d919fb7', + version: '7.9.1', + }, + process: { name: 'sshd', pid: 21506 }, + log: { file: { path: '/var/log/auth.log' }, offset: 556761 }, + source: { + geo: { + continent_name: 'Asia', + region_iso_code: 'IN-DL', + city_name: 'New Delhi', + country_iso_code: 'IN', + region_name: 'National Capital Territory of Delhi', + location: { lon: 77.2245, lat: 28.6358 }, + }, + as: { number: 10029, organization: { name: 'SHYAM SPECTRA PVT LTD' } }, + ip: '180.151.228.166', + }, + cloud: { + availability_zone: 'us-east1-b', + instance: { name: 'siem-kibana', id: '5412578377715150143' }, + provider: 'gcp', + machine: { type: 'n1-standard-2' }, + project: { id: 'elastic-beats' }, + }, + input: { type: 'log' }, + '@timestamp': '2020-09-04T12:26:36.000Z', + system: { auth: { ssh: { event: 'Invalid' } } }, + ecs: { version: '1.5.0' }, + data_stream: { namespace: 'default', type: 'logs', dataset: 'system.auth' }, + host: { + hostname: 'siem-kibana', + os: { + kernel: '4.9.0-8-amd64', + codename: 'stretch', + name: 'Debian GNU/Linux', + family: 'debian', + version: '9 (stretch)', + platform: 'debian', + }, + containerized: false, + ip: ['10.142.0.7', 'fe80::4001:aff:fe8e:7'], + name: 'siem-kibana', + id: 'aa7ca589f1b8220002f2fc61c64cfbf1', + mac: ['42:01:0a:8e:00:07'], + architecture: 'x86_64', + }, + event: { + timezone: '+00:00', + action: 'ssh_login', + type: 'authentication_failure', + category: 'authentication', + dataset: 'system.auth', + outcome: 'failure', + }, + user: { name: 'odoo' }, + }, + sort: [1599222396000], + }, + ], + }, + }, + }, + successes: { + doc_count: 0, + lastSuccess: { hits: { total: 0, max_score: 0, hits: [] } }, + }, + }, + { + key: 'pi', + doc_count: 17, + failures: { + doc_count: 17, + lastFailure: { + hits: { + total: 17, + max_score: 0, + hits: [ + { + _index: 'filebeat-8.0.0-2020.09.02-000001', + _id: 'aaToWHQBA6bGZw2uR-St', + _score: null, + _source: { + agent: { + name: 'bastion00.siem.estc.dev', + id: 'f9a321c1-ec27-49fa-aacf-6a50ef6d836f', + type: 'filebeat', + ephemeral_id: '734ee3da-1a4f-4bc9-b400-e0cf0e5eeebc', + version: '8.0.0', + }, + process: { name: 'sshd', pid: 20475 }, + log: { file: { path: '/var/log/auth.log' }, offset: 1019218 }, + source: { + geo: { + continent_name: 'Europe', + region_iso_code: 'SE-AB', + city_name: 'Stockholm', + country_iso_code: 'SE', + region_name: 'Stockholm', + location: { lon: 17.7833, lat: 59.25 }, + }, + as: { number: 8473, organization: { name: 'Bahnhof AB' } }, + ip: '178.174.148.58', + }, + fileset: { name: 'auth' }, + input: { type: 'log' }, + '@timestamp': '2020-09-04T11:37:22.000Z', + system: { auth: { ssh: { event: 'Invalid' } } }, + ecs: { version: '1.5.0' }, + related: { ip: ['178.174.148.58'], user: ['pi'] }, + service: { type: 'system' }, + host: { hostname: 'bastion00', name: 'bastion00.siem.estc.dev' }, + event: { + ingested: '2020-09-04T11:37:31.797423Z', + timezone: '+00:00', + kind: 'event', + module: 'system', + action: 'ssh_login', + type: ['authentication_failure', 'info'], + category: ['authentication'], + dataset: 'system.auth', + outcome: 'failure', + }, + user: { name: 'pi' }, + }, + sort: [1599219442000], + }, + ], + }, + }, + }, + successes: { + doc_count: 0, + lastSuccess: { hits: { total: 0, max_score: 0, hits: [] } }, + }, + }, + { + key: 'demo', + doc_count: 14, + failures: { + doc_count: 14, + lastFailure: { + hits: { + total: 14, + max_score: 0, + hits: [ + { + _index: 'filebeat-8.0.0-2020.09.02-000001', + _id: 'VaP_V3QBA6bGZw2upUbg', + _score: null, + _source: { + agent: { + name: 'bastion00.siem.estc.dev', + id: 'f9a321c1-ec27-49fa-aacf-6a50ef6d836f', + type: 'filebeat', + ephemeral_id: '734ee3da-1a4f-4bc9-b400-e0cf0e5eeebc', + version: '8.0.0', + }, + process: { name: 'sshd', pid: 19849 }, + log: { file: { path: '/var/log/auth.log' }, offset: 981036 }, + source: { + geo: { + continent_name: 'Europe', + country_iso_code: 'HR', + location: { lon: 15.5, lat: 45.1667 }, + }, + as: { + number: 42864, + organization: { name: 'Giganet Internet Szolgaltato Kft' }, + }, + ip: '45.95.168.157', + }, + fileset: { name: 'auth' }, + input: { type: 'log' }, + '@timestamp': '2020-09-04T07:23:22.000Z', + system: { auth: { ssh: { event: 'Invalid' } } }, + ecs: { version: '1.5.0' }, + related: { ip: ['45.95.168.157'], user: ['demo'] }, + service: { type: 'system' }, + host: { hostname: 'bastion00', name: 'bastion00.siem.estc.dev' }, + event: { + ingested: '2020-09-04T07:23:26.046346Z', + timezone: '+00:00', + kind: 'event', + module: 'system', + action: 'ssh_login', + type: ['authentication_failure', 'info'], + category: ['authentication'], + dataset: 'system.auth', + outcome: 'failure', + }, + user: { name: 'demo' }, + }, + sort: [1599204202000], + }, + ], + }, + }, + }, + successes: { + doc_count: 0, + lastSuccess: { hits: { total: 0, max_score: 0, hits: [] } }, + }, + }, + { + key: 'git', + doc_count: 13, + failures: { + doc_count: 13, + lastFailure: { + hits: { + total: 13, + max_score: 0, + hits: [ + { + _index: '.ds-logs-system.auth-default-000001', + _id: 'PqYfWXQBA6bGZw2uIhVU', + _score: null, + _source: { + agent: { + hostname: 'siem-kibana', + name: 'siem-kibana', + id: 'aa3d9dc7-fef1-4c2f-a68d-25785d624e35', + ephemeral_id: 'e503bd85-11c7-4bc9-ae7d-70be1d919fb7', + type: 'filebeat', + version: '7.9.1', + }, + process: { name: 'sshd', pid: 20396 }, + log: { file: { path: '/var/log/auth.log' }, offset: 550795 }, + source: { + geo: { + continent_name: 'Asia', + region_iso_code: 'CN-BJ', + city_name: 'Beijing', + country_iso_code: 'CN', + region_name: 'Beijing', + location: { lon: 116.3889, lat: 39.9288 }, + }, + as: { + number: 45090, + organization: { + name: 'Shenzhen Tencent Computer Systems Company Limited', + }, + }, + ip: '123.206.30.76', + }, + cloud: { + availability_zone: 'us-east1-b', + instance: { name: 'siem-kibana', id: '5412578377715150143' }, + provider: 'gcp', + machine: { type: 'n1-standard-2' }, + project: { id: 'elastic-beats' }, + }, + input: { type: 'log' }, + '@timestamp': '2020-09-04T11:20:26.000Z', + system: { auth: { ssh: { event: 'Invalid' } } }, + ecs: { version: '1.5.0' }, + data_stream: { namespace: 'default', type: 'logs', dataset: 'system.auth' }, + host: { + hostname: 'siem-kibana', + os: { + kernel: '4.9.0-8-amd64', + codename: 'stretch', + name: 'Debian GNU/Linux', + family: 'debian', + version: '9 (stretch)', + platform: 'debian', + }, + containerized: false, + ip: ['10.142.0.7', 'fe80::4001:aff:fe8e:7'], + name: 'siem-kibana', + id: 'aa7ca589f1b8220002f2fc61c64cfbf1', + mac: ['42:01:0a:8e:00:07'], + architecture: 'x86_64', + }, + event: { + timezone: '+00:00', + action: 'ssh_login', + type: 'authentication_failure', + category: 'authentication', + dataset: 'system.auth', + outcome: 'failure', + }, + user: { name: 'git' }, + }, + sort: [1599218426000], + }, + ], + }, + }, + }, + successes: { + doc_count: 0, + lastSuccess: { hits: { total: 0, max_score: 0, hits: [] } }, + }, + }, + { + key: 'webadmin', + doc_count: 13, + failures: { + doc_count: 13, + lastFailure: { + hits: { + total: 13, + max_score: 0, + hits: [ + { + _index: 'filebeat-8.0.0-2020.09.02-000001', + _id: 'iMABWHQBB-gskclyitP-', + _score: null, + _source: { + agent: { + name: 'bastion00.siem.estc.dev', + id: 'f9a321c1-ec27-49fa-aacf-6a50ef6d836f', + type: 'filebeat', + ephemeral_id: '734ee3da-1a4f-4bc9-b400-e0cf0e5eeebc', + version: '8.0.0', + }, + process: { name: 'sshd', pid: 19870 }, + log: { file: { path: '/var/log/auth.log' }, offset: 984133 }, + source: { + geo: { + continent_name: 'Europe', + country_iso_code: 'HR', + location: { lon: 15.5, lat: 45.1667 }, + }, + as: { + number: 42864, + organization: { name: 'Giganet Internet Szolgaltato Kft' }, + }, + ip: '45.95.168.157', + }, + fileset: { name: 'auth' }, + input: { type: 'log' }, + '@timestamp': '2020-09-04T07:25:28.000Z', + system: { auth: { ssh: { event: 'Invalid' } } }, + ecs: { version: '1.5.0' }, + related: { ip: ['45.95.168.157'], user: ['webadmin'] }, + service: { type: 'system' }, + host: { hostname: 'bastion00', name: 'bastion00.siem.estc.dev' }, + event: { + ingested: '2020-09-04T07:25:30.236651Z', + timezone: '+00:00', + kind: 'event', + module: 'system', + action: 'ssh_login', + type: ['authentication_failure', 'info'], + category: ['authentication'], + dataset: 'system.auth', + outcome: 'failure', + }, + user: { name: 'webadmin' }, + }, + sort: [1599204328000], + }, + ], + }, + }, + }, + successes: { + doc_count: 0, + lastSuccess: { hits: { total: 0, max_score: 0, hits: [] } }, + }, + }, + ], + }, + user_count: { value: 188 }, + }, + }, + total: 21, + loaded: 21, +}; + +export const formattedSearchStrategyResponse = { + isPartial: false, + isRunning: false, + rawResponse: { + took: 14, + timed_out: false, + _shards: { total: 21, successful: 21, skipped: 0, failed: 0 }, + hits: { total: -1, max_score: 0, hits: [] }, + aggregations: { + group_by_users: { + doc_count_error_upper_bound: -1, + sum_other_doc_count: 408, + buckets: [ + { + key: 'SYSTEM', + doc_count: 281, + failures: { + meta: {}, + doc_count: 0, + lastFailure: { hits: { total: 0, max_score: 0, hits: [] } }, + }, + successes: { + meta: {}, + doc_count: 4, + lastSuccess: { + hits: { + total: 4, + max_score: 0, + hits: [ + { + _index: 'winlogbeat-8.0.0-2020.09.02-000001', + _id: 'zqY7WXQBA6bGZw2uLeKI', + _score: null, + _source: { + process: { + name: 'services.exe', + pid: 564, + executable: 'C:\\Windows\\System32\\services.exe', + }, + agent: { + build_date: '2020-07-16 09:16:27 +0000 UTC ', + name: 'siem-windows', + commit: '4dcbde39492bdc3843034bba8db811c68cb44b97 ', + id: '05e1bff7-d7a8-416a-8554-aa10288fa07d', + type: 'winlogbeat', + ephemeral_id: '655abd6c-6c33-435d-a2eb-79b2a01e6d61', + version: '8.0.0', + user: { name: 'inside_winlogbeat_user' }, + }, + winlog: { + computer_name: 'siem-windows', + process: { pid: 576, thread: { id: 880 } }, + keywords: ['Audit Success'], + logon: { id: '0x3e7', type: 'Service' }, + channel: 'Security', + event_data: { + LogonGuid: '{00000000-0000-0000-0000-000000000000}', + TargetOutboundDomainName: '-', + VirtualAccount: '%%1843', + LogonType: '5', + IpPort: '-', + TransmittedServices: '-', + SubjectLogonId: '0x3e7', + LmPackageName: '-', + TargetOutboundUserName: '-', + KeyLength: '0', + TargetLogonId: '0x3e7', + RestrictedAdminMode: '-', + SubjectUserName: 'SIEM-WINDOWS$', + TargetLinkedLogonId: '0x0', + ElevatedToken: '%%1842', + SubjectDomainName: 'WORKGROUP', + IpAddress: '-', + ImpersonationLevel: '%%1833', + TargetUserName: 'SYSTEM', + LogonProcessName: 'Advapi ', + TargetDomainName: 'NT AUTHORITY', + SubjectUserSid: 'S-1-5-18', + TargetUserSid: 'S-1-5-18', + AuthenticationPackageName: 'Negotiate', + }, + opcode: 'Info', + version: 2, + record_id: 57818, + task: 'Logon', + event_id: 4624, + provider_guid: '{54849625-5478-4994-a5ba-3e3b0328c30d}', + activity_id: '{d2485217-6bac-0000-8fbb-3f7e2571d601}', + api: 'wineventlog', + provider_name: 'Microsoft-Windows-Security-Auditing', + }, + log: { level: 'information' }, + source: { domain: '-' }, + message: + 'An account was successfully logged on.\n\nSubject:\n\tSecurity ID:\t\tS-1-5-18\n\tAccount Name:\t\tSIEM-WINDOWS$\n\tAccount Domain:\t\tWORKGROUP\n\tLogon ID:\t\t0x3E7\n\nLogon Information:\n\tLogon Type:\t\t5\n\tRestricted Admin Mode:\t-\n\tVirtual Account:\t\tNo\n\tElevated Token:\t\tYes\n\nImpersonation Level:\t\tImpersonation\n\nNew Logon:\n\tSecurity ID:\t\tS-1-5-18\n\tAccount Name:\t\tSYSTEM\n\tAccount Domain:\t\tNT AUTHORITY\n\tLogon ID:\t\t0x3E7\n\tLinked Logon ID:\t\t0x0\n\tNetwork Account Name:\t-\n\tNetwork Account Domain:\t-\n\tLogon GUID:\t\t{00000000-0000-0000-0000-000000000000}\n\nProcess Information:\n\tProcess ID:\t\t0x234\n\tProcess Name:\t\tC:\\Windows\\System32\\services.exe\n\nNetwork Information:\n\tWorkstation Name:\t-\n\tSource Network Address:\t-\n\tSource Port:\t\t-\n\nDetailed Authentication Information:\n\tLogon Process:\t\tAdvapi \n\tAuthentication Package:\tNegotiate\n\tTransited Services:\t-\n\tPackage Name (NTLM only):\t-\n\tKey Length:\t\t0\n\nThis event is generated when a logon session is created. It is generated on the computer that was accessed.\n\nThe subject fields indicate the account on the local system which requested the logon. This is most commonly a service such as the Server service, or a local process such as Winlogon.exe or Services.exe.\n\nThe logon type field indicates the kind of logon that occurred. The most common types are 2 (interactive) and 3 (network).\n\nThe New Logon fields indicate the account for whom the new logon was created, i.e. the account that was logged on.\n\nThe network fields indicate where a remote logon request originated. Workstation name is not always available and may be left blank in some cases.\n\nThe impersonation level field indicates the extent to which a process in the logon session can impersonate.\n\nThe authentication information fields provide detailed information about this specific logon request.\n\t- Logon GUID is a unique identifier that can be used to correlate this event with a KDC event.\n\t- Transited services indicate which intermediate services have participated in this logon request.\n\t- Package name indicates which sub-protocol was used among the NTLM protocols.\n\t- Key length indicates the length of the generated session key. This will be 0 if no session key was requested.', + cloud: { + availability_zone: 'us-central1-c', + instance: { name: 'siem-windows', id: '9156726559029788564' }, + provider: 'gcp', + machine: { type: 'g1-small' }, + project: { id: 'elastic-siem' }, + }, + '@timestamp': '2020-09-04T13:08:02.532Z', + related: { user: ['SYSTEM', 'SIEM-WINDOWS$'] }, + ecs: { version: '1.5.0' }, + host: { + hostname: 'siem-windows', + os: { + build: '17763.1397', + kernel: '10.0.17763.1397 (WinBuild.160101.0800)', + name: 'Windows Server 2019 Datacenter', + family: 'windows', + version: '10.0', + platform: 'windows', + }, + ip: ['fe80::ecf5:decc:3ec3:767e', '10.200.0.15'], + name: 'siem-windows', + id: 'ce1d3c9b-a815-4643-9641-ada0f2c00609', + mac: ['42:01:0a:c8:00:0f'], + architecture: 'x86_64', + }, + event: { + code: 4624, + provider: 'Microsoft-Windows-Security-Auditing', + created: '2020-09-04T13:08:03.638Z', + kind: 'event', + module: 'security', + action: 'logged-in', + category: 'authentication', + type: 'start', + outcome: 'success', + }, + user: { domain: 'NT AUTHORITY', name: 'SYSTEM', id: 'S-1-5-18' }, + }, + sort: [1599224882532], + }, + ], + }, + }, + }, + }, + { + key: 'tsg', + doc_count: 1, + failures: { + doc_count: 0, + lastFailure: { hits: { total: 0, max_score: 0, hits: [] } }, + }, + successes: { + doc_count: 1, + lastSuccess: { + hits: { + total: 1, + max_score: 0, + hits: [ + { + _index: '.ds-logs-system.auth-default-000001', + _id: '9_sfWXQBc39KFIJbIsDh', + _score: null, + _source: { + agent: { + hostname: 'siem-kibana', + name: 'siem-kibana', + id: 'aa3d9dc7-fef1-4c2f-a68d-25785d624e35', + ephemeral_id: 'e503bd85-11c7-4bc9-ae7d-70be1d919fb7', + type: 'filebeat', + version: '7.9.1', + }, + process: { name: 'sshd', pid: 20764 }, + log: { file: { path: '/var/log/auth.log' }, offset: 552463 }, + source: { + geo: { + continent_name: 'Europe', + region_iso_code: 'DE-BE', + city_name: 'Berlin', + country_iso_code: 'DE', + region_name: 'Land Berlin', + location: { lon: 13.3512, lat: 52.5727 }, + }, + as: { number: 6805, organization: { name: 'Telefonica Germany' } }, + port: 57457, + ip: '77.183.42.188', + }, + cloud: { + availability_zone: 'us-east1-b', + instance: { name: 'siem-kibana', id: '5412578377715150143' }, + provider: 'gcp', + machine: { type: 'n1-standard-2' }, + project: { id: 'elastic-beats' }, + }, + input: { type: 'log' }, + '@timestamp': '2020-09-04T11:49:21.000Z', + system: { + auth: { + ssh: { + method: 'publickey', + signature: 'RSA SHA256:vv64JNLzKZWYA9vonnGWuW7zxWhyZrL/BFxyIGbISx8', + event: 'Accepted', + }, + }, + }, + ecs: { version: '1.5.0' }, + data_stream: { namespace: 'default', type: 'logs', dataset: 'system.auth' }, + host: { + hostname: 'siem-kibana', + os: { + kernel: '4.9.0-8-amd64', + codename: 'stretch', + name: 'Debian GNU/Linux', + family: 'debian', + version: '9 (stretch)', + platform: 'debian', + }, + containerized: false, + ip: ['10.142.0.7', 'fe80::4001:aff:fe8e:7'], + name: 'siem-kibana', + id: 'aa7ca589f1b8220002f2fc61c64cfbf1', + mac: ['42:01:0a:8e:00:07'], + architecture: 'x86_64', + }, + event: { + timezone: '+00:00', + action: 'ssh_login', + type: 'authentication_success', + category: 'authentication', + dataset: 'system.auth', + outcome: 'success', + }, + user: { name: 'tsg' }, + }, + sort: [1599220161000], + }, + ], + }, + }, + }, + }, + { + key: 'admin', + doc_count: 23, + failures: { + doc_count: 23, + lastFailure: { + hits: { + total: 23, + max_score: 0, + hits: [ + { + _index: '.ds-logs-system.auth-default-000001', + _id: 'ZfxZWXQBc39KFIJbLN5U', + _score: null, + _source: { + agent: { + hostname: 'siem-kibana', + name: 'siem-kibana', + id: 'aa3d9dc7-fef1-4c2f-a68d-25785d624e35', + ephemeral_id: 'e503bd85-11c7-4bc9-ae7d-70be1d919fb7', + type: 'filebeat', + version: '7.9.1', + }, + process: { name: 'sshd', pid: 22913 }, + log: { file: { path: '/var/log/auth.log' }, offset: 562910 }, + source: { + geo: { + continent_name: 'Asia', + region_iso_code: 'KR-28', + city_name: 'Incheon', + country_iso_code: 'KR', + region_name: 'Incheon', + location: { lon: 126.7288, lat: 37.4562 }, + }, + as: { number: 4766, organization: { name: 'Korea Telecom' } }, + ip: '59.15.3.197', + }, + cloud: { + availability_zone: 'us-east1-b', + instance: { name: 'siem-kibana', id: '5412578377715150143' }, + provider: 'gcp', + machine: { type: 'n1-standard-2' }, + project: { id: 'elastic-beats' }, + }, + input: { type: 'log' }, + '@timestamp': '2020-09-04T13:40:46.000Z', + system: { auth: { ssh: { event: 'Invalid' } } }, + ecs: { version: '1.5.0' }, + data_stream: { namespace: 'default', type: 'logs', dataset: 'system.auth' }, + host: { + hostname: 'siem-kibana', + os: { + kernel: '4.9.0-8-amd64', + codename: 'stretch', + name: 'Debian GNU/Linux', + family: 'debian', + version: '9 (stretch)', + platform: 'debian', + }, + containerized: false, + ip: ['10.142.0.7', 'fe80::4001:aff:fe8e:7'], + name: 'siem-kibana', + id: 'aa7ca589f1b8220002f2fc61c64cfbf1', + mac: ['42:01:0a:8e:00:07'], + architecture: 'x86_64', + }, + event: { + timezone: '+00:00', + action: 'ssh_login', + type: 'authentication_failure', + category: 'authentication', + dataset: 'system.auth', + outcome: 'failure', + }, + user: { name: 'admin' }, + }, + sort: [1599226846000], + }, + ], + }, + }, + }, + successes: { + doc_count: 0, + lastSuccess: { hits: { total: 0, max_score: 0, hits: [] } }, + }, + }, + { + key: 'user', + doc_count: 21, + failures: { + doc_count: 21, + lastFailure: { + hits: { + total: 21, + max_score: 0, + hits: [ + { + _index: 'filebeat-8.0.0-2020.09.02-000001', + _id: 'M_xLWXQBc39KFIJbY7Cb', + _score: null, + _source: { + agent: { + name: 'bastion00.siem.estc.dev', + id: 'f9a321c1-ec27-49fa-aacf-6a50ef6d836f', + type: 'filebeat', + ephemeral_id: '734ee3da-1a4f-4bc9-b400-e0cf0e5eeebc', + version: '8.0.0', + }, + process: { name: 'sshd', pid: 20671 }, + log: { file: { path: '/var/log/auth.log' }, offset: 1028103 }, + source: { + geo: { + continent_name: 'North America', + region_iso_code: 'US-NY', + city_name: 'New York', + country_iso_code: 'US', + region_name: 'New York', + location: { lon: -74, lat: 40.7157 }, + }, + ip: '64.227.88.245', + }, + fileset: { name: 'auth' }, + input: { type: 'log' }, + '@timestamp': '2020-09-04T13:25:43.000Z', + system: { auth: { ssh: { event: 'Invalid' } } }, + ecs: { version: '1.5.0' }, + related: { ip: ['64.227.88.245'], user: ['user'] }, + service: { type: 'system' }, + host: { hostname: 'bastion00', name: 'bastion00.siem.estc.dev' }, + event: { + ingested: '2020-09-04T13:25:47.034172Z', + timezone: '+00:00', + kind: 'event', + module: 'system', + action: 'ssh_login', + type: ['authentication_failure', 'info'], + category: ['authentication'], + dataset: 'system.auth', + outcome: 'failure', + }, + user: { name: 'user' }, + }, + sort: [1599225943000], + }, + ], + }, + }, + }, + successes: { + doc_count: 0, + lastSuccess: { hits: { total: 0, max_score: 0, hits: [] } }, + }, + }, + { + key: 'ubuntu', + doc_count: 18, + failures: { + doc_count: 18, + lastFailure: { + hits: { + total: 18, + max_score: 0, + hits: [ + { + _index: 'filebeat-8.0.0-2020.09.02-000001', + _id: 'nPxKWXQBc39KFIJb7q4w', + _score: null, + _source: { + agent: { + name: 'bastion00.siem.estc.dev', + id: 'f9a321c1-ec27-49fa-aacf-6a50ef6d836f', + ephemeral_id: '734ee3da-1a4f-4bc9-b400-e0cf0e5eeebc', + type: 'filebeat', + version: '8.0.0', + }, + process: { name: 'sshd', pid: 20665 }, + log: { file: { path: '/var/log/auth.log' }, offset: 1027372 }, + source: { + geo: { + continent_name: 'North America', + region_iso_code: 'US-NY', + city_name: 'New York', + country_iso_code: 'US', + region_name: 'New York', + location: { lon: -74, lat: 40.7157 }, + }, + ip: '64.227.88.245', + }, + fileset: { name: 'auth' }, + input: { type: 'log' }, + '@timestamp': '2020-09-04T13:25:07.000Z', + system: { auth: { ssh: { event: 'Invalid' } } }, + ecs: { version: '1.5.0' }, + related: { ip: ['64.227.88.245'], user: ['ubuntu'] }, + service: { type: 'system' }, + host: { hostname: 'bastion00', name: 'bastion00.siem.estc.dev' }, + event: { + ingested: '2020-09-04T13:25:16.974606Z', + timezone: '+00:00', + kind: 'event', + module: 'system', + action: 'ssh_login', + type: ['authentication_failure', 'info'], + category: ['authentication'], + dataset: 'system.auth', + outcome: 'failure', + }, + user: { name: 'ubuntu' }, + }, + sort: [1599225907000], + }, + ], + }, + }, + }, + successes: { + doc_count: 0, + lastSuccess: { hits: { total: 0, max_score: 0, hits: [] } }, + }, + }, + { + key: 'odoo', + doc_count: 17, + failures: { + doc_count: 17, + lastFailure: { + hits: { + total: 17, + max_score: 0, + hits: [ + { + _index: '.ds-logs-system.auth-default-000001', + _id: 'mPsfWXQBc39KFIJbI8HI', + _score: null, + _source: { + agent: { + hostname: 'siem-kibana', + name: 'siem-kibana', + id: 'aa3d9dc7-fef1-4c2f-a68d-25785d624e35', + type: 'filebeat', + ephemeral_id: 'e503bd85-11c7-4bc9-ae7d-70be1d919fb7', + version: '7.9.1', + }, + process: { name: 'sshd', pid: 21506 }, + log: { file: { path: '/var/log/auth.log' }, offset: 556761 }, + source: { + geo: { + continent_name: 'Asia', + region_iso_code: 'IN-DL', + city_name: 'New Delhi', + country_iso_code: 'IN', + region_name: 'National Capital Territory of Delhi', + location: { lon: 77.2245, lat: 28.6358 }, + }, + as: { number: 10029, organization: { name: 'SHYAM SPECTRA PVT LTD' } }, + ip: '180.151.228.166', + }, + cloud: { + availability_zone: 'us-east1-b', + instance: { name: 'siem-kibana', id: '5412578377715150143' }, + provider: 'gcp', + machine: { type: 'n1-standard-2' }, + project: { id: 'elastic-beats' }, + }, + input: { type: 'log' }, + '@timestamp': '2020-09-04T12:26:36.000Z', + system: { auth: { ssh: { event: 'Invalid' } } }, + ecs: { version: '1.5.0' }, + data_stream: { namespace: 'default', type: 'logs', dataset: 'system.auth' }, + host: { + hostname: 'siem-kibana', + os: { + kernel: '4.9.0-8-amd64', + codename: 'stretch', + name: 'Debian GNU/Linux', + family: 'debian', + version: '9 (stretch)', + platform: 'debian', + }, + containerized: false, + ip: ['10.142.0.7', 'fe80::4001:aff:fe8e:7'], + name: 'siem-kibana', + id: 'aa7ca589f1b8220002f2fc61c64cfbf1', + mac: ['42:01:0a:8e:00:07'], + architecture: 'x86_64', + }, + event: { + timezone: '+00:00', + action: 'ssh_login', + type: 'authentication_failure', + category: 'authentication', + dataset: 'system.auth', + outcome: 'failure', + }, + user: { name: 'odoo' }, + }, + sort: [1599222396000], + }, + ], + }, + }, + }, + successes: { + doc_count: 0, + lastSuccess: { hits: { total: 0, max_score: 0, hits: [] } }, + }, + }, + { + key: 'pi', + doc_count: 17, + failures: { + doc_count: 17, + lastFailure: { + hits: { + total: 17, + max_score: 0, + hits: [ + { + _index: 'filebeat-8.0.0-2020.09.02-000001', + _id: 'aaToWHQBA6bGZw2uR-St', + _score: null, + _source: { + agent: { + name: 'bastion00.siem.estc.dev', + id: 'f9a321c1-ec27-49fa-aacf-6a50ef6d836f', + type: 'filebeat', + ephemeral_id: '734ee3da-1a4f-4bc9-b400-e0cf0e5eeebc', + version: '8.0.0', + }, + process: { name: 'sshd', pid: 20475 }, + log: { file: { path: '/var/log/auth.log' }, offset: 1019218 }, + source: { + geo: { + continent_name: 'Europe', + region_iso_code: 'SE-AB', + city_name: 'Stockholm', + country_iso_code: 'SE', + region_name: 'Stockholm', + location: { lon: 17.7833, lat: 59.25 }, + }, + as: { number: 8473, organization: { name: 'Bahnhof AB' } }, + ip: '178.174.148.58', + }, + fileset: { name: 'auth' }, + input: { type: 'log' }, + '@timestamp': '2020-09-04T11:37:22.000Z', + system: { auth: { ssh: { event: 'Invalid' } } }, + ecs: { version: '1.5.0' }, + related: { ip: ['178.174.148.58'], user: ['pi'] }, + service: { type: 'system' }, + host: { hostname: 'bastion00', name: 'bastion00.siem.estc.dev' }, + event: { + ingested: '2020-09-04T11:37:31.797423Z', + timezone: '+00:00', + kind: 'event', + module: 'system', + action: 'ssh_login', + type: ['authentication_failure', 'info'], + category: ['authentication'], + dataset: 'system.auth', + outcome: 'failure', + }, + user: { name: 'pi' }, + }, + sort: [1599219442000], + }, + ], + }, + }, + }, + successes: { + doc_count: 0, + lastSuccess: { hits: { total: 0, max_score: 0, hits: [] } }, + }, + }, + { + key: 'demo', + doc_count: 14, + failures: { + doc_count: 14, + lastFailure: { + hits: { + total: 14, + max_score: 0, + hits: [ + { + _index: 'filebeat-8.0.0-2020.09.02-000001', + _id: 'VaP_V3QBA6bGZw2upUbg', + _score: null, + _source: { + agent: { + name: 'bastion00.siem.estc.dev', + id: 'f9a321c1-ec27-49fa-aacf-6a50ef6d836f', + type: 'filebeat', + ephemeral_id: '734ee3da-1a4f-4bc9-b400-e0cf0e5eeebc', + version: '8.0.0', + }, + process: { name: 'sshd', pid: 19849 }, + log: { file: { path: '/var/log/auth.log' }, offset: 981036 }, + source: { + geo: { + continent_name: 'Europe', + country_iso_code: 'HR', + location: { lon: 15.5, lat: 45.1667 }, + }, + as: { + number: 42864, + organization: { name: 'Giganet Internet Szolgaltato Kft' }, + }, + ip: '45.95.168.157', + }, + fileset: { name: 'auth' }, + input: { type: 'log' }, + '@timestamp': '2020-09-04T07:23:22.000Z', + system: { auth: { ssh: { event: 'Invalid' } } }, + ecs: { version: '1.5.0' }, + related: { ip: ['45.95.168.157'], user: ['demo'] }, + service: { type: 'system' }, + host: { hostname: 'bastion00', name: 'bastion00.siem.estc.dev' }, + event: { + ingested: '2020-09-04T07:23:26.046346Z', + timezone: '+00:00', + kind: 'event', + module: 'system', + action: 'ssh_login', + type: ['authentication_failure', 'info'], + category: ['authentication'], + dataset: 'system.auth', + outcome: 'failure', + }, + user: { name: 'demo' }, + }, + sort: [1599204202000], + }, + ], + }, + }, + }, + successes: { + doc_count: 0, + lastSuccess: { hits: { total: 0, max_score: 0, hits: [] } }, + }, + }, + { + key: 'git', + doc_count: 13, + failures: { + doc_count: 13, + lastFailure: { + hits: { + total: 13, + max_score: 0, + hits: [ + { + _index: '.ds-logs-system.auth-default-000001', + _id: 'PqYfWXQBA6bGZw2uIhVU', + _score: null, + _source: { + agent: { + hostname: 'siem-kibana', + name: 'siem-kibana', + id: 'aa3d9dc7-fef1-4c2f-a68d-25785d624e35', + ephemeral_id: 'e503bd85-11c7-4bc9-ae7d-70be1d919fb7', + type: 'filebeat', + version: '7.9.1', + }, + process: { name: 'sshd', pid: 20396 }, + log: { file: { path: '/var/log/auth.log' }, offset: 550795 }, + source: { + geo: { + continent_name: 'Asia', + region_iso_code: 'CN-BJ', + city_name: 'Beijing', + country_iso_code: 'CN', + region_name: 'Beijing', + location: { lon: 116.3889, lat: 39.9288 }, + }, + as: { + number: 45090, + organization: { + name: 'Shenzhen Tencent Computer Systems Company Limited', + }, + }, + ip: '123.206.30.76', + }, + cloud: { + availability_zone: 'us-east1-b', + instance: { name: 'siem-kibana', id: '5412578377715150143' }, + provider: 'gcp', + machine: { type: 'n1-standard-2' }, + project: { id: 'elastic-beats' }, + }, + input: { type: 'log' }, + '@timestamp': '2020-09-04T11:20:26.000Z', + system: { auth: { ssh: { event: 'Invalid' } } }, + ecs: { version: '1.5.0' }, + data_stream: { namespace: 'default', type: 'logs', dataset: 'system.auth' }, + host: { + hostname: 'siem-kibana', + os: { + kernel: '4.9.0-8-amd64', + codename: 'stretch', + name: 'Debian GNU/Linux', + family: 'debian', + version: '9 (stretch)', + platform: 'debian', + }, + containerized: false, + ip: ['10.142.0.7', 'fe80::4001:aff:fe8e:7'], + name: 'siem-kibana', + id: 'aa7ca589f1b8220002f2fc61c64cfbf1', + mac: ['42:01:0a:8e:00:07'], + architecture: 'x86_64', + }, + event: { + timezone: '+00:00', + action: 'ssh_login', + type: 'authentication_failure', + category: 'authentication', + dataset: 'system.auth', + outcome: 'failure', + }, + user: { name: 'git' }, + }, + sort: [1599218426000], + }, + ], + }, + }, + }, + successes: { + doc_count: 0, + lastSuccess: { hits: { total: 0, max_score: 0, hits: [] } }, + }, + }, + { + key: 'webadmin', + doc_count: 13, + failures: { + doc_count: 13, + lastFailure: { + hits: { + total: 13, + max_score: 0, + hits: [ + { + _index: 'filebeat-8.0.0-2020.09.02-000001', + _id: 'iMABWHQBB-gskclyitP-', + _score: null, + _source: { + agent: { + name: 'bastion00.siem.estc.dev', + id: 'f9a321c1-ec27-49fa-aacf-6a50ef6d836f', + type: 'filebeat', + ephemeral_id: '734ee3da-1a4f-4bc9-b400-e0cf0e5eeebc', + version: '8.0.0', + }, + process: { name: 'sshd', pid: 19870 }, + log: { file: { path: '/var/log/auth.log' }, offset: 984133 }, + source: { + geo: { + continent_name: 'Europe', + country_iso_code: 'HR', + location: { lon: 15.5, lat: 45.1667 }, + }, + as: { + number: 42864, + organization: { name: 'Giganet Internet Szolgaltato Kft' }, + }, + ip: '45.95.168.157', + }, + fileset: { name: 'auth' }, + input: { type: 'log' }, + '@timestamp': '2020-09-04T07:25:28.000Z', + system: { auth: { ssh: { event: 'Invalid' } } }, + ecs: { version: '1.5.0' }, + related: { ip: ['45.95.168.157'], user: ['webadmin'] }, + service: { type: 'system' }, + host: { hostname: 'bastion00', name: 'bastion00.siem.estc.dev' }, + event: { + ingested: '2020-09-04T07:25:30.236651Z', + timezone: '+00:00', + kind: 'event', + module: 'system', + action: 'ssh_login', + type: ['authentication_failure', 'info'], + category: ['authentication'], + dataset: 'system.auth', + outcome: 'failure', + }, + user: { name: 'webadmin' }, + }, + sort: [1599204328000], + }, + ], + }, + }, + }, + successes: { + doc_count: 0, + lastSuccess: { hits: { total: 0, max_score: 0, hits: [] } }, + }, + }, + ], + }, + user_count: { value: 188 }, + }, + }, + total: 21, + loaded: 21, + inspect: { + dsl: [ + '{\n "allowNoIndices": true,\n "index": [\n "apm-*-transaction*",\n "auditbeat-*",\n "endgame-*",\n "filebeat-*",\n "logs-*",\n "packetbeat-*",\n "winlogbeat-*"\n ],\n "ignoreUnavailable": true,\n "body": {\n "aggregations": {\n "user_count": {\n "cardinality": {\n "field": "user.name"\n }\n },\n "group_by_users": {\n "terms": {\n "size": 10,\n "field": "user.name",\n "order": [\n {\n "successes.doc_count": "desc"\n },\n {\n "failures.doc_count": "desc"\n }\n ]\n },\n "aggs": {\n "failures": {\n "filter": {\n "term": {\n "event.outcome": "failure"\n }\n },\n "aggs": {\n "lastFailure": {\n "top_hits": {\n "size": 1,\n "_source": [],\n "sort": [\n {\n "@timestamp": {\n "order": "desc"\n }\n }\n ]\n }\n }\n }\n },\n "successes": {\n "filter": {\n "term": {\n "event.outcome": "success"\n }\n },\n "aggs": {\n "lastSuccess": {\n "top_hits": {\n "size": 1,\n "_source": [],\n "sort": [\n {\n "@timestamp": {\n "order": "desc"\n }\n }\n ]\n }\n }\n }\n }\n }\n }\n },\n "query": {\n "bool": {\n "filter": [\n "{\\"bool\\":{\\"must\\":[],\\"filter\\":[{\\"match_all\\":{}}],\\"should\\":[],\\"must_not\\":[]}}",\n {\n "term": {\n "event.category": "authentication"\n }\n },\n {\n "range": {\n "@timestamp": {\n "gte": "2020-09-02T15:17:13.678Z",\n "lte": "2020-09-03T15:17:13.678Z",\n "format": "strict_date_optional_time"\n }\n }\n }\n ]\n }\n },\n "size": 0\n },\n "track_total_hits": false\n}', + ], + }, + edges: [ + { + node: { + failures: 0, + successes: 4, + _id: 'SYSTEM+281', + user: { name: ['SYSTEM'] }, + lastSuccess: { + timestamp: ['2020-09-04T13:08:02.532Z'], + host: { id: ['ce1d3c9b-a815-4643-9641-ada0f2c00609'], name: ['siem-windows'] }, + }, + }, + cursor: { value: '', tiebreaker: null }, + }, + { + node: { + failures: 0, + successes: 1, + _id: 'tsg+1', + user: { name: ['tsg'] }, + lastSuccess: { + timestamp: ['2020-09-04T11:49:21.000Z'], + source: { ip: ['77.183.42.188'] }, + host: { id: ['aa7ca589f1b8220002f2fc61c64cfbf1'], name: ['siem-kibana'] }, + }, + }, + cursor: { value: '', tiebreaker: null }, + }, + { + node: { + failures: 23, + successes: 0, + _id: 'admin+23', + user: { name: ['admin'] }, + lastFailure: { + timestamp: ['2020-09-04T13:40:46.000Z'], + source: { ip: ['59.15.3.197'] }, + host: { id: ['aa7ca589f1b8220002f2fc61c64cfbf1'], name: ['siem-kibana'] }, + }, + }, + cursor: { value: '', tiebreaker: null }, + }, + { + node: { + failures: 21, + successes: 0, + _id: 'user+21', + user: { name: ['user'] }, + lastFailure: { + timestamp: ['2020-09-04T13:25:43.000Z'], + source: { ip: ['64.227.88.245'] }, + host: { name: ['bastion00.siem.estc.dev'] }, + }, + }, + cursor: { value: '', tiebreaker: null }, + }, + { + node: { + failures: 18, + successes: 0, + _id: 'ubuntu+18', + user: { name: ['ubuntu'] }, + lastFailure: { + timestamp: ['2020-09-04T13:25:07.000Z'], + source: { ip: ['64.227.88.245'] }, + host: { name: ['bastion00.siem.estc.dev'] }, + }, + }, + cursor: { value: '', tiebreaker: null }, + }, + { + node: { + failures: 17, + successes: 0, + _id: 'odoo+17', + user: { name: ['odoo'] }, + lastFailure: { + timestamp: ['2020-09-04T12:26:36.000Z'], + source: { ip: ['180.151.228.166'] }, + host: { id: ['aa7ca589f1b8220002f2fc61c64cfbf1'], name: ['siem-kibana'] }, + }, + }, + cursor: { value: '', tiebreaker: null }, + }, + { + node: { + failures: 17, + successes: 0, + _id: 'pi+17', + user: { name: ['pi'] }, + lastFailure: { + timestamp: ['2020-09-04T11:37:22.000Z'], + source: { ip: ['178.174.148.58'] }, + host: { name: ['bastion00.siem.estc.dev'] }, + }, + }, + cursor: { value: '', tiebreaker: null }, + }, + { + node: { + failures: 14, + successes: 0, + _id: 'demo+14', + user: { name: ['demo'] }, + lastFailure: { + timestamp: ['2020-09-04T07:23:22.000Z'], + source: { ip: ['45.95.168.157'] }, + host: { name: ['bastion00.siem.estc.dev'] }, + }, + }, + cursor: { value: '', tiebreaker: null }, + }, + { + node: { + failures: 13, + successes: 0, + _id: 'git+13', + user: { name: ['git'] }, + lastFailure: { + timestamp: ['2020-09-04T11:20:26.000Z'], + source: { ip: ['123.206.30.76'] }, + host: { id: ['aa7ca589f1b8220002f2fc61c64cfbf1'], name: ['siem-kibana'] }, + }, + }, + cursor: { value: '', tiebreaker: null }, + }, + { + node: { + failures: 13, + successes: 0, + _id: 'webadmin+13', + user: { name: ['webadmin'] }, + lastFailure: { + timestamp: ['2020-09-04T07:25:28.000Z'], + source: { ip: ['45.95.168.157'] }, + host: { name: ['bastion00.siem.estc.dev'] }, + }, + }, + cursor: { value: '', tiebreaker: null }, + }, + ], + totalCount: 188, + pageInfo: { activePage: 0, fakeTotalCount: 50, showMorePagesIndicator: true }, +}; + +export const expectedDsl = { + allowNoIndices: true, + index: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'winlogbeat-*', + ], + ignoreUnavailable: true, + body: { + aggregations: { + user_count: { cardinality: { field: 'user.name' } }, + group_by_users: { + terms: { + size: 10, + field: 'user.name', + order: [{ 'successes.doc_count': 'desc' }, { 'failures.doc_count': 'desc' }], + }, + aggs: { + failures: { + filter: { term: { 'event.outcome': 'failure' } }, + aggs: { + lastFailure: { + top_hits: { size: 1, _source: [], sort: [{ '@timestamp': { order: 'desc' } }] }, + }, + }, + }, + successes: { + filter: { term: { 'event.outcome': 'success' } }, + aggs: { + lastSuccess: { + top_hits: { size: 1, _source: [], sort: [{ '@timestamp': { order: 'desc' } }] }, + }, + }, + }, + }, + }, + }, + query: { + bool: { + filter: [ + '{"bool":{"must":[],"filter":[{"match_all":{}}],"should":[],"must_not":[]}}', + { term: { 'event.category': 'authentication' } }, + { + range: { + '@timestamp': { + gte: '2020-09-02T15:17:13.678Z', + lte: '2020-09-03T15:17:13.678Z', + format: 'strict_date_optional_time', + }, + }, + }, + ], + }, + }, + size: 0, + }, + track_total_hits: false, +}; + +export const mockHit: AuthenticationHit = { + _index: 'index-123', + _type: 'type-123', + _id: 'id-123', + _score: 10, + _source: { + '@timestamp': 'time-1', + }, + cursor: 'cursor-1', + sort: [0], + user: 'Evan', + failures: 10, + successes: 20, +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/dsl/query.dsl.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/dsl/query.dsl.test.ts new file mode 100644 index 0000000000000..31e4069e458be --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/dsl/query.dsl.test.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; + * you may not use this file except in compliance with the Elastic License. + */ +import { buildQuery } from './query.dsl'; +import { mockOptions, expectedDsl } from '../__mocks__/'; + +describe('buildQuery', () => { + test('build query from options correctly', () => { + expect(buildQuery(mockOptions)).toEqual(expectedDsl); + }); +}); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/helpers.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/helpers.test.ts new file mode 100644 index 0000000000000..c2c5bc9181c74 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/helpers.test.ts @@ -0,0 +1,118 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { AuthenticationsEdges } from '../../../../../../common/search_strategy/security_solution/hosts/authentications'; +import { auditdFieldsMap } from './dsl/query.dsl'; + +import { formatAuthenticationData } from './helpers'; +import { mockHit } from './__mocks__'; + +describe('#formatAuthenticationsData', () => { + test('it formats a authentication with an empty set', () => { + const fields: readonly string[] = ['']; + const data = formatAuthenticationData(fields, mockHit, auditdFieldsMap); + const expected: AuthenticationsEdges = { + cursor: { + tiebreaker: null, + value: 'cursor-1', + }, + node: { + _id: 'id-123', + failures: 10, + successes: 20, + user: { + name: ['Evan'], + }, + }, + }; + + expect(data).toEqual(expected); + }); + + test('it formats a authentications with a source ip correctly', () => { + const fields: readonly string[] = ['lastSuccess.source.ip']; + const data = formatAuthenticationData(fields, mockHit, auditdFieldsMap); + const expected: AuthenticationsEdges = { + cursor: { + tiebreaker: null, + value: 'cursor-1', + }, + node: { + _id: 'id-123', + failures: 10, + successes: 20, + user: { + name: ['Evan'], + }, + }, + }; + + expect(data).toEqual(expected); + }); + + test('it formats a authentications with a host name only', () => { + const fields: readonly string[] = ['lastSuccess.host.name']; + const data = formatAuthenticationData(fields, mockHit, auditdFieldsMap); + const expected: AuthenticationsEdges = { + cursor: { + tiebreaker: null, + value: 'cursor-1', + }, + node: { + _id: 'id-123', + failures: 10, + successes: 20, + user: { + name: ['Evan'], + }, + }, + }; + + expect(data).toEqual(expected); + }); + + test('it formats a authentications with a host id only', () => { + const fields: readonly string[] = ['lastSuccess.host.id']; + const data = formatAuthenticationData(fields, mockHit, auditdFieldsMap); + const expected: AuthenticationsEdges = { + cursor: { + tiebreaker: null, + value: 'cursor-1', + }, + node: { + _id: 'id-123', + failures: 10, + successes: 20, + user: { + name: ['Evan'], + }, + }, + }; + + expect(data).toEqual(expected); + }); + + test('it formats a authentications with a host name and id correctly', () => { + const fields: readonly string[] = ['lastSuccess.host.name', 'lastSuccess.host.id']; + const data = formatAuthenticationData(fields, mockHit, auditdFieldsMap); + const expected: AuthenticationsEdges = { + cursor: { + tiebreaker: null, + value: 'cursor-1', + }, + node: { + _id: 'id-123', + failures: 10, + successes: 20, + user: { + name: ['Evan'], + }, + }, + }; + + expect(data).toEqual(expected); + }); +}); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/helpers.ts index c6b68bd1c0762..d61914fda7d06 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/helpers.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { get, getOr } from 'lodash/fp'; +import { get, getOr, isEmpty } from 'lodash/fp'; import { set } from '@elastic/safer-lodash-set/fp'; import { mergeFieldsWithHit } from '../../../../../utils/build_query'; import { toArray } from '../../../../helpers/to_array'; @@ -31,10 +31,11 @@ export const authenticationFields = [ ]; export const formatAuthenticationData = ( + fields: readonly string[] = authenticationFields, hit: AuthenticationHit, fieldMap: Readonly> ): AuthenticationsEdges => - authenticationFields.reduce( + fields.reduce( (flattenedFields, fieldName) => { if (hit.cursor) { flattenedFields.cursor.value = hit.cursor; @@ -51,8 +52,11 @@ export const formatAuthenticationData = ( const mergedResult = mergeFieldsWithHit(fieldName, flattenedFields, fieldMap, hit); const fieldPath = `node.${fieldName}`; const fieldValue = get(fieldPath, mergedResult); - - return set(fieldPath, toArray(fieldValue), mergedResult); + if (!isEmpty(fieldValue)) { + return set(fieldPath, toArray(fieldValue), mergedResult); + } else { + return mergedResult; + } }, { node: { diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/index.test.tsx b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/index.test.tsx new file mode 100644 index 0000000000000..9e8e2ead0ed4a --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/index.test.tsx @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../../../common/constants'; + +import { HostAuthenticationsRequestOptions } from '../../../../../../common/search_strategy/security_solution/hosts/authentications'; +import * as buildQuery from './dsl/query.dsl'; +import { authentications } from '.'; +import { + mockOptions, + mockSearchStrategyResponse, + formattedSearchStrategyResponse, +} from './__mocks__'; + +describe('authentications search strategy', () => { + const buildAuthenticationQuery = jest.spyOn(buildQuery, 'buildQuery'); + + afterEach(() => { + buildAuthenticationQuery.mockClear(); + }); + + describe('buildDsl', () => { + test('should build dsl query', () => { + authentications.buildDsl(mockOptions); + expect(buildAuthenticationQuery).toHaveBeenCalledWith(mockOptions); + }); + + test('should throw error if query size is greater equal than DEFAULT_MAX_TABLE_QUERY_SIZE ', () => { + const overSizeOptions = { + ...mockOptions, + pagination: { + ...mockOptions.pagination, + querySize: DEFAULT_MAX_TABLE_QUERY_SIZE, + }, + } as HostAuthenticationsRequestOptions; + + expect(() => { + authentications.buildDsl(overSizeOptions); + }).toThrowError(`No query size above ${DEFAULT_MAX_TABLE_QUERY_SIZE}`); + }); + }); + + describe('parse', () => { + test('should parse data correctly', async () => { + const result = await authentications.parse(mockOptions, mockSearchStrategyResponse); + expect(result).toMatchObject(formattedSearchStrategyResponse); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/index.tsx b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/index.tsx index ded9a7917d921..d5bdeac38cee5 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/index.tsx +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/index.tsx @@ -20,7 +20,7 @@ import { import { inspectStringifyObject } from '../../../../../utils/build_query'; import { SecuritySolutionFactory } from '../../types'; import { auditdFieldsMap, buildQuery as buildAuthenticationQuery } from './dsl/query.dsl'; -import { formatAuthenticationData, getHits } from './helpers'; +import { authenticationFields, formatAuthenticationData, getHits } from './helpers'; export const authentications: SecuritySolutionFactory = { buildDsl: (options: HostAuthenticationsRequestOptions) => { @@ -40,7 +40,7 @@ export const authentications: SecuritySolutionFactory - formatAuthenticationData(hit, auditdFieldsMap) + formatAuthenticationData(authenticationFields, hit, auditdFieldsMap) ); const edges = authenticationEdges.splice(cursorStart, querySize - cursorStart); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/index.ts index a50c9e4004856..338e733b23914 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/index.ts @@ -7,6 +7,7 @@ import { FactoryQueryTypes } from '../../../../common/search_strategy/security_solution'; import { hostsFactory } from './hosts'; +import { matrixHistogramFactory } from './matrix_histogram'; import { networkFactory } from './network'; import { SecuritySolutionFactory } from './types'; @@ -15,5 +16,6 @@ export const securitySolutionFactory: Record< SecuritySolutionFactory > = { ...hostsFactory, + ...matrixHistogramFactory, ...networkFactory, }; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/alerts/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/alerts/index.ts new file mode 100644 index 0000000000000..6f27f298bd699 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/alerts/index.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { buildAlertsHistogramQuery } from './query.alerts_histogram.dsl'; + +export const alertsMatrixHistogramConfig = { + buildDsl: buildAlertsHistogramQuery, + aggName: 'aggregations.alertsGroup.buckets', + parseKey: 'alerts.buckets', +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/alerts/query.alerts_histogram.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/alerts/query.alerts_histogram.dsl.ts new file mode 100644 index 0000000000000..6ec6a110ec3d9 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/alerts/query.alerts_histogram.dsl.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import moment from 'moment'; + +import { + createQueryFilterClauses, + calculateTimeSeriesInterval, +} from '../../../../../utils/build_query'; +import { MatrixHistogramRequestOptions } from '../../../../../../common/search_strategy/security_solution/matrix_histogram'; + +export const buildAlertsHistogramQuery = ({ + filterQuery, + timerange: { from, to }, + defaultIndex, + stackByField, +}: MatrixHistogramRequestOptions) => { + const filter = [ + ...createQueryFilterClauses(filterQuery), + { + bool: { + filter: [ + { + bool: { + should: [ + { + match: { + 'event.kind': 'alert', + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + }, + }, + { + range: { + '@timestamp': { + gte: from, + lte: to, + format: 'strict_date_optional_time', + }, + }, + }, + ]; + + const getHistogramAggregation = () => { + const interval = calculateTimeSeriesInterval(from, to); + const histogramTimestampField = '@timestamp'; + const dateHistogram = { + date_histogram: { + field: histogramTimestampField, + fixed_interval: interval, + min_doc_count: 0, + extended_bounds: { + min: moment(from).valueOf(), + max: moment(to).valueOf(), + }, + }, + }; + return { + alertsGroup: { + terms: { + field: stackByField, + missing: 'All others', + order: { + _count: 'desc', + }, + size: 10, + }, + aggs: { + alerts: dateHistogram, + }, + }, + }; + }; + + const dslQuery = { + index: defaultIndex, + allowNoIndices: true, + ignoreUnavailable: true, + body: { + aggregations: getHistogramAggregation(), + query: { + bool: { + filter, + }, + }, + size: 0, + track_total_hits: true, + }, + }; + + return dslQuery; +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/anomalies/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/anomalies/index.ts new file mode 100644 index 0000000000000..ab273d962ae94 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/anomalies/index.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { buildAnomaliesHistogramQuery } from './query.anomalies_histogram.dsl'; + +export const anomaliesMatrixHistogramConfig = { + buildDsl: buildAnomaliesHistogramQuery, + aggName: 'aggregations.anomalyActionGroup.buckets', + parseKey: 'anomalies.buckets', +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/anomalies/query.anomalies_histogram.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/anomalies/query.anomalies_histogram.dsl.ts new file mode 100644 index 0000000000000..e7e0c4b9ab56f --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/anomalies/query.anomalies_histogram.dsl.ts @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import moment from 'moment'; + +import { + createQueryFilterClauses, + calculateTimeSeriesInterval, +} from '../../../../../utils/build_query'; +import { MatrixHistogramRequestOptions } from '../../../../../../common/search_strategy/security_solution/matrix_histogram'; + +export const buildAnomaliesHistogramQuery = ({ + filterQuery, + timerange: { from, to }, + defaultIndex, + stackByField = 'job_id', +}: MatrixHistogramRequestOptions) => { + const filter = [ + ...createQueryFilterClauses(filterQuery), + { + range: { + timestamp: { + gte: from, + lte: to, + format: 'strict_date_optional_time', + }, + }, + }, + ]; + + const getHistogramAggregation = () => { + const interval = calculateTimeSeriesInterval(from, to); + const histogramTimestampField = 'timestamp'; + const dateHistogram = { + date_histogram: { + field: histogramTimestampField, + fixed_interval: interval, + min_doc_count: 0, + extended_bounds: { + min: moment(from).valueOf(), + max: moment(to).valueOf(), + }, + }, + }; + return { + anomalyActionGroup: { + terms: { + field: stackByField, + order: { + _count: 'desc', + }, + size: 10, + }, + aggs: { + anomalies: dateHistogram, + }, + }, + }; + }; + + const dslQuery = { + index: defaultIndex, + allowNoIndices: true, + ignoreUnavailable: true, + body: { + aggs: getHistogramAggregation(), + query: { + bool: { + filter, + }, + }, + size: 0, + track_total_hits: true, + }, + }; + + return dslQuery; +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/authentications/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/authentications/index.ts new file mode 100644 index 0000000000000..17fb67e5fe94d --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/authentications/index.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { buildAuthenticationsHistogramQuery } from './query.authentications_histogram.dsl'; + +export const authenticationsMatrixHistogramConfig = { + buildDsl: buildAuthenticationsHistogramQuery, + aggName: 'aggregations.eventActionGroup.buckets', + parseKey: 'events.buckets', +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/authentications/query.authentications_histogram.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/authentications/query.authentications_histogram.dsl.ts new file mode 100644 index 0000000000000..a580ae7e0355f --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/authentications/query.authentications_histogram.dsl.ts @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import moment from 'moment'; + +import { + createQueryFilterClauses, + calculateTimeSeriesInterval, +} from '../../../../../utils/build_query'; +import { MatrixHistogramRequestOptions } from '../../../../../../common/search_strategy/security_solution/matrix_histogram'; + +export const buildAuthenticationsHistogramQuery = ({ + filterQuery, + timerange: { from, to }, + defaultIndex, + stackByField = 'event.outcome', +}: MatrixHistogramRequestOptions) => { + const filter = [ + ...createQueryFilterClauses(filterQuery), + { + bool: { + must: [ + { + term: { + 'event.category': 'authentication', + }, + }, + ], + }, + }, + { + range: { + '@timestamp': { + gte: from, + lte: to, + format: 'strict_date_optional_time', + }, + }, + }, + ]; + + const getHistogramAggregation = () => { + const interval = calculateTimeSeriesInterval(from, to); + const histogramTimestampField = '@timestamp'; + const dateHistogram = { + date_histogram: { + field: histogramTimestampField, + fixed_interval: interval, + min_doc_count: 0, + extended_bounds: { + min: moment(from).valueOf(), + max: moment(to).valueOf(), + }, + }, + }; + return { + eventActionGroup: { + terms: { + field: stackByField, + include: ['success', 'failure'], + order: { + _count: 'desc', + }, + size: 2, + }, + aggs: { + events: dateHistogram, + }, + }, + }; + }; + + const dslQuery = { + index: defaultIndex, + allowNoIndices: true, + ignoreUnavailable: true, + body: { + aggregations: getHistogramAggregation(), + query: { + bool: { + filter, + }, + }, + size: 0, + track_total_hits: true, + }, + }; + + return dslQuery; +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/dns/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/dns/helpers.ts new file mode 100644 index 0000000000000..d0fff848b426a --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/dns/helpers.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { get, getOr } from 'lodash/fp'; +import { + MatrixHistogramData, + MatrixHistogramParseData, + DnsHistogramSubBucket, +} from '../../../../../../common/search_strategy/security_solution/matrix_histogram'; + +export const getDnsParsedData = ( + data: MatrixHistogramParseData, + keyBucket: string +): MatrixHistogramData[] => { + let result: MatrixHistogramData[] = []; + data.forEach((bucketData: unknown) => { + const time = get('key', bucketData); + const histData = getOr([], keyBucket, bucketData).map( + // eslint-disable-next-line @typescript-eslint/naming-convention + ({ key, doc_count }: DnsHistogramSubBucket) => ({ + x: time, + y: doc_count, + g: key, + }) + ); + result = [...result, ...histData]; + }); + return result; +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/dns/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/dns/index.ts new file mode 100644 index 0000000000000..557e2ebf759e6 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/dns/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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { buildDnsHistogramQuery } from './query.dns_histogram.dsl'; +import { getDnsParsedData } from './helpers'; + +export const dnsMatrixHistogramConfig = { + buildDsl: buildDnsHistogramQuery, + aggName: 'aggregations.NetworkDns.buckets', + parseKey: 'dns.buckets', + parser: getDnsParsedData, +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/dns/query.dns_histogram.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/dns/query.dns_histogram.dsl.ts new file mode 100644 index 0000000000000..08a080865dfc0 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/dns/query.dns_histogram.dsl.ts @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + createQueryFilterClauses, + calculateTimeSeriesInterval, +} from '../../../../../utils/build_query'; +import { MatrixHistogramRequestOptions } from '../../../../../../common/search_strategy/security_solution/matrix_histogram'; + +export const buildDnsHistogramQuery = ({ + filterQuery, + timerange: { from, to }, + defaultIndex, + stackByField, +}: MatrixHistogramRequestOptions) => { + const filter = [ + ...createQueryFilterClauses(filterQuery), + { + range: { + '@timestamp': { + gte: from, + lte: to, + format: 'strict_date_optional_time', + }, + }, + }, + ]; + + const getHistogramAggregation = () => { + const interval = calculateTimeSeriesInterval(from, to); + const histogramTimestampField = '@timestamp'; + const dateHistogram = { + date_histogram: { + field: histogramTimestampField, + fixed_interval: interval, + }, + }; + + return { + NetworkDns: { + ...dateHistogram, + aggs: { + dns: { + terms: { + field: stackByField, + order: { + orderAgg: 'desc', + }, + size: 10, + }, + aggs: { + orderAgg: { + cardinality: { + field: 'dns.question.name', + }, + }, + }, + }, + }, + }, + }; + }; + + const dslQuery = { + index: defaultIndex, + allowNoIndices: true, + ignoreUnavailable: true, + body: { + aggregations: getHistogramAggregation(), + query: { + bool: { + filter, + }, + }, + size: 0, + track_total_hits: true, + }, + }; + + return dslQuery; +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/events/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/events/index.ts new file mode 100644 index 0000000000000..051436ee6c691 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/events/index.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { buildEventsHistogramQuery } from './query.events_histogram.dsl'; + +export const eventsMatrixHistogramConfig = { + buildDsl: buildEventsHistogramQuery, + aggName: 'aggregations.eventActionGroup.buckets', + parseKey: 'events.buckets', +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/events/query.events_histogram.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/events/query.events_histogram.dsl.ts new file mode 100644 index 0000000000000..d3b85872c5f06 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/events/query.events_histogram.dsl.ts @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import moment from 'moment'; + +import { showAllOthersBucket } from '../../../../../../common/constants'; +import { + createQueryFilterClauses, + calculateTimeSeriesInterval, +} from '../../../../../utils/build_query'; +import { MatrixHistogramRequestOptions } from '../../../../../../common/search_strategy/security_solution/matrix_histogram'; +import * as i18n from './translations'; + +export const buildEventsHistogramQuery = ({ + filterQuery, + timerange: { from, to }, + defaultIndex, + stackByField = 'event.action', +}: MatrixHistogramRequestOptions) => { + const filter = [ + ...createQueryFilterClauses(filterQuery), + { + range: { + '@timestamp': { + gte: from, + lte: to, + format: 'strict_date_optional_time', + }, + }, + }, + ]; + + const getHistogramAggregation = () => { + const interval = calculateTimeSeriesInterval(from, to); + const histogramTimestampField = '@timestamp'; + const dateHistogram = { + date_histogram: { + field: histogramTimestampField, + fixed_interval: interval, + min_doc_count: 0, + extended_bounds: { + min: moment(from).valueOf(), + max: moment(to).valueOf(), + }, + }, + }; + + const missing = + stackByField != null && showAllOthersBucket.includes(stackByField) + ? { + missing: stackByField?.endsWith('.ip') ? '0.0.0.0' : i18n.ALL_OTHERS, + } + : {}; + + return { + eventActionGroup: { + terms: { + field: stackByField, + ...missing, + order: { + _count: 'desc', + }, + size: 10, + }, + aggs: { + events: dateHistogram, + }, + }, + }; + }; + + const dslQuery = { + index: defaultIndex, + allowNoIndices: true, + ignoreUnavailable: true, + body: { + aggregations: getHistogramAggregation(), + query: { + bool: { + filter, + }, + }, + size: 0, + track_total_hits: true, + }, + }; + + return dslQuery; +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/events/translations.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/events/translations.ts new file mode 100644 index 0000000000000..0e46f5cff1445 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/events/translations.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const ALL_OTHERS = i18n.translate( + 'xpack.securitySolution.detectionEngine.alerts.histogram.allOthersGroupingLabel', + { + defaultMessage: 'All others', + } +); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/helpers.ts new file mode 100644 index 0000000000000..f306518fc3350 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/helpers.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { get, getOr } from 'lodash/fp'; +import { + MatrixHistogramParseData, + MatrixHistogramBucket, + MatrixHistogramData, +} from '../../../../../common/search_strategy/security_solution/matrix_histogram'; + +export const getGenericData = ( + data: MatrixHistogramParseData, + keyBucket: string +): MatrixHistogramData[] => { + let result: MatrixHistogramData[] = []; + data.forEach((bucketData: unknown) => { + const group = get('key', bucketData); + const histData = getOr([], keyBucket, bucketData).map( + // eslint-disable-next-line @typescript-eslint/naming-convention + ({ key, doc_count }: MatrixHistogramBucket) => ({ + x: key, + y: doc_count, + g: group, + }) + ); + result = [...result, ...histData]; + }); + + return result; +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/index.ts new file mode 100644 index 0000000000000..9cee2c0f1dc43 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/index.ts @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getOr } from 'lodash/fp'; + +import { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common'; +import { + FactoryQueryTypes, + MatrixHistogramRequestOptions, + MatrixHistogramStrategyResponse, + MatrixHistogramQuery, + MatrixHistogramType, + MatrixHistogramDataConfig, +} from '../../../../../common/search_strategy/security_solution'; +import { inspectStringifyObject } from '../../../../utils/build_query'; +import { SecuritySolutionFactory } from '../types'; +import { getGenericData } from './helpers'; +import { alertsMatrixHistogramConfig } from './alerts'; +import { anomaliesMatrixHistogramConfig } from './anomalies'; +import { authenticationsMatrixHistogramConfig } from './authentications'; +import { dnsMatrixHistogramConfig } from './dns'; +import { eventsMatrixHistogramConfig } from './events'; + +const matrixHistogramConfig: MatrixHistogramDataConfig = { + [MatrixHistogramType.alerts]: alertsMatrixHistogramConfig, + [MatrixHistogramType.anomalies]: anomaliesMatrixHistogramConfig, + [MatrixHistogramType.authentications]: authenticationsMatrixHistogramConfig, + [MatrixHistogramType.dns]: dnsMatrixHistogramConfig, + [MatrixHistogramType.events]: eventsMatrixHistogramConfig, +}; + +export const matrixHistogram: SecuritySolutionFactory = { + buildDsl: (options: MatrixHistogramRequestOptions) => { + const myConfig = getOr(null, options.histogramType, matrixHistogramConfig); + if (myConfig == null) { + throw new Error(`This histogram type ${options.histogramType} is unknown to the server side`); + } + return myConfig.buildDsl(options); + }, + parse: async ( + options: MatrixHistogramRequestOptions, + response: IEsSearchResponse + ): Promise => { + const myConfig = getOr(null, options.histogramType, matrixHistogramConfig); + if (myConfig == null) { + throw new Error(`This histogram type ${options.histogramType} is unknown to the server side`); + } + const totalCount = getOr(0, 'hits.total.value', response.rawResponse); + const matrixHistogramData = getOr([], myConfig.aggName, response.rawResponse); + const inspect = { + dsl: [inspectStringifyObject(myConfig.buildDsl(options))], + }; + const dataParser = myConfig.parser ?? getGenericData; + + return { + ...response, + inspect, + matrixHistogramData: dataParser( + matrixHistogramData, + myConfig.parseKey + ), + totalCount, + }; + }, +}; + +export const matrixHistogramFactory: Record< + typeof MatrixHistogramQuery, + SecuritySolutionFactory +> = { + [MatrixHistogramQuery]: matrixHistogram, +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/dns/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/dns/helpers.ts new file mode 100644 index 0000000000000..aa242e6ece7bf --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/dns/helpers.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { get, getOr } from 'lodash/fp'; + +import { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; +import { + NetworkDnsBuckets, + NetworkDnsEdges, +} from '../../../../../../common/search_strategy/security_solution/network'; + +export const getDnsEdges = (response: IEsSearchResponse): NetworkDnsEdges[] => + formatDnsEdges(getOr([], `aggregations.dns_name_query_count.buckets`, response.rawResponse)); + +export const formatDnsEdges = (buckets: NetworkDnsBuckets[]): NetworkDnsEdges[] => + buckets.map((bucket: NetworkDnsBuckets) => ({ + node: { + _id: bucket.key, + dnsBytesIn: getOrNumber('dns_bytes_in.value', bucket), + dnsBytesOut: getOrNumber('dns_bytes_out.value', bucket), + dnsName: bucket.key, + queryCount: bucket.doc_count, + uniqueDomains: getOrNumber('unique_domains.value', bucket), + }, + cursor: { + value: bucket.key, + tiebreaker: null, + }, + })); + +const getOrNumber = (path: string, bucket: NetworkDnsBuckets) => { + const numb = get(path, bucket); + if (numb == null) { + return null; + } + return numb; +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/dns/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/dns/index.ts new file mode 100644 index 0000000000000..8e734ca9d1179 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/dns/index.ts @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getOr } from 'lodash/fp'; + +import { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; + +import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../../../common/constants'; +import { + NetworkDnsStrategyResponse, + NetworkQueries, + NetworkDnsRequestOptions, + NetworkDnsEdges, +} from '../../../../../../common/search_strategy/security_solution/network'; + +import { inspectStringifyObject } from '../../../../../utils/build_query'; +import { SecuritySolutionFactory } from '../../types'; + +import { getDnsEdges } from './helpers'; +import { buildDnsQuery } from './query.dns_network.dsl'; + +export const networkDns: SecuritySolutionFactory = { + buildDsl: (options: NetworkDnsRequestOptions) => { + if (options.pagination && options.pagination.querySize >= DEFAULT_MAX_TABLE_QUERY_SIZE) { + throw new Error(`No query size above ${DEFAULT_MAX_TABLE_QUERY_SIZE}`); + } + return buildDnsQuery(options); + }, + parse: async ( + options: NetworkDnsRequestOptions, + response: IEsSearchResponse + ): Promise => { + const { activePage, cursorStart, fakePossibleCount, querySize } = options.pagination; + const totalCount = getOr(0, 'aggregations.dns_count.value', response.rawResponse); + const networkDnsEdges: NetworkDnsEdges[] = getDnsEdges(response); + const fakeTotalCount = fakePossibleCount <= totalCount ? fakePossibleCount : totalCount; + const edges = networkDnsEdges.splice(cursorStart, querySize - cursorStart); + const inspect = { + dsl: [inspectStringifyObject(buildDnsQuery(options))], + }; + const showMorePagesIndicator = totalCount > fakeTotalCount; + + return { + ...response, + edges, + inspect, + pageInfo: { + activePage: activePage ? activePage : 0, + fakeTotalCount, + showMorePagesIndicator, + }, + totalCount, + }; + }, +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/dns/query.dns_network.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/dns/query.dns_network.dsl.ts new file mode 100644 index 0000000000000..85b9051189bfe --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/dns/query.dns_network.dsl.ts @@ -0,0 +1,134 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isEmpty } from 'lodash/fp'; + +import { assertUnreachable } from '../../../../../../common/utility_types'; +import { + Direction, + SortField, + NetworkDnsRequestOptions, + NetworkDnsFields, +} from '../../../../../../common/search_strategy'; +import { createQueryFilterClauses } from '../../../../../utils/build_query'; + +type QueryOrder = + | { _count: Direction } + | { _key: Direction } + | { unique_domains: Direction } + | { dns_bytes_in: Direction } + | { dns_bytes_out: Direction }; + +const getQueryOrder = (sort: SortField): QueryOrder => { + switch (sort.field) { + case NetworkDnsFields.queryCount: + return { _count: sort.direction }; + case NetworkDnsFields.dnsName: + return { _key: sort.direction }; + case NetworkDnsFields.uniqueDomains: + return { unique_domains: sort.direction }; + case NetworkDnsFields.dnsBytesIn: + return { dns_bytes_in: sort.direction }; + case NetworkDnsFields.dnsBytesOut: + return { dns_bytes_out: sort.direction }; + } + assertUnreachable(sort.field); +}; + +const getCountAgg = () => ({ + dns_count: { + cardinality: { + field: 'dns.question.registered_domain', + }, + }, +}); + +const createIncludePTRFilter = (isPtrIncluded: boolean) => + isPtrIncluded + ? {} + : { + must_not: [ + { + term: { + 'dns.question.type': { + value: 'PTR', + }, + }, + }, + ], + }; + +export const buildDnsQuery = ({ + defaultIndex, + docValueFields, + filterQuery, + isPtrIncluded, + sort, + pagination: { querySize }, + stackByField = 'dns.question.registered_domain', + timerange: { from, to }, +}: NetworkDnsRequestOptions) => { + const filter = [ + ...createQueryFilterClauses(filterQuery), + { + range: { + '@timestamp': { + gte: from, + lte: to, + format: 'strict_date_optional_time', + }, + }, + }, + ]; + + const dslQuery = { + allowNoIndices: true, + index: defaultIndex, + ignoreUnavailable: true, + body: { + ...(isEmpty(docValueFields) ? { docvalue_fields: docValueFields } : {}), + aggregations: { + ...getCountAgg(), + dns_name_query_count: { + terms: { + field: stackByField, + size: querySize, + order: { + ...getQueryOrder(sort), + }, + }, + aggs: { + unique_domains: { + cardinality: { + field: 'dns.question.name', + }, + }, + dns_bytes_in: { + sum: { + field: 'source.bytes', + }, + }, + dns_bytes_out: { + sum: { + field: 'destination.bytes', + }, + }, + }, + }, + }, + query: { + bool: { + filter, + ...createIncludePTRFilter(isPtrIncluded), + }, + }, + }, + size: 0, + track_total_hits: false, + }; + + return dslQuery; +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/index.ts index c5c98e5facbdf..f2b4f85174797 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/index.ts @@ -10,12 +10,14 @@ import { } from '../../../../../common/search_strategy/security_solution'; import { SecuritySolutionFactory } from '../types'; +import { networkDns } from './dns'; import { networkHttp } from './http'; import { networkTls } from './tls'; import { networkTopCountries } from './top_countries'; import { networkTopNFlow } from './top_n_flow'; export const networkFactory: Record> = { + [NetworkQueries.dns]: networkDns, [NetworkQueries.http]: networkHttp, [NetworkQueries.tls]: networkTls, [NetworkQueries.topCountries]: networkTopCountries, diff --git a/x-pack/test/security_solution_cypress/cli_config.ts b/x-pack/test/security_solution_cypress/cli_config.ts new file mode 100644 index 0000000000000..f80066c53c95d --- /dev/null +++ b/x-pack/test/security_solution_cypress/cli_config.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; + +import { SecuritySolutionCypressCliTestRunner } from './runner'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const securitySolutionCypressConfig = await readConfigFile(require.resolve('./config.ts')); + return { + ...securitySolutionCypressConfig.getAll(), + + testRunner: SecuritySolutionCypressCliTestRunner, + }; +} diff --git a/x-pack/test/security_solution_cypress/config.ts b/x-pack/test/security_solution_cypress/config.ts index 83290a60a17a6..3a524467f7451 100644 --- a/x-pack/test/security_solution_cypress/config.ts +++ b/x-pack/test/security_solution_cypress/config.ts @@ -10,8 +10,6 @@ import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; import { CA_CERT_PATH } from '@kbn/dev-utils'; -import { SiemCypressTestRunner } from './runner'; - export default async function ({ readConfigFile }: FtrConfigProviderContext) { const kibanaCommonTestsConfig = await readConfigFile( require.resolve('../../../test/common/config.js') @@ -23,8 +21,6 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { return { ...kibanaCommonTestsConfig.getAll(), - testRunner: SiemCypressTestRunner, - esArchiver: { directory: resolve(__dirname, 'es_archives'), }, diff --git a/x-pack/test/security_solution_cypress/runner.ts b/x-pack/test/security_solution_cypress/runner.ts index 11c960389e25f..ccdc2fa4424ac 100644 --- a/x-pack/test/security_solution_cypress/runner.ts +++ b/x-pack/test/security_solution_cypress/runner.ts @@ -11,7 +11,7 @@ import { withProcRunner } from '@kbn/dev-utils'; import { FtrProviderContext } from './ftr_provider_context'; -export async function SiemCypressTestRunner({ getService }: FtrProviderContext) { +export async function SecuritySolutionCypressCliTestRunner({ getService }: FtrProviderContext) { const log = getService('log'); const config = getService('config'); const esArchiver = getService('esArchiver'); @@ -37,3 +37,30 @@ export async function SiemCypressTestRunner({ getService }: FtrProviderContext) }); }); } + +export async function SecuritySolutionCypressVisualTestRunner({ getService }: FtrProviderContext) { + const log = getService('log'); + const config = getService('config'); + const esArchiver = getService('esArchiver'); + + await esArchiver.load('empty_kibana'); + await esArchiver.load('auditbeat'); + + await withProcRunner(log, async (procs) => { + await procs.run('cypress', { + cmd: 'yarn', + args: ['cypress:open'], + cwd: resolve(__dirname, '../../plugins/security_solution'), + env: { + FORCE_COLOR: '1', + // eslint-disable-next-line @typescript-eslint/naming-convention + CYPRESS_baseUrl: Url.format(config.get('servers.kibana')), + CYPRESS_ELASTICSEARCH_URL: Url.format(config.get('servers.elasticsearch')), + CYPRESS_ELASTICSEARCH_USERNAME: config.get('servers.elasticsearch.username'), + CYPRESS_ELASTICSEARCH_PASSWORD: config.get('servers.elasticsearch.password'), + ...process.env, + }, + wait: true, + }); + }); +} diff --git a/x-pack/test/security_solution_cypress/visual_config.ts b/x-pack/test/security_solution_cypress/visual_config.ts new file mode 100644 index 0000000000000..a278e9d0a3443 --- /dev/null +++ b/x-pack/test/security_solution_cypress/visual_config.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; + +import { SecuritySolutionCypressVisualTestRunner } from './runner'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const securitySolutionCypressConfig = await readConfigFile(require.resolve('./config.ts')); + return { + ...securitySolutionCypressConfig.getAll(), + + testRunner: SecuritySolutionCypressVisualTestRunner, + }; +}