diff --git a/.ci/es-snapshots/Jenkinsfile_verify_es b/.ci/es-snapshots/Jenkinsfile_verify_es index 6f46650a2c635..d56ec61314ac7 100644 --- a/.ci/es-snapshots/Jenkinsfile_verify_es +++ b/.ci/es-snapshots/Jenkinsfile_verify_es @@ -42,7 +42,7 @@ kibanaPipeline(timeoutMinutes: 210) { } task { - kibanaPipeline.buildXpack(10) + kibanaPipeline.buildXpack(10, true) tasks.xpackCiGroups() tasks.xpackCiGroupDocker() } diff --git a/.eslintrc.js b/.eslintrc.js index 211aed1da7279..20875a2c2913d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1256,6 +1256,22 @@ module.exports = { }, }, + /** + * Discover overrides + */ + { + files: ['src/plugins/discover/**/*.{ts,tsx}'], + rules: { + '@typescript-eslint/no-explicit-any': 'error', + '@typescript-eslint/ban-ts-comment': [ + 'error', + { + 'ts-expect-error': false, + }, + ], + }, + }, + /** * Enterprise Search overrides * NOTE: We also have a single rule at the bottom of the file that diff --git a/api_docs/usage_collection.json b/api_docs/usage_collection.json index a10e2e73f0098..440187d1083b0 100644 --- a/api_docs/usage_collection.json +++ b/api_docs/usage_collection.json @@ -14,7 +14,13 @@ "isRequired": true, "signature": [ "React.PropsWithChildren<", - "TrackApplicationViewProps", + { + "pluginId": "usageCollection", + "scope": "public", + "docId": "kibUsageCollectionPluginApi", + "section": "def-public.TrackApplicationViewProps", + "text": "TrackApplicationViewProps" + }, ">" ], "description": [], @@ -26,7 +32,13 @@ ], "signature": [ "(props: React.PropsWithChildren<", - "TrackApplicationViewProps", + { + "pluginId": "usageCollection", + "scope": "public", + "docId": "kibUsageCollectionPluginApi", + "section": "def-public.TrackApplicationViewProps", + "text": "TrackApplicationViewProps" + }, ">) => JSX.Element" ], "description": [ @@ -38,38 +50,75 @@ "lineNumber": 23 }, "tags": [ - "Link", "constructor" ], "returnComment": [], "initialIsOpen": false } ], - "interfaces": [], - "enums": [ + "interfaces": [ { - "id": "def-public.METRIC_TYPE", - "type": "Enum", - "label": "METRIC_TYPE", - "tags": [], - "description": [], + "id": "def-public.TrackApplicationViewProps", + "type": "Interface", + "label": "TrackApplicationViewProps", + "description": [ + "\nProps to provide to the {@link TrackApplicationView} component." + ], + "tags": [ + "public" + ], + "children": [ + { + "tags": [ + "public" + ], + "id": "def-public.TrackApplicationViewProps.viewId", + "type": "string", + "label": "viewId", + "description": [ + "\nThe name of the view to be tracked. The appId will be obtained automatically." + ], + "source": { + "path": "src/plugins/usage_collection/public/components/track_application_view/types.ts", + "lineNumber": 20 + } + }, + { + "tags": [ + "public" + ], + "id": "def-public.TrackApplicationViewProps.children", + "type": "CompoundType", + "label": "children", + "description": [ + "\nThe React component to be tracked." + ], + "source": { + "path": "src/plugins/usage_collection/public/components/track_application_view/types.ts", + "lineNumber": 25 + }, + "signature": [ + "React.ReactNode" + ] + } + ], "source": { - "path": "node_modules/@kbn/analytics/target/types/metrics/index.d.ts", - "lineNumber": 10 + "path": "src/plugins/usage_collection/public/components/track_application_view/types.ts", + "lineNumber": 15 }, - "signature": [ - "METRIC_TYPE" - ], "initialIsOpen": false } ], + "enums": [], "misc": [], "objects": [], "setup": { "id": "def-public.UsageCollectionSetup", "type": "Interface", "label": "UsageCollectionSetup", - "description": [], + "description": [ + "Public's setup APIs exposed by the UsageCollection Service" + ], "tags": [], "children": [ { @@ -77,10 +126,12 @@ "id": "def-public.UsageCollectionSetup.components", "type": "Object", "label": "components", - "description": [], + "description": [ + "Component helpers to track usage collection in the UI" + ], "source": { "path": "src/plugins/usage_collection/public/plugin.tsx", - "lineNumber": 35 + "lineNumber": 38 }, "signature": [ "{ ApplicationUsageTrackingProvider: React.FC<{}>; }" @@ -91,10 +142,12 @@ "id": "def-public.UsageCollectionSetup.reportUiCounter", "type": "Function", "label": "reportUiCounter", - "description": [], + "description": [ + "Report whenever a UI event occurs for UI counters to report it" + ], "source": { "path": "src/plugins/usage_collection/public/plugin.tsx", - "lineNumber": 38 + "lineNumber": 75 }, "signature": [ "(appName: string, type: ", @@ -105,7 +158,7 @@ ], "source": { "path": "src/plugins/usage_collection/public/plugin.tsx", - "lineNumber": 34 + "lineNumber": 36 }, "lifecycle": "setup", "initialIsOpen": true @@ -114,7 +167,9 @@ "id": "def-public.UsageCollectionStart", "type": "Interface", "label": "UsageCollectionStart", - "description": [], + "description": [ + "Public's start APIs exposed by the UsageCollection Service" + ], "tags": [], "children": [ { @@ -122,10 +177,12 @@ "id": "def-public.UsageCollectionStart.reportUiCounter", "type": "Function", "label": "reportUiCounter", - "description": [], + "description": [ + "Report whenever a UI event occurs for UI counters to report it" + ], "source": { "path": "src/plugins/usage_collection/public/plugin.tsx", - "lineNumber": 42 + "lineNumber": 86 }, "signature": [ "(appName: string, type: ", @@ -136,186 +193,310 @@ ], "source": { "path": "src/plugins/usage_collection/public/plugin.tsx", - "lineNumber": 41 + "lineNumber": 84 }, "lifecycle": "start", "initialIsOpen": true } }, "server": { - "classes": [ + "classes": [], + "functions": [], + "interfaces": [ { - "id": "def-server.Collector", - "type": "Class", - "tags": [], - "label": "Collector", - "description": [], + "id": "def-server.ICollector", + "type": "Interface", + "label": "ICollector", "signature": [ { "pluginId": "usageCollection", "scope": "server", "docId": "kibUsageCollectionPluginApi", - "section": "def-server.Collector", - "text": "Collector" + "section": "def-server.ICollector", + "text": "ICollector" }, "" ], + "description": [ + "\nCommon interface for Usage and Stats Collectors" + ], + "tags": [], "children": [ { "tags": [], - "id": "def-server.Collector.extendFetchContext", + "id": "def-server.ICollector.log", + "type": "Object", + "label": "log", + "description": [ + "Logger" + ], + "source": { + "path": "src/plugins/usage_collection/server/collector/types.ts", + "lineNumber": 173 + }, + "signature": [ + "Logger" + ] + }, + { + "tags": [], + "id": "def-server.ICollector.extendFetchContext", "type": "CompoundType", "label": "extendFetchContext", - "description": [], + "description": [ + "\nThe options to extend the context provided to the `fetch` method: {@link CollectorOptionsFetchExtendedContext}." + ], "source": { - "path": "src/plugins/usage_collection/server/collector/collector.ts", - "lineNumber": 144 + "path": "src/plugins/usage_collection/server/collector/types.ts", + "lineNumber": 178 }, "signature": [ - "CollectorOptionsFetchExtendedContext", - "" + { + "pluginId": "usageCollection", + "scope": "server", + "docId": "kibUsageCollectionPluginApi", + "section": "def-server.CollectorOptionsFetchExtendedContext", + "text": "CollectorOptionsFetchExtendedContext" + }, + "" ] }, { "tags": [], - "id": "def-server.Collector.type", + "id": "def-server.ICollector.type", "type": "string", "label": "type", - "description": [], + "description": [ + "The registered type (aka name) of the collector" + ], "source": { - "path": "src/plugins/usage_collection/server/collector/collector.ts", - "lineNumber": 145 + "path": "src/plugins/usage_collection/server/collector/types.ts", + "lineNumber": 180 } }, { "tags": [], - "id": "def-server.Collector.init", - "type": "Object", - "label": "init", - "description": [], + "id": "def-server.ICollector.fetch", + "type": "Function", + "label": "fetch", + "description": [ + "\nThe actual logic that reports the Usage collection.\nIt will be called on every collection request.\nWhatever is returned in this method will be passed through as-is under\nthe {@link ICollector.type} key.\n" + ], "source": { - "path": "src/plugins/usage_collection/server/collector/collector.ts", - "lineNumber": 146 + "path": "src/plugins/usage_collection/server/collector/types.ts", + "lineNumber": 192 }, "signature": [ - "Function | undefined" + { + "pluginId": "usageCollection", + "scope": "server", + "docId": "kibUsageCollectionPluginApi", + "section": "def-server.CollectorFetchMethod", + "text": "CollectorFetchMethod" + }, + "" ] }, { "tags": [], - "id": "def-server.Collector.fetch", + "id": "def-server.ICollector.isReady", "type": "Function", - "label": "fetch", - "description": [], + "label": "isReady", + "description": [ + "\nShould return `true` when it's safe to call the `fetch` method." + ], "source": { - "path": "src/plugins/usage_collection/server/collector/collector.ts", - "lineNumber": 147 + "path": "src/plugins/usage_collection/server/collector/types.ts", + "lineNumber": 196 }, "signature": [ - "CollectorFetchMethod", - "" + "() => boolean | Promise" ] + } + ], + "source": { + "path": "src/plugins/usage_collection/server/collector/types.ts", + "lineNumber": 171 + }, + "initialIsOpen": false + }, + { + "id": "def-server.IncrementCounterParams", + "type": "Interface", + "label": "IncrementCounterParams", + "description": [ + "\nDetails about the counter to be incremented" + ], + "tags": [], + "children": [ + { + "tags": [], + "id": "def-server.IncrementCounterParams.counterName", + "type": "string", + "label": "counterName", + "description": [ + "The name of the counter" + ], + "source": { + "path": "src/plugins/usage_collection/server/usage_counters/usage_counter.ts", + "lineNumber": 28 + } }, { "tags": [], - "id": "def-server.Collector.isReady", - "type": "Function", - "label": "isReady", - "description": [], + "id": "def-server.IncrementCounterParams.counterType", + "type": "string", + "label": "counterType", + "description": [ + "The counter type (\"count\" by default)" + ], "source": { - "path": "src/plugins/usage_collection/server/collector/collector.ts", - "lineNumber": 148 + "path": "src/plugins/usage_collection/server/usage_counters/usage_counter.ts", + "lineNumber": 30 }, "signature": [ - "() => boolean | Promise" + "string | undefined" ] }, { - "id": "def-server.Collector.Unnamed", - "type": "Function", - "label": "Constructor", + "tags": [], + "id": "def-server.IncrementCounterParams.incrementBy", + "type": "number", + "label": "incrementBy", + "description": [ + "Increment the counter by this number (1 if not specified)" + ], + "source": { + "path": "src/plugins/usage_collection/server/usage_counters/usage_counter.ts", + "lineNumber": 32 + }, "signature": [ - "any" + "number | undefined" + ] + } + ], + "source": { + "path": "src/plugins/usage_collection/server/usage_counters/usage_counter.ts", + "lineNumber": 26 + }, + "initialIsOpen": false + }, + { + "id": "def-server.IUsageCounter", + "type": "Interface", + "label": "IUsageCounter", + "description": [ + "\nUsage Counter allows to keep track of any events that occur.\nBy calling {@link IUsageCounter.incrementCounter} devs can notify this\nAPI whenever the event happens." + ], + "tags": [], + "children": [ + { + "tags": [], + "id": "def-server.IUsageCounter.incrementCounter", + "type": "Function", + "label": "incrementCounter", + "description": [ + "\nNotifies the counter about a new event happening so it can increase the count internally." ], - "description": [], - "children": [ + "source": { + "path": "src/plugins/usage_collection/server/usage_counters/usage_counter.ts", + "lineNumber": 45 + }, + "signature": [ + "(params: ", { - "id": "def-server.Collector.Unnamed.$1", - "type": "Object", - "label": "log", - "isRequired": true, - "signature": [ - "Logger" - ], - "description": [], - "source": { - "path": "src/plugins/usage_collection/server/collector/collector.ts", - "lineNumber": 155 - } + "pluginId": "usageCollection", + "scope": "server", + "docId": "kibUsageCollectionPluginApi", + "section": "def-server.IncrementCounterParams", + "text": "IncrementCounterParams" }, - { - "id": "def-server.Collector.Unnamed.$2", - "type": "CompoundType", - "label": "{\n type,\n init,\n fetch,\n isReady,\n extendFetchContext = {},\n ...options\n }", - "isRequired": true, - "signature": [ - { - "pluginId": "usageCollection", - "scope": "server", - "docId": "kibUsageCollectionPluginApi", - "section": "def-server.CollectorOptions", - "text": "CollectorOptions" - }, - "" - ], - "description": [], - "source": { - "path": "src/plugins/usage_collection/server/collector/collector.ts", - "lineNumber": 156 - } - } - ], - "tags": [ - "private" - ], - "returnComment": [], - "source": { - "path": "src/plugins/usage_collection/server/collector/collector.ts", - "lineNumber": 154 - } + ") => void" + ] } ], "source": { - "path": "src/plugins/usage_collection/server/collector/collector.ts", - "lineNumber": 143 + "path": "src/plugins/usage_collection/server/usage_counters/usage_counter.ts", + "lineNumber": 40 }, "initialIsOpen": false - } - ], - "functions": [], - "interfaces": [ + }, { - "id": "def-server.SchemaField", + "id": "def-server.UsageCountersSavedObjectAttributes", "type": "Interface", - "label": "SchemaField", - "description": [], + "label": "UsageCountersSavedObjectAttributes", + "signature": [ + { + "pluginId": "usageCollection", + "scope": "server", + "docId": "kibUsageCollectionPluginApi", + "section": "def-server.UsageCountersSavedObjectAttributes", + "text": "UsageCountersSavedObjectAttributes" + }, + " extends ", + "SavedObjectAttributes" + ], + "description": [ + "\nThe attributes stored in the UsageCounters' SavedObjects" + ], "tags": [], "children": [ { "tags": [], - "id": "def-server.SchemaField.type", + "id": "def-server.UsageCountersSavedObjectAttributes.domainId", "type": "string", - "label": "type", - "description": [], + "label": "domainId", + "description": [ + "The domain ID registered in the Usage Counter" + ], + "source": { + "path": "src/plugins/usage_collection/server/usage_counters/saved_objects.ts", + "lineNumber": 23 + } + }, + { + "tags": [], + "id": "def-server.UsageCountersSavedObjectAttributes.counterName", + "type": "string", + "label": "counterName", + "description": [ + "The counter name" + ], + "source": { + "path": "src/plugins/usage_collection/server/usage_counters/saved_objects.ts", + "lineNumber": 25 + } + }, + { + "tags": [], + "id": "def-server.UsageCountersSavedObjectAttributes.counterType", + "type": "string", + "label": "counterType", + "description": [ + "The counter type" + ], + "source": { + "path": "src/plugins/usage_collection/server/usage_counters/saved_objects.ts", + "lineNumber": 27 + } + }, + { + "tags": [], + "id": "def-server.UsageCountersSavedObjectAttributes.count", + "type": "number", + "label": "count", + "description": [ + "Number of times the event has occurred" + ], "source": { - "path": "src/plugins/usage_collection/server/collector/collector.ts", - "lineNumber": 33 + "path": "src/plugins/usage_collection/server/usage_counters/saved_objects.ts", + "lineNumber": 29 } } ], "source": { - "path": "src/plugins/usage_collection/server/collector/collector.ts", - "lineNumber": 32 + "path": "src/plugins/usage_collection/server/usage_counters/saved_objects.ts", + "lineNumber": 21 }, "initialIsOpen": false } @@ -327,10 +508,12 @@ "type": "Type", "label": "AllowedSchemaTypes", "tags": [], - "description": [], + "description": [ + "\nPossible type values in the schema" + ], "source": { - "path": "src/plugins/usage_collection/server/collector/collector.ts", - "lineNumber": 27 + "path": "src/plugins/usage_collection/server/collector/types.ts", + "lineNumber": 33 }, "signature": [ "\"boolean\" | \"date\" | \"text\" | \"keyword\" | \"long\" | \"double\" | \"short\" | \"integer\" | \"byte\" | \"float\"" @@ -346,26 +529,70 @@ "\nThe context for the `fetch` method: It includes the most commonly used clients in the collectors (ES and SO clients).\nBoth are scoped based on the request and the context:\n- When users are requesting a sample of data, it is scoped to their role to avoid exposing data they shouldn't read\n- When building the telemetry data payload to report to the remote cluster, the requests are scoped to the `kibana` internal user\n" ], "source": { - "path": "src/plugins/usage_collection/server/collector/collector.ts", - "lineNumber": 64 + "path": "src/plugins/usage_collection/server/collector/types.ts", + "lineNumber": 76 }, "signature": [ "{ esClient: ElasticsearchClient; soClient: SavedObjectsClientContract; } & (WithKibanaRequest extends true ? { kibanaRequest?: KibanaRequest | undefined; } : {})" ], "initialIsOpen": false }, + { + "id": "def-server.CollectorFetchMethod", + "type": "Type", + "label": "CollectorFetchMethod", + "tags": [], + "description": [ + "\nThe fetch method has the context of the Collector itself\n(this has access to all the properties of the collector like the logger)\nand the the first parameter is {@link CollectorFetchContext}." + ], + "source": { + "path": "src/plugins/usage_collection/server/collector/types.ts", + "lineNumber": 104 + }, + "signature": [ + "(context: ", + { + "pluginId": "usageCollection", + "scope": "server", + "docId": "kibUsageCollectionPluginApi", + "section": "def-server.CollectorFetchContext", + "text": "CollectorFetchContext" + }, + ") => TReturn | Promise" + ], + "initialIsOpen": false + }, { "id": "def-server.CollectorOptions", "type": "Type", "label": "CollectorOptions", "tags": [], - "description": [], + "description": [ + "\nOptions to instantiate a collector" + ], "source": { - "path": "src/plugins/usage_collection/server/collector/collector.ts", - "lineNumber": 111 + "path": "src/plugins/usage_collection/server/collector/types.ts", + "lineNumber": 135 }, "signature": [ - "{ type: string; init?: Function | undefined; isReady: () => Promise | boolean; schema?: MakeSchemaFrom | undefined; fetch: CollectorFetchMethod; } & ExtraOptions & (WithKibanaRequest extends true ? { extendFetchContext: CollectorOptionsFetchExtendedContext; } : { extendFetchContext?: CollectorOptionsFetchExtendedContext | undefined; })" + "{ type: string; isReady: () => Promise | boolean; schema?: MakeSchemaFrom | undefined; fetch: CollectorFetchMethod; } & ExtraOptions & (WithKibanaRequest extends true ? { extendFetchContext: CollectorOptionsFetchExtendedContext; } : { extendFetchContext?: CollectorOptionsFetchExtendedContext | undefined; })" + ], + "initialIsOpen": false + }, + { + "id": "def-server.CollectorOptionsFetchExtendedContext", + "type": "Type", + "label": "CollectorOptionsFetchExtendedContext", + "tags": [], + "description": [ + "\nThe options to extend the context provided to the `fetch` method." + ], + "source": { + "path": "src/plugins/usage_collection/server/collector/types.ts", + "lineNumber": 125 + }, + "signature": [ + "ICollectorOptionsFetchExtendedContext & (WithKibanaRequest extends true ? Required, \"kibanaRequest\">> : {})" ], "initialIsOpen": false }, @@ -374,10 +601,12 @@ "type": "Type", "label": "MakeSchemaFrom", "tags": [], - "description": [], + "description": [ + "\nThe `schema` property in {@link CollectorOptions} must match the output of\nthe `fetch` method. This type helps ensure that is correct" + ], "source": { - "path": "src/plugins/usage_collection/server/collector/collector.ts", - "lineNumber": 50 + "path": "src/plugins/usage_collection/server/collector/types.ts", + "lineNumber": 61 }, "signature": [ "{ [Key in keyof Required]: Required[Key] extends (infer U)[] ? { type: 'array'; items: RecursiveMakeSchemaFrom; } : RecursiveMakeSchemaFrom[Key]>; }" @@ -385,84 +614,234 @@ "initialIsOpen": false }, { - "id": "def-server.UsageCollectionSetup", + "tags": [], + "id": "def-server.USAGE_COUNTERS_SAVED_OBJECT_TYPE", + "type": "string", + "label": "USAGE_COUNTERS_SAVED_OBJECT_TYPE", + "description": [ + "The Saved Objects type for Usage Counters" + ], + "source": { + "path": "src/plugins/usage_collection/server/usage_counters/saved_objects.ts", + "lineNumber": 38 + }, + "signature": [ + "\"usage-counters\"" + ], + "initialIsOpen": false + }, + { + "id": "def-server.UsageCollectorOptions", "type": "Type", - "label": "UsageCollectionSetup", + "label": "UsageCollectorOptions", "tags": [], - "description": [], + "description": [ + "\nSame as {@link CollectorOptions} but with the `schema` property enforced" + ], "source": { - "path": "src/plugins/usage_collection/server/plugin.ts", - "lineNumber": 21 + "path": "src/plugins/usage_collection/server/collector/usage_collector.ts", + "lineNumber": 16 }, "signature": [ - "{ makeStatsCollector: (options: ", + "{ type: string; isReady: () => boolean | Promise; schema?: ", { "pluginId": "usageCollection", "scope": "server", "docId": "kibUsageCollectionPluginApi", - "section": "def-server.CollectorOptions", - "text": "CollectorOptions" + "section": "def-server.MakeSchemaFrom", + "text": "MakeSchemaFrom" }, - ") => ", + " | undefined; fetch: ", { "pluginId": "usageCollection", "scope": "server", "docId": "kibUsageCollectionPluginApi", - "section": "def-server.Collector", - "text": "Collector" + "section": "def-server.CollectorFetchMethod", + "text": "CollectorFetchMethod" }, - "; makeUsageCollector: (options: ", + "; } & ExtraOptions & (WithKibanaRequest extends true ? { extendFetchContext: ", { "pluginId": "usageCollection", "scope": "server", "docId": "kibUsageCollectionPluginApi", - "section": "def-server.UsageCollectorOptions", - "text": "UsageCollectorOptions" + "section": "def-server.CollectorOptionsFetchExtendedContext", + "text": "CollectorOptionsFetchExtendedContext" }, - ") => ", - "UsageCollector", - "; registerCollector: (collector: ", + "; } : { extendFetchContext?: ", { "pluginId": "usageCollection", "scope": "server", "docId": "kibUsageCollectionPluginApi", - "section": "def-server.Collector", - "text": "Collector" - } + "section": "def-server.CollectorOptionsFetchExtendedContext", + "text": "CollectorOptionsFetchExtendedContext" + }, + " | undefined; }) & Required, \"schema\">>" ], "initialIsOpen": false }, { - "id": "def-server.UsageCollectorOptions", + "id": "def-server.UsageCountersSavedObject", "type": "Type", - "label": "UsageCollectorOptions", + "label": "UsageCountersSavedObject", "tags": [], - "description": [], + "description": [ + "\nThe structure of the SavedObjects of type \"usage-counters\"" + ], "source": { - "path": "src/plugins/usage_collection/server/collector/usage_collector.ts", - "lineNumber": 13 + "path": "src/plugins/usage_collection/server/usage_counters/saved_objects.ts", + "lineNumber": 35 }, "signature": [ - "{ type: string; init?: Function | undefined; isReady: () => boolean | Promise; schema?: ", - { - "pluginId": "usageCollection", - "scope": "server", - "docId": "kibUsageCollectionPluginApi", - "section": "def-server.MakeSchemaFrom", - "text": "MakeSchemaFrom" - }, - " | undefined; fetch: ", - "CollectorFetchMethod", - "; } & ExtraOptions & (WithKibanaRequest extends true ? { extendFetchContext: ", - "CollectorOptionsFetchExtendedContext", - "; } : { extendFetchContext?: ", - "CollectorOptionsFetchExtendedContext", - " | undefined; }) & Required, \"schema\">>" + "SavedObject" ], "initialIsOpen": false } ], - "objects": [] + "objects": [], + "setup": { + "id": "def-server.UsageCollectionSetup", + "type": "Interface", + "label": "UsageCollectionSetup", + "description": [ + "Server's setup APIs exposed by the UsageCollection Service" + ], + "tags": [], + "children": [ + { + "tags": [], + "id": "def-server.UsageCollectionSetup.createUsageCounter", + "type": "Function", + "label": "createUsageCounter", + "description": [ + "\nCreates and registers a usage counter to collect daily aggregated plugin counter events" + ], + "source": { + "path": "src/plugins/usage_collection/server/plugin.ts", + "lineNumber": 33 + }, + "signature": [ + "(type: string) => ", + { + "pluginId": "usageCollection", + "scope": "server", + "docId": "kibUsageCollectionPluginApi", + "section": "def-server.IUsageCounter", + "text": "IUsageCounter" + } + ] + }, + { + "tags": [], + "id": "def-server.UsageCollectionSetup.getUsageCounterByType", + "type": "Function", + "label": "getUsageCounterByType", + "description": [ + "\nReturns a usage counter by type" + ], + "source": { + "path": "src/plugins/usage_collection/server/plugin.ts", + "lineNumber": 37 + }, + "signature": [ + "(type: string) => ", + { + "pluginId": "usageCollection", + "scope": "server", + "docId": "kibUsageCollectionPluginApi", + "section": "def-server.IUsageCounter", + "text": "IUsageCounter" + }, + " | undefined" + ] + }, + { + "tags": [], + "id": "def-server.UsageCollectionSetup.makeUsageCollector", + "type": "Function", + "label": "makeUsageCollector", + "description": [ + "\nCreates a usage collector to collect plugin telemetry data.\nregisterCollector must be called to connect the created collector with the service." + ], + "source": { + "path": "src/plugins/usage_collection/server/plugin.ts", + "lineNumber": 42 + }, + "signature": [ + "(options: ", + { + "pluginId": "usageCollection", + "scope": "server", + "docId": "kibUsageCollectionPluginApi", + "section": "def-server.UsageCollectorOptions", + "text": "UsageCollectorOptions" + }, + ") => ", + { + "pluginId": "usageCollection", + "scope": "server", + "docId": "kibUsageCollectionPluginApi", + "section": "def-server.ICollector", + "text": "ICollector" + }, + "" + ] + }, + { + "tags": [], + "id": "def-server.UsageCollectionSetup.registerCollector", + "type": "Function", + "label": "registerCollector", + "description": [ + "\nRegister a usage collector or a stats collector.\nUsed to connect the created collector to telemetry." + ], + "source": { + "path": "src/plugins/usage_collection/server/plugin.ts", + "lineNumber": 53 + }, + "signature": [ + "(collector: ", + { + "pluginId": "usageCollection", + "scope": "server", + "docId": "kibUsageCollectionPluginApi", + "section": "def-server.ICollector", + "text": "ICollector" + }, + ") => void" + ] + }, + { + "tags": [], + "id": "def-server.UsageCollectionSetup.getCollectorByType", + "type": "Function", + "label": "getCollectorByType", + "description": [ + "\nReturns a usage collector by type" + ], + "source": { + "path": "src/plugins/usage_collection/server/plugin.ts", + "lineNumber": 59 + }, + "signature": [ + "(type: string) => ", + { + "pluginId": "usageCollection", + "scope": "server", + "docId": "kibUsageCollectionPluginApi", + "section": "def-server.ICollector", + "text": "ICollector" + }, + " | undefined" + ] + } + ], + "source": { + "path": "src/plugins/usage_collection/server/plugin.ts", + "lineNumber": 29 + }, + "lifecycle": "setup", + "initialIsOpen": true + } }, "common": { "classes": [], diff --git a/api_docs/usage_collection.mdx b/api_docs/usage_collection.mdx index 5f2801990d4b4..62db0389c2d5c 100644 --- a/api_docs/usage_collection.mdx +++ b/api_docs/usage_collection.mdx @@ -22,13 +22,13 @@ import usageCollectionObj from './usage_collection.json'; ### Functions -### Enums - +### Interfaces + ## Server -### Classes - +### Setup + ### Interfaces diff --git a/docs/developer/getting-started/monorepo-packages.asciidoc b/docs/developer/getting-started/monorepo-packages.asciidoc index 02f96f304ec1e..3fc006c0aae6b 100644 --- a/docs/developer/getting-started/monorepo-packages.asciidoc +++ b/docs/developer/getting-started/monorepo-packages.asciidoc @@ -69,6 +69,7 @@ yarn kbn watch-bazel - @kbn/apm-utils - @kbn/babel-code-parser - @kbn/babel-preset +- @kbn/config - @kbn/config-schema - @kbn/dev-utils - @kbn/eslint-import-resolver-kibana @@ -76,6 +77,9 @@ yarn kbn watch-bazel - @kbn/expect - @kbn/legacy-logging - @kbn/logging +- @kbn/securitysolution-constants +- @kbn/securitysolution-utils +- @kbn/securitysolution-io-ts-utils - @kbn/std - @kbn/tinymath - @kbn/utility-types diff --git a/docs/developer/plugin/migrating-legacy-plugins-examples.asciidoc b/docs/developer/plugin/migrating-legacy-plugins-examples.asciidoc index 6361b3c921128..acc42ec91bb71 100644 --- a/docs/developer/plugin/migrating-legacy-plugins-examples.asciidoc +++ b/docs/developer/plugin/migrating-legacy-plugins-examples.asciidoc @@ -522,7 +522,7 @@ platform, these were referred to as `hidden` applications and were set via the `hidden` property in a {kib} plugin. Chromeless applications are also not displayed in the left navbar. -To mark an application as chromeless, specify `chromeless: false` when +To mark an application as chromeless, specify `chromeless: true` when registering your application to hide the chrome UI when the application is mounted: @@ -813,7 +813,7 @@ been renamed: `SavedObjectsType.management.importableAndExportable`. ---- `(doc: SavedObjectUnsanitizedDoc, log: SavedObjectsMigrationLogger) => SavedObjectUnsanitizedDoc;` ---- -In {kib} Platform, it is +In {kib} Platform, it is [source,typescript] ---- `(doc: SavedObjectUnsanitizedDoc, context: SavedObjectMigrationContext) => SavedObjectUnsanitizedDoc;` @@ -1092,7 +1092,7 @@ the current user. [source,typescript] ---- -const request = client.asCurrentUser.ping({}, { +const request = client.asCurrentUser.ping({}, { headers: { authorization: 'foo', custom: 'bar', @@ -1143,9 +1143,9 @@ router.get( ==== Accessing the client from a collector's `fetch` method -At the moment, the `fetch` method's context receives preconfigured -<> for Elasticsearch and SavedObjects. -To help in the transition, both, the legacy (`callCluster`) and new clients are provided, +At the moment, the `fetch` method's context receives preconfigured +<> for Elasticsearch and SavedObjects. +To help in the transition, both, the legacy (`callCluster`) and new clients are provided, but we strongly discourage using the deprecated legacy ones for any new implementation. [source,typescript] @@ -1156,19 +1156,19 @@ usageCollection.makeUsageCollector({ schema: {...}, async fetch(context) { const { callCluster, esClient, soClient } = context; - + // Before: const result = callCluster('search', options) // After: const { body: result } = esClient.search(options); - + return result; } }); ---- -Regarding the `soClient`, it is encouraged to use it instead of the plugin's owned SavedObject's repository +Regarding the `soClient`, it is encouraged to use it instead of the plugin's owned SavedObject's repository as we used to do in the past. Before: @@ -1176,7 +1176,7 @@ Before: [source,typescript] ---- function getUsageCollector( - usageCollection: UsageCollectionSetup, + usageCollection: UsageCollectionSetup, getSavedObjectsRepository: () => ISavedObjectsRepository | undefined ) { usageCollection.makeUsageCollector({ @@ -1185,12 +1185,12 @@ function getUsageCollector( schema: {...}, async fetch() { const savedObjectsRepository = getSavedObjectsRepository(); - + const { attributes: result } = await savedObjectsRepository.get('my-so-type', 'my-so-id'); - + return result; } - }); + }); } ---- @@ -1205,10 +1205,10 @@ function getUsageCollector(usageCollection: UsageCollectionSetup) { schema: {...}, async fetch({ soClient }) { const { attributes: result } = await soClient.get('my-so-type', 'my-so-id'); - + return result; } - }); + }); } ---- diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md index 1830e8f140e60..180d376ceaf51 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md @@ -104,6 +104,9 @@ readonly links: { readonly painlessWalkthrough: string; readonly luceneExpressions: string; }; + readonly search: { + readonly sessions: string; + }; readonly indexPatterns: { readonly introduction: string; readonly fieldFormattersNumber: string; diff --git a/package.json b/package.json index 81727c4028610..af300b164ec18 100644 --- a/package.json +++ b/package.json @@ -88,7 +88,8 @@ "**/react-syntax-highlighter/**/highlight.js": "^10.4.1", "**/request": "^2.88.2", "**/trim": "0.0.3", - "**/typescript": "4.1.3" + "**/typescript": "4.1.3", + "**/underscore": "^1.13.1" }, "engines": { "node": "14.16.1", @@ -125,7 +126,7 @@ "@kbn/analytics": "link:bazel-bin/packages/kbn-analytics/npm_module", "@kbn/apm-config-loader": "link:bazel-bin/packages/kbn-apm-config-loader/npm_module", "@kbn/apm-utils": "link:bazel-bin/packages/kbn-apm-utils/npm_module", - "@kbn/config": "link:packages/kbn-config", + "@kbn/config": "link:bazel-bin/packages/kbn-config/npm_module", "@kbn/config-schema": "link:bazel-bin/packages/kbn-config-schema/npm_module", "@kbn/crypto": "link:packages/kbn-crypto", "@kbn/i18n": "link:packages/kbn-i18n", @@ -134,6 +135,9 @@ "@kbn/legacy-logging": "link:bazel-bin/packages/kbn-legacy-logging/npm_module", "@kbn/logging": "link:bazel-bin/packages/kbn-logging/npm_module", "@kbn/monaco": "link:packages/kbn-monaco", + "@kbn/securitysolution-constants": "link:bazel-bin/packages/kbn-securitysolution-constants/npm_module", + "@kbn/securitysolution-utils": "link:bazel-bin/packages/kbn-securitysolution-utils/npm_module", + "@kbn/securitysolution-io-ts-utils": "link:bazel-bin/packages/kbn-securitysolution-io-ts-utils/npm_module", "@kbn/server-http-tools": "link:packages/kbn-server-http-tools", "@kbn/server-route-repository": "link:packages/kbn-server-route-repository", "@kbn/std": "link:bazel-bin/packages/kbn-std/npm_module", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index eb93a1055e7b7..38374513502fb 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -11,6 +11,7 @@ filegroup( "//packages/kbn-apm-utils:build", "//packages/kbn-babel-code-parser:build", "//packages/kbn-babel-preset:build", + "//packages/kbn-config:build", "//packages/kbn-config-schema:build", "//packages/kbn-dev-utils:build", "//packages/kbn-eslint-import-resolver-kibana:build", @@ -18,6 +19,9 @@ filegroup( "//packages/kbn-expect:build", "//packages/kbn-legacy-logging:build", "//packages/kbn-logging:build", + "//packages/kbn-securitysolution-constants:build", + "//packages/kbn-securitysolution-io-ts-utils:build", + "//packages/kbn-securitysolution-utils:build", "//packages/kbn-std:build", "//packages/kbn-tinymath:build", "//packages/kbn-utility-types:build", diff --git a/packages/kbn-cli-dev-mode/package.json b/packages/kbn-cli-dev-mode/package.json index 9def59623c938..0401e6a82e11a 100644 --- a/packages/kbn-cli-dev-mode/package.json +++ b/packages/kbn-cli-dev-mode/package.json @@ -14,7 +14,6 @@ "devOnly": true }, "dependencies": { - "@kbn/config": "link:../kbn-config", "@kbn/server-http-tools": "link:../kbn-server-http-tools", "@kbn/optimizer": "link:../kbn-optimizer" } diff --git a/packages/kbn-config/BUILD.bazel b/packages/kbn-config/BUILD.bazel new file mode 100644 index 0000000000000..a079d9c8f6413 --- /dev/null +++ b/packages/kbn-config/BUILD.bazel @@ -0,0 +1,97 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") + +PKG_BASE_NAME = "kbn-config" +PKG_REQUIRE_NAME = "@kbn/config" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + ], + exclude = [ + "**/*.test.*", + "**/__fixtures__", + "**/__mocks__", + "**/__snapshots__" + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", + "README.md" +] + +SRC_DEPS = [ + "//packages/elastic-safer-lodash-set", + "//packages/kbn-config-schema", + "//packages/kbn-logging", + "//packages/kbn-std", + "//packages/kbn-utility-types", + "@npm//js-yaml", + "@npm//load-json-file", + "@npm//lodash", + "@npm//rxjs", + "@npm//type-detect", +] + +TYPES_DEPS = [ + "@npm//@types/jest", + "@npm//@types/js-yaml", + "@npm//@types/lodash", + "@npm//@types/node", + "@npm//@types/type-detect", +] + +DEPS = SRC_DEPS + TYPES_DEPS + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + ], +) + +ts_project( + name = "tsc", + args = ['--pretty'], + srcs = SRCS, + deps = DEPS, + declaration = True, + declaration_map = True, + incremental = True, + out_dir = "target", + source_map = True, + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_BASE_NAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = [":tsc"] + DEPS, + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [ + ":%s" % PKG_BASE_NAME, + ] +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-config/package.json b/packages/kbn-config/package.json index b114cb13933d1..56f1855bebbce 100644 --- a/packages/kbn-config/package.json +++ b/packages/kbn-config/package.json @@ -4,9 +4,5 @@ "types": "./target/index.d.ts", "version": "1.0.0", "license": "SSPL-1.0 OR Elastic License 2.0", - "private": true, - "scripts": { - "build": "../../node_modules/.bin/tsc", - "kbn:bootstrap": "yarn build" - } + "private": true } \ No newline at end of file diff --git a/packages/kbn-config/tsconfig.json b/packages/kbn-config/tsconfig.json index 4e1bf573f488a..115af2c46b549 100644 --- a/packages/kbn-config/tsconfig.json +++ b/packages/kbn-config/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "incremental": false, + "incremental": true, "outDir": "./target", "stripInternal": false, "declaration": true, diff --git a/packages/kbn-docs-utils/package.json b/packages/kbn-docs-utils/package.json index 6aca554f0f945..27d38d2d8ed4f 100644 --- a/packages/kbn-docs-utils/package.json +++ b/packages/kbn-docs-utils/package.json @@ -11,8 +11,5 @@ "scripts": { "kbn:bootstrap": "../../node_modules/.bin/tsc", "kbn:watch": "../../node_modules/.bin/tsc --watch" - }, - "dependencies": { - "@kbn/config": "link:../kbn-config" } } \ No newline at end of file diff --git a/packages/kbn-optimizer/package.json b/packages/kbn-optimizer/package.json index f193fcf898a3d..54fcdc3bb130f 100644 --- a/packages/kbn-optimizer/package.json +++ b/packages/kbn-optimizer/package.json @@ -11,7 +11,6 @@ "kbn:watch": "yarn build --watch" }, "dependencies": { - "@kbn/config": "link:../kbn-config", "@kbn/ui-shared-deps": "link:../kbn-ui-shared-deps" } } \ No newline at end of file diff --git a/packages/kbn-securitysolution-constants/BUILD.bazel b/packages/kbn-securitysolution-constants/BUILD.bazel new file mode 100644 index 0000000000000..8b2d8c9103f3e --- /dev/null +++ b/packages/kbn-securitysolution-constants/BUILD.bazel @@ -0,0 +1,84 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") + +PKG_BASE_NAME = "kbn-securitysolution-constants" + +PKG_REQUIRE_NAME = "@kbn/securitysolution-constants" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + ], + exclude = [ + "**/*.test.*", + "**/*.mock.*", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", + "README.md", +] + +SRC_DEPS = [ + "@npm//tslib", +] + +TYPES_DEPS = [ + "@npm//@types/jest", + "@npm//@types/node", +] + +DEPS = SRC_DEPS + TYPES_DEPS + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + ], +) + +ts_project( + name = "tsc", + srcs = SRCS, + args = ["--pretty"], + declaration = True, + declaration_map = True, + incremental = True, + out_dir = "target", + root_dir = "src", + source_map = True, + tsconfig = ":tsconfig", + deps = DEPS, +) + +js_library( + name = PKG_BASE_NAME, + package_name = PKG_REQUIRE_NAME, + srcs = NPM_MODULE_EXTRA_FILES, + visibility = ["//visibility:public"], + deps = [":tsc"] + DEPS, +) + +pkg_npm( + name = "npm_module", + deps = [ + ":%s" % PKG_BASE_NAME, + ], +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-securitysolution-constants/README.md b/packages/kbn-securitysolution-constants/README.md new file mode 100644 index 0000000000000..dd1ab8da6a2a8 --- /dev/null +++ b/packages/kbn-securitysolution-constants/README.md @@ -0,0 +1,6 @@ +# kbn-securitysolution-constants + +This is where shared constants for security solution should go that are going to be shared among plugins. +This was originally created to remove the dependencies between security_solution and other projects such as lists. + + diff --git a/packages/kbn-securitysolution-constants/jest.config.js b/packages/kbn-securitysolution-constants/jest.config.js new file mode 100644 index 0000000000000..f0bb13e39417c --- /dev/null +++ b/packages/kbn-securitysolution-constants/jest.config.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/kbn-securitysolution-constants'], +}; diff --git a/packages/kbn-securitysolution-constants/package.json b/packages/kbn-securitysolution-constants/package.json new file mode 100644 index 0000000000000..bb60f79aa03e5 --- /dev/null +++ b/packages/kbn-securitysolution-constants/package.json @@ -0,0 +1,9 @@ +{ + "name": "@kbn/securitysolution-constants", + "version": "1.0.0", + "description": "security solution constants to use across plugins such lists, security_solution, cases, etc...", + "license": "SSPL-1.0 OR Elastic License 2.0", + "main": "./target/index.js", + "types": "./target/index.d.ts", + "private": true +} diff --git a/packages/kbn-securitysolution-constants/src/index.ts b/packages/kbn-securitysolution-constants/src/index.ts new file mode 100644 index 0000000000000..06b741d761367 --- /dev/null +++ b/packages/kbn-securitysolution-constants/src/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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/** + * Value list routes + */ +export const LIST_URL = '/api/lists'; +export const LIST_INDEX = `${LIST_URL}/index`; +export const LIST_ITEM_URL = `${LIST_URL}/items`; +export const LIST_PRIVILEGES_URL = `${LIST_URL}/privileges`; + +/** + * Exception list routes + */ +export const EXCEPTION_LIST_URL = '/api/exception_lists'; +export const EXCEPTION_LIST_ITEM_URL = '/api/exception_lists/items'; + +/** + * Exception list spaces + */ +export const EXCEPTION_LIST_NAMESPACE_AGNOSTIC = 'exception-list-agnostic'; +export const EXCEPTION_LIST_NAMESPACE = 'exception-list'; + +/** + * Specific routes for the single global space agnostic endpoint list + */ +export const ENDPOINT_LIST_URL = '/api/endpoint_list'; + +/** + * Specific routes for the single global space agnostic endpoint list. These are convenience + * routes where they are going to try and create the global space agnostic endpoint list if it + * does not exist yet or if it was deleted at some point and re-create it before adding items to + * the list + */ +export const ENDPOINT_LIST_ITEM_URL = '/api/endpoint_list/items'; + +/** + * This ID is used for _both_ the Saved Object ID and for the list_id + * for the single global space agnostic endpoint list + */ +export const ENDPOINT_LIST_ID = 'endpoint_list'; + +/** The name of the single global space agnostic endpoint list */ +export const ENDPOINT_LIST_NAME = 'Endpoint Security Exception List'; + +/** The description of the single global space agnostic endpoint list */ +export const ENDPOINT_LIST_DESCRIPTION = 'Endpoint Security Exception List'; + +export const MAX_EXCEPTION_LIST_SIZE = 10000; + +/** ID of trusted apps agnostic list */ +export const ENDPOINT_TRUSTED_APPS_LIST_ID = 'endpoint_trusted_apps'; + +/** Name of trusted apps agnostic list */ +export const ENDPOINT_TRUSTED_APPS_LIST_NAME = 'Endpoint Security Trusted Apps List'; + +/** Description of trusted apps agnostic list */ +export const ENDPOINT_TRUSTED_APPS_LIST_DESCRIPTION = 'Endpoint Security Trusted Apps List'; + +/** ID of event filters agnostic list */ +export const ENDPOINT_EVENT_FILTERS_LIST_ID = 'endpoint_event_filters'; + +/** Name of event filters agnostic list */ +export const ENDPOINT_EVENT_FILTERS_LIST_NAME = 'Endpoint Security Event Filters List'; + +/** Description of event filters agnostic list */ +export const ENDPOINT_EVENT_FILTERS_LIST_DESCRIPTION = 'Endpoint Security Event Filters List'; + +/** The default max signals without any additional configuration */ +export const DEFAULT_MAX_SIGNALS = 100; diff --git a/packages/kbn-securitysolution-constants/tsconfig.json b/packages/kbn-securitysolution-constants/tsconfig.json new file mode 100644 index 0000000000000..cf06f4ced4b9f --- /dev/null +++ b/packages/kbn-securitysolution-constants/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "incremental": true, + "outDir": "target", + "rootDir": "src", + "sourceMap": true, + "sourceRoot": "../../../../packages/kbn-securitysolution-constants/src", + "types": [ + "jest", + "node" + ] + }, + "include": [ + "src/**/*" + ] +} diff --git a/packages/kbn-securitysolution-io-ts-utils/BUILD.bazel b/packages/kbn-securitysolution-io-ts-utils/BUILD.bazel new file mode 100644 index 0000000000000..66c5674067ecd --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/BUILD.bazel @@ -0,0 +1,92 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") + +PKG_BASE_NAME = "kbn-securitysolution-io-ts-utils" +PKG_REQUIRE_NAME = "@kbn/securitysolution-io-ts-utils" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + ], + exclude = [ + "**/*.test.*", + "**/*.mock.*" + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", + "README.md", +] + +SRC_DEPS = [ + "//packages/elastic-datemath", + "@npm//fp-ts", + "@npm//io-ts", + "@npm//lodash", + "@npm//moment", + "@npm//tslib", + "@npm//uuid", +] + +TYPES_DEPS = [ + "@npm//@types/flot", + "@npm//@types/jest", + "@npm//@types/lodash", + "@npm//@types/node", + "@npm//@types/uuid" +] + +DEPS = SRC_DEPS + TYPES_DEPS + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + ], +) + +ts_project( + name = "tsc", + args = ['--pretty'], + srcs = SRCS, + deps = DEPS, + declaration = True, + declaration_map = True, + incremental = True, + out_dir = "target", + source_map = True, + root_dir = "src", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_BASE_NAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = [":tsc"] + DEPS, + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [ + ":%s" % PKG_BASE_NAME, + ] +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-securitysolution-io-ts-utils/README.md b/packages/kbn-securitysolution-io-ts-utils/README.md new file mode 100644 index 0000000000000..908651b50b80a --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/README.md @@ -0,0 +1,10 @@ +# kbn-securitysolution-io-ts-utils + +Temporary location for all the io-ts-utils from security solutions. This is a lift-and-shift, where +we are moving them here for phase 1. + +Phase 2 is deprecating across plugins any copied code or sharing of io-ts utils that are now in here. + +Phase 3 is replacing those deprecated types with the ones in here. + +Phase 4+ is (potentially) consolidating any duplication or everything altogether with the `kbn-io-ts-utils` project \ No newline at end of file diff --git a/packages/kbn-securitysolution-io-ts-utils/jest.config.js b/packages/kbn-securitysolution-io-ts-utils/jest.config.js new file mode 100644 index 0000000000000..bd4e22ed3caaa --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/jest.config.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/kbn-securitysolution-io-ts-utils'], +}; diff --git a/packages/kbn-securitysolution-io-ts-utils/package.json b/packages/kbn-securitysolution-io-ts-utils/package.json new file mode 100644 index 0000000000000..0912fbfcb6e8d --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/package.json @@ -0,0 +1,9 @@ +{ + "name": "@kbn/securitysolution-io-ts-utils", + "version": "1.0.0", + "description": "io ts utilities and types to be shared with plugins from the security solution project", + "license": "SSPL-1.0 OR Elastic License 2.0", + "main": "./target/index.js", + "types": "./target/index.d.ts", + "private": true +} diff --git a/packages/kbn-securitysolution-io-ts-utils/src/actions/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/actions/index.ts new file mode 100644 index 0000000000000..7503bcc330e0e --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/actions/index.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/* eslint-disable @typescript-eslint/naming-convention */ + +import * as t from 'io-ts'; +import { saved_object_attributes } from '../saved_object_attributes'; + +/** + * Params is an "object", since it is a type of AlertActionParams which is action templates. + * @see x-pack/plugins/alerting/common/alert.ts + */ +export const action_group = t.string; +export const action_id = t.string; +export const action_action_type_id = t.string; +export const action_params = saved_object_attributes; + +export const action = t.exact( + t.type({ + group: action_group, + id: action_id, + action_type_id: action_action_type_id, + params: action_params, + }) +); + +export const actions = t.array(action); +export type Actions = t.TypeOf; + +export const actionsCamel = t.array( + t.exact( + t.type({ + group: action_group, + id: action_id, + actionTypeId: action_action_type_id, + params: action_params, + }) + ) +); +export type ActionsCamel = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/constants/index.mock.ts b/packages/kbn-securitysolution-io-ts-utils/src/constants/index.mock.ts new file mode 100644 index 0000000000000..d2107ae864f15 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/constants/index.mock.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +export const ENTRY_VALUE = 'some host name'; +export const FIELD = 'host.name'; +export const MATCH = 'match'; +export const MATCH_ANY = 'match_any'; +export const OPERATOR = 'included'; +export const NESTED = 'nested'; +export const NESTED_FIELD = 'parent.field'; +export const LIST_ID = 'some-list-id'; +export const LIST = 'list'; +export const TYPE = 'ip'; +export const EXISTS = 'exists'; +export const WILDCARD = 'wildcard'; +export const USER = 'some user'; +export const DATE_NOW = '2020-04-20T15:25:31.830Z'; + +// Exception List specific +export const ID = 'uuid_here'; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/constants/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/constants/index.ts new file mode 100644 index 0000000000000..91a3951ef11fc --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/constants/index.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/** + * This ID is used for _both_ the Saved Object ID and for the list_id + * for the single global space agnostic endpoint list + * TODO: Create a kbn-securitysolution-constants and add this to it. + * @deprecated Use the ENDPOINT_LIST_ID from the kbn-securitysolution-constants. + */ +export const ENDPOINT_LIST_ID = 'endpoint_list'; + +/** + * TODO: Create a kbn-securitysolution-constants and add this to it. + * @deprecated Use the DEFAULT_MAX_SIGNALS from the kbn-securitysolution-constants. + */ +export const DEFAULT_MAX_SIGNALS = 100; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/created_at/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/created_at/index.ts new file mode 100644 index 0000000000000..9051f68855818 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/created_at/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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/* eslint-disable @typescript-eslint/naming-convention */ + +import * as t from 'io-ts'; + +export const created_at = t.string; // TODO: Make this into an ISO Date string check diff --git a/packages/kbn-securitysolution-io-ts-utils/src/created_by/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/created_by/index.ts new file mode 100644 index 0000000000000..fbcf8da0cf458 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/created_by/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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/* eslint-disable @typescript-eslint/naming-convention */ + +import * as t from 'io-ts'; + +export const created_by = t.string; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/deafult_version_number/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/deafult_version_number/index.test.ts new file mode 100644 index 0000000000000..f77903d2d030d --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/deafult_version_number/index.test.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { DefaultVersionNumber } from '../default_version_number'; +import { foldLeftRight, getPaths } from '../test_utils'; + +describe('default_version_number', () => { + test('it should validate a version number', () => { + const payload = 5; + const decoded = DefaultVersionNumber.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate a 0', () => { + const payload = 0; + const decoded = DefaultVersionNumber.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "0" supplied to "DefaultVersionNumber"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not validate a -1', () => { + const payload = -1; + const decoded = DefaultVersionNumber.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "-1" supplied to "DefaultVersionNumber"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not validate a string', () => { + const payload = '5'; + const decoded = DefaultVersionNumber.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "5" supplied to "DefaultVersionNumber"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should return a default of 1', () => { + const payload = null; + const decoded = DefaultVersionNumber.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(1); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/deafult_version_number/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/deafult_version_number/index.ts new file mode 100644 index 0000000000000..245ff9d0db7dd --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/deafult_version_number/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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; +import { version, Version } from '../version'; + +/** + * Types the DefaultVersionNumber as: + * - If null or undefined, then a default of the number 1 will be used + */ +export const DefaultVersionNumber = new t.Type( + 'DefaultVersionNumber', + version.is, + (input, context): Either => + input == null ? t.success(1) : version.validate(input, context), + t.identity +); + +export type DefaultVersionNumberDecoded = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_actions_array/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/default_actions_array/index.ts new file mode 100644 index 0000000000000..9d741aa65e079 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/default_actions_array/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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; +import { actions, Actions } from '../actions'; + +export const DefaultActionsArray = new t.Type( + 'DefaultActionsArray', + actions.is, + (input, context): Either => + input == null ? t.success([]) : actions.validate(input, context), + t.identity +); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_array/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/default_array/index.test.ts new file mode 100644 index 0000000000000..82fa884b1c577 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/default_array/index.test.ts @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { DefaultArray } from '.'; +import { foldLeftRight, getPaths } from '../test_utils'; + +const testSchema = t.keyof({ + valid: true, + also_valid: true, +}); +type TestSchema = t.TypeOf; + +const defaultArraySchema = DefaultArray(testSchema); + +describe('default_array', () => { + test('it should validate an empty array', () => { + const payload: string[] = []; + const decoded = defaultArraySchema.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate an array of testSchema', () => { + const payload: TestSchema[] = ['valid']; + const decoded = defaultArraySchema.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate an array of valid testSchema strings', () => { + const payload = ['valid', 'also_valid']; + const decoded = defaultArraySchema.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate an array with a number', () => { + const payload = ['valid', 123]; + const decoded = defaultArraySchema.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "123" supplied to "DefaultArray"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not validate an array with an invalid string', () => { + const payload = ['valid', 'invalid']; + const decoded = defaultArraySchema.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "invalid" supplied to "DefaultArray"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should return a default array entry', () => { + const payload = null; + const decoded = defaultArraySchema.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual([]); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_array/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/default_array/index.ts new file mode 100644 index 0000000000000..7a39f484e601c --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/default_array/index.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; + +/** + * Types the DefaultArray as: + * - If undefined, then a default array will be set + * - If an array is sent in, then the array will be validated to ensure all elements are type C + */ +export const DefaultArray = (codec: C) => { + const arrType = t.array(codec); + type ArrType = t.TypeOf; + return new t.Type( + 'DefaultArray', + arrType.is, + (input, context): Either => + input == null ? t.success([]) : arrType.validate(input, context), + t.identity + ); +}; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_boolean_false/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/default_boolean_false/index.test.ts new file mode 100644 index 0000000000000..bddf9cc0747ea --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/default_boolean_false/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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { DefaultBooleanFalse } from '.'; +import { foldLeftRight, getPaths } from '../test_utils'; + +describe('default_boolean_false', () => { + test('it should validate a boolean false', () => { + const payload = false; + const decoded = DefaultBooleanFalse.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate a boolean true', () => { + const payload = true; + const decoded = DefaultBooleanFalse.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate a number', () => { + const payload = 5; + const decoded = DefaultBooleanFalse.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "5" supplied to "DefaultBooleanFalse"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should return a default false', () => { + const payload = null; + const decoded = DefaultBooleanFalse.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(false); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_boolean_false/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/default_boolean_false/index.ts new file mode 100644 index 0000000000000..f0b51afd2dc4d --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/default_boolean_false/index.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; + +/** + * Types the DefaultBooleanFalse as: + * - If null or undefined, then a default false will be set + */ +export const DefaultBooleanFalse = new t.Type( + 'DefaultBooleanFalse', + t.boolean.is, + (input, context): Either => + input == null ? t.success(false) : t.boolean.validate(input, context), + t.identity +); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_boolean_true/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/default_boolean_true/index.test.ts new file mode 100644 index 0000000000000..a05fb586c2e92 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/default_boolean_true/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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { DefaultBooleanTrue } from '.'; +import { foldLeftRight, getPaths } from '../test_utils'; + +describe('default_boolean_true', () => { + test('it should validate a boolean false', () => { + const payload = false; + const decoded = DefaultBooleanTrue.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate a boolean true', () => { + const payload = true; + const decoded = DefaultBooleanTrue.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate a number', () => { + const payload = 5; + const decoded = DefaultBooleanTrue.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "5" supplied to "DefaultBooleanTrue"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should return a default true', () => { + const payload = null; + const decoded = DefaultBooleanTrue.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(true); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_boolean_true/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/default_boolean_true/index.ts new file mode 100644 index 0000000000000..888417a6fe043 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/default_boolean_true/index.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; + +/** + * Types the DefaultBooleanTrue as: + * - If null or undefined, then a default true will be set + */ +export const DefaultBooleanTrue = new t.Type( + 'DefaultBooleanTrue', + t.boolean.is, + (input, context): Either => + input == null ? t.success(true) : t.boolean.validate(input, context), + t.identity +); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_empty_string/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/default_empty_string/index.test.ts new file mode 100644 index 0000000000000..5bdc9b298649e --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/default_empty_string/index.test.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { DefaultEmptyString } from '.'; +import { foldLeftRight, getPaths } from '../test_utils'; + +describe('default_empty_string', () => { + test('it should validate a regular string', () => { + const payload = 'some string'; + const decoded = DefaultEmptyString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate a number', () => { + const payload = 5; + const decoded = DefaultEmptyString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "5" supplied to "DefaultEmptyString"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should return a default of ""', () => { + const payload = null; + const decoded = DefaultEmptyString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(''); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_empty_string/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/default_empty_string/index.ts new file mode 100644 index 0000000000000..aed0d11a75684 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/default_empty_string/index.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; + +/** + * Types the DefaultEmptyString as: + * - If null or undefined, then a default of an empty string "" will be used + */ +export const DefaultEmptyString = new t.Type( + 'DefaultEmptyString', + t.string.is, + (input, context): Either => + input == null ? t.success('') : t.string.validate(input, context), + t.identity +); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_export_file_name/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/default_export_file_name/index.test.ts new file mode 100644 index 0000000000000..1f81f056386d7 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/default_export_file_name/index.test.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { DefaultExportFileName } from '.'; +import { foldLeftRight, getPaths } from '../test_utils'; + +describe('default_export_file_name', () => { + test('it should validate a regular string', () => { + const payload = 'some string'; + const decoded = DefaultExportFileName.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate a number', () => { + const payload = 5; + const decoded = DefaultExportFileName.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "5" supplied to "DefaultExportFileName"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should return a default of "export.ndjson"', () => { + const payload = null; + const decoded = DefaultExportFileName.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual('export.ndjson'); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_export_file_name/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/default_export_file_name/index.ts new file mode 100644 index 0000000000000..df6afd800c1d6 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/default_export_file_name/index.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; + +/** + * Types the DefaultExportFileName as: + * - If null or undefined, then a default of "export.ndjson" will be used + */ +export const DefaultExportFileName = new t.Type( + 'DefaultExportFileName', + t.string.is, + (input, context): Either => + input == null ? t.success('export.ndjson') : t.string.validate(input, context), + t.identity +); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_from_string/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/default_from_string/index.test.ts new file mode 100644 index 0000000000000..c1261f514540b --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/default_from_string/index.test.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { DefaultFromString } from '.'; +import { foldLeftRight, getPaths } from '../test_utils'; + +describe('default_from_string', () => { + test('it should validate a from string', () => { + const payload = 'now-20m'; + const decoded = DefaultFromString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate a number', () => { + const payload = 5; + const decoded = DefaultFromString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "5" supplied to "DefaultFromString"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should return a default of "now-6m"', () => { + const payload = null; + const decoded = DefaultFromString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual('now-6m'); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_from_string/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/default_from_string/index.ts new file mode 100644 index 0000000000000..55b76ab7c1a4e --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/default_from_string/index.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; +import { from } from '../from'; + +/** + * Types the DefaultFromString as: + * - If null or undefined, then a default of the string "now-6m" will be used + */ +export const DefaultFromString = new t.Type( + 'DefaultFromString', + t.string.is, + (input, context): Either => { + if (input == null) { + return t.success('now-6m'); + } + return from.validate(input, context); + }, + t.identity +); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_interval_string/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/default_interval_string/index.test.ts new file mode 100644 index 0000000000000..c4a0dc3664d0e --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/default_interval_string/index.test.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { DefaultIntervalString } from '.'; +import { foldLeftRight, getPaths } from '../test_utils'; + +describe('default_interval_string', () => { + test('it should validate a interval string', () => { + const payload = '20m'; + const decoded = DefaultIntervalString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate a number', () => { + const payload = 5; + const decoded = DefaultIntervalString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "5" supplied to "DefaultIntervalString"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should return a default of "5m"', () => { + const payload = null; + const decoded = DefaultIntervalString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual('5m'); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_interval_string/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/default_interval_string/index.ts new file mode 100644 index 0000000000000..7e42d32e42b13 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/default_interval_string/index.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; + +/** + * Types the DefaultIntervalString as: + * - If null or undefined, then a default of the string "5m" will be used + */ +export const DefaultIntervalString = new t.Type( + 'DefaultIntervalString', + t.string.is, + (input, context): Either => + input == null ? t.success('5m') : t.string.validate(input, context), + t.identity +); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_language_string/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/default_language_string/index.test.ts new file mode 100644 index 0000000000000..072c541a808a3 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/default_language_string/index.test.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { Language } from '../language'; +import { DefaultLanguageString } from '.'; +import { foldLeftRight, getPaths } from '../test_utils'; + +describe('default_language_string', () => { + test('it should validate a string', () => { + const payload: Language = 'lucene'; + const decoded = DefaultLanguageString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate a number', () => { + const payload = 5; + const decoded = DefaultLanguageString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "5" supplied to "DefaultLanguageString"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should return a default of "kuery"', () => { + const payload = null; + const decoded = DefaultLanguageString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual('kuery'); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_language_string/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/default_language_string/index.ts new file mode 100644 index 0000000000000..c7cdd02802bec --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/default_language_string/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; +import { language } from '../language'; + +/** + * Types the DefaultLanguageString as: + * - If null or undefined, then a default of the string "kuery" will be used + */ +export const DefaultLanguageString = new t.Type( + 'DefaultLanguageString', + t.string.is, + (input, context): Either => + input == null ? t.success('kuery') : language.validate(input, context), + t.identity +); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_max_signals_number/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/default_max_signals_number/index.test.ts new file mode 100644 index 0000000000000..bf703fa52d844 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/default_max_signals_number/index.test.ts @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { DefaultMaxSignalsNumber } from '.'; +import { foldLeftRight, getPaths } from '../test_utils'; +import { DEFAULT_MAX_SIGNALS } from '../constants'; + +describe('default_from_string', () => { + test('it should validate a max signal number', () => { + const payload = 5; + const decoded = DefaultMaxSignalsNumber.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate a string', () => { + const payload = '5'; + const decoded = DefaultMaxSignalsNumber.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "5" supplied to "DefaultMaxSignals"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not validate a zero', () => { + const payload = 0; + const decoded = DefaultMaxSignalsNumber.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "0" supplied to "DefaultMaxSignals"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not validate a negative number', () => { + const payload = -1; + const decoded = DefaultMaxSignalsNumber.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "-1" supplied to "DefaultMaxSignals"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should return a default of DEFAULT_MAX_SIGNALS', () => { + const payload = null; + const decoded = DefaultMaxSignalsNumber.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(DEFAULT_MAX_SIGNALS); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_max_signals_number/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/default_max_signals_number/index.ts new file mode 100644 index 0000000000000..09bc45f589e11 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/default_max_signals_number/index.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; +import { max_signals } from '../max_signals'; +import { DEFAULT_MAX_SIGNALS } from '../constants'; + +/** + * Types the default max signal: + * - Natural Number (positive integer and not a float), + * - greater than 1 + * - If undefined then it will use DEFAULT_MAX_SIGNALS (100) as the default + */ +export const DefaultMaxSignalsNumber = new t.Type( + 'DefaultMaxSignals', + t.number.is, + (input, context): Either => { + return input == null ? t.success(DEFAULT_MAX_SIGNALS) : max_signals.validate(input, context); + }, + t.identity +); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_page/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/default_page/index.test.ts new file mode 100644 index 0000000000000..3bcad15a7ebb8 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/default_page/index.test.ts @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { DefaultPage } from '.'; +import { foldLeftRight, getPaths } from '../test_utils'; + +describe('default_page', () => { + test('it should validate a regular number greater than zero', () => { + const payload = 5; + const decoded = DefaultPage.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate a string of a number', () => { + const payload = '5'; + const decoded = DefaultPage.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(5); + }); + + test('it should not validate a junk string', () => { + const payload = 'invalid-string'; + const decoded = DefaultPage.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "NaN" supplied to "DefaultPerPage"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not validate an empty string', () => { + const payload = ''; + const decoded = DefaultPage.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "NaN" supplied to "DefaultPerPage"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not validate a zero', () => { + const payload = 0; + const decoded = DefaultPage.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "0" supplied to "DefaultPerPage"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not validate a negative number', () => { + const payload = -1; + const decoded = DefaultPage.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "-1" supplied to "DefaultPerPage"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should return a default of 20', () => { + const payload = null; + const decoded = DefaultPage.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(1); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_page/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/default_page/index.ts new file mode 100644 index 0000000000000..056005b452a03 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/default_page/index.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; +import { PositiveIntegerGreaterThanZero } from '../positive_integer_greater_than_zero'; + +/** + * Types the DefaultPerPage as: + * - If a string this will convert the string to a number + * - If null or undefined, then a default of 1 will be used + * - If the number is 0 or less this will not validate as it has to be a positive number greater than zero + */ +export const DefaultPage = new t.Type( + 'DefaultPerPage', + t.number.is, + (input, context): Either => { + if (input == null) { + return t.success(1); + } else if (typeof input === 'string') { + return PositiveIntegerGreaterThanZero.validate(parseInt(input, 10), context); + } else { + return PositiveIntegerGreaterThanZero.validate(input, context); + } + }, + t.identity +); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_per_page/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/default_per_page/index.test.ts new file mode 100644 index 0000000000000..f7361ba12a570 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/default_per_page/index.test.ts @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { DefaultPerPage } from '.'; +import { foldLeftRight, getPaths } from '../test_utils'; + +describe('default_per_page', () => { + test('it should validate a regular number greater than zero', () => { + const payload = 5; + const decoded = DefaultPerPage.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate a string of a number', () => { + const payload = '5'; + const decoded = DefaultPerPage.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(5); + }); + + test('it should not validate a junk string', () => { + const payload = 'invalid-string'; + const decoded = DefaultPerPage.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "NaN" supplied to "DefaultPerPage"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not validate an empty string', () => { + const payload = ''; + const decoded = DefaultPerPage.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "NaN" supplied to "DefaultPerPage"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not validate a zero', () => { + const payload = 0; + const decoded = DefaultPerPage.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "0" supplied to "DefaultPerPage"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not validate a negative number', () => { + const payload = -1; + const decoded = DefaultPerPage.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "-1" supplied to "DefaultPerPage"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should return a default of 20', () => { + const payload = null; + const decoded = DefaultPerPage.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(20); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_per_page/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/default_per_page/index.ts new file mode 100644 index 0000000000000..026642f91c08a --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/default_per_page/index.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; +import { PositiveIntegerGreaterThanZero } from '../positive_integer_greater_than_zero'; + +/** + * Types the DefaultPerPage as: + * - If a string this will convert the string to a number + * - If null or undefined, then a default of 20 will be used + * - If the number is 0 or less this will not validate as it has to be a positive number greater than zero + */ +export const DefaultPerPage = new t.Type( + 'DefaultPerPage', + t.number.is, + (input, context): Either => { + if (input == null) { + return t.success(20); + } else if (typeof input === 'string') { + return PositiveIntegerGreaterThanZero.validate(parseInt(input, 10), context); + } else { + return PositiveIntegerGreaterThanZero.validate(input, context); + } + }, + t.identity +); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_risk_score_mapping_array/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/default_risk_score_mapping_array/index.ts new file mode 100644 index 0000000000000..8bd913af9255b --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/default_risk_score_mapping_array/index.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; +import { RiskScoreMapping, risk_score_mapping } from '../risk_score_mapping'; + +/** + * Types the DefaultStringArray as: + * - If null or undefined, then a default risk_score_mapping array will be set + */ +export const DefaultRiskScoreMappingArray = new t.Type< + RiskScoreMapping, + RiskScoreMapping | undefined, + unknown +>( + 'DefaultRiskScoreMappingArray', + risk_score_mapping.is, + (input, context): Either => + input == null ? t.success([]) : risk_score_mapping.validate(input, context), + t.identity +); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_severity_mapping_array/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/default_severity_mapping_array/index.ts new file mode 100644 index 0000000000000..58a96eef5a14f --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/default_severity_mapping_array/index.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; +import { SeverityMapping, severity_mapping } from '../severity_mapping'; + +/** + * Types the DefaultStringArray as: + * - If null or undefined, then a default severity_mapping array will be set + */ +export const DefaultSeverityMappingArray = new t.Type< + SeverityMapping, + SeverityMapping | undefined, + unknown +>( + 'DefaultSeverityMappingArray', + severity_mapping.is, + (input, context): Either => + input == null ? t.success([]) : severity_mapping.validate(input, context), + t.identity +); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_string_array/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/default_string_array/index.test.ts new file mode 100644 index 0000000000000..c7137d9c56b0d --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/default_string_array/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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { DefaultStringArray } from '.'; +import { foldLeftRight, getPaths } from '../test_utils'; + +describe('default_string_array', () => { + test('it should validate an empty array', () => { + const payload: string[] = []; + const decoded = DefaultStringArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate an array of strings', () => { + const payload = ['value 1', 'value 2']; + const decoded = DefaultStringArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate an array with a number', () => { + const payload = ['value 1', 5]; + const decoded = DefaultStringArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "5" supplied to "DefaultStringArray"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should return a default array entry', () => { + const payload = null; + const decoded = DefaultStringArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual([]); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_string_array/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/default_string_array/index.ts new file mode 100644 index 0000000000000..02f1831cfde29 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/default_string_array/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; + +/** + * Types the DefaultStringArray as: + * - If undefined, then a default array will be set + * - If an array is sent in, then the array will be validated to ensure all elements are a string + */ +export const DefaultStringArray = new t.Type( + 'DefaultStringArray', + t.array(t.string).is, + (input, context): Either => + input == null ? t.success([]) : t.array(t.string).validate(input, context), + t.identity +); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_string_boolean_false/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/default_string_boolean_false/index.test.ts new file mode 100644 index 0000000000000..2443e8f71fecd --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/default_string_boolean_false/index.test.ts @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { DefaultStringBooleanFalse } from '.'; +import { foldLeftRight, getPaths } from '../test_utils'; + +describe('default_string_boolean_false', () => { + test('it should validate a boolean false', () => { + const payload = false; + const decoded = DefaultStringBooleanFalse.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate a boolean true', () => { + const payload = true; + const decoded = DefaultStringBooleanFalse.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate a number', () => { + const payload = 5; + const decoded = DefaultStringBooleanFalse.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "5" supplied to "DefaultStringBooleanFalse"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should return a default false', () => { + const payload = null; + const decoded = DefaultStringBooleanFalse.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(false); + }); + + test('it should return a default false when given a string of "false"', () => { + const payload = 'false'; + const decoded = DefaultStringBooleanFalse.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(false); + }); + + test('it should return a default true when given a string of "true"', () => { + const payload = 'true'; + const decoded = DefaultStringBooleanFalse.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(true); + }); + + test('it should return a default true when given a string of "TruE"', () => { + const payload = 'TruE'; + const decoded = DefaultStringBooleanFalse.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(true); + }); + + test('it should not work with a string of junk "junk"', () => { + const payload = 'junk'; + const decoded = DefaultStringBooleanFalse.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "junk" supplied to "DefaultStringBooleanFalse"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not work with an empty string', () => { + const payload = ''; + const decoded = DefaultStringBooleanFalse.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "" supplied to "DefaultStringBooleanFalse"', + ]); + expect(message.schema).toEqual({}); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_string_boolean_false/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/default_string_boolean_false/index.ts new file mode 100644 index 0000000000000..f5f74f90e6b30 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/default_string_boolean_false/index.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; + +/** + * Types the DefaultStringBooleanFalse as: + * - If a string this will convert the string to a boolean + * - If null or undefined, then a default false will be set + */ +export const DefaultStringBooleanFalse = new t.Type( + 'DefaultStringBooleanFalse', + t.boolean.is, + (input, context): Either => { + if (input == null) { + return t.success(false); + } else if (typeof input === 'string' && input.toLowerCase() === 'true') { + return t.success(true); + } else if (typeof input === 'string' && input.toLowerCase() === 'false') { + return t.success(false); + } else { + return t.boolean.validate(input, context); + } + }, + t.identity +); + +export type DefaultStringBooleanFalseC = typeof DefaultStringBooleanFalse; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_threat_array/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/default_threat_array/index.test.ts new file mode 100644 index 0000000000000..ac86b5508ff14 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/default_threat_array/index.test.ts @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { Threats } from '../threat'; +import { DefaultThreatArray } from '.'; +import { foldLeftRight, getPaths } from '../test_utils'; + +describe('default_threat_null', () => { + test('it should validate an empty array', () => { + const payload: Threats = []; + const decoded = DefaultThreatArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate an array of threats', () => { + const payload: Threats = [ + { + framework: 'MITRE ATTACK', + technique: [{ reference: 'https://test.com', name: 'Audio Capture', id: 'T1123' }], + tactic: { reference: 'https://test.com', name: 'Collection', id: 'TA000999' }, + }, + ]; + const decoded = DefaultThreatArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate an array with a number', () => { + const payload = [ + { + framework: 'MITRE ATTACK', + technique: [{ reference: 'https://test.com', name: 'Audio Capture', id: 'T1123' }], + tactic: { reference: 'https://test.com', name: 'Collection', id: 'TA000999' }, + }, + 5, + ]; + const decoded = DefaultThreatArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "5" supplied to "DefaultThreatArray"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should return a default empty array if not provided a value', () => { + const payload = null; + const decoded = DefaultThreatArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual([]); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_threat_array/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/default_threat_array/index.ts new file mode 100644 index 0000000000000..8905369d270eb --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/default_threat_array/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; +import { threats, Threats } from '../threat'; + +/** + * Types the DefaultThreatArray as: + * - If null or undefined, then an empty array will be set + */ +export const DefaultThreatArray = new t.Type( + 'DefaultThreatArray', + threats.is, + (input, context): Either => + input == null ? t.success([]) : threats.validate(input, context), + t.identity +); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_throttle_null/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/default_throttle_null/index.test.ts new file mode 100644 index 0000000000000..4b8877bd532c2 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/default_throttle_null/index.test.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { Throttle } from '../throttle'; +import { DefaultThrottleNull } from '.'; +import { foldLeftRight, getPaths } from '../test_utils'; + +describe('default_throttle_null', () => { + test('it should validate a throttle string', () => { + const payload: Throttle = 'some string'; + const decoded = DefaultThrottleNull.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate an array with a number', () => { + const payload = 5; + const decoded = DefaultThrottleNull.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "5" supplied to "DefaultThreatNull"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should return a default "null" if not provided a value', () => { + const payload = undefined; + const decoded = DefaultThrottleNull.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(null); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_throttle_null/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/default_throttle_null/index.ts new file mode 100644 index 0000000000000..e9b9ec27ea418 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/default_throttle_null/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; +import { throttle, ThrottleOrNull } from '../throttle'; + +/** + * Types the DefaultThrottleNull as: + * - If null or undefined, then a null will be set + */ +export const DefaultThrottleNull = new t.Type( + 'DefaultThreatNull', + throttle.is, + (input, context): Either => + input == null ? t.success(null) : throttle.validate(input, context), + t.identity +); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_to_string/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/default_to_string/index.test.ts new file mode 100644 index 0000000000000..bcab8ebd5f17c --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/default_to_string/index.test.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { DefaultToString } from '.'; +import { foldLeftRight, getPaths } from '../test_utils'; + +describe('default_to_string', () => { + test('it should validate a to string', () => { + const payload = 'now-5m'; + const decoded = DefaultToString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate a number', () => { + const payload = 5; + const decoded = DefaultToString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "5" supplied to "DefaultToString"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should return a default of "now"', () => { + const payload = null; + const decoded = DefaultToString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual('now'); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_to_string/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/default_to_string/index.ts new file mode 100644 index 0000000000000..9ef50783c08c1 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/default_to_string/index.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; + +/** + * Types the DefaultToString as: + * - If null or undefined, then a default of the string "now" will be used + */ +export const DefaultToString = new t.Type( + 'DefaultToString', + t.string.is, + (input, context): Either => + input == null ? t.success('now') : t.string.validate(input, context), + t.identity +); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_uuid/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/default_uuid/index.test.ts new file mode 100644 index 0000000000000..d8cdff416037c --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/default_uuid/index.test.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { DefaultUuid } from '.'; +import { foldLeftRight, getPaths } from '../test_utils'; + +describe('default_uuid', () => { + test('it should validate a regular string', () => { + const payload = '1'; + const decoded = DefaultUuid.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate a number', () => { + const payload = 5; + const decoded = DefaultUuid.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to "DefaultUuid"']); + expect(message.schema).toEqual({}); + }); + + test('it should return a default of a uuid', () => { + const payload = null; + const decoded = DefaultUuid.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toMatch( + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i + ); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_uuid/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/default_uuid/index.ts new file mode 100644 index 0000000000000..01bd58d11e96d --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/default_uuid/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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; +import uuid from 'uuid'; +import { NonEmptyString } from '../non_empty_string'; + +/** + * Types the DefaultUuid as: + * - If null or undefined, then a default string uuid.v4() will be + * created otherwise it will be checked just against an empty string + */ +export const DefaultUuid = new t.Type( + 'DefaultUuid', + t.string.is, + (input, context): Either => + input == null ? t.success(uuid.v4()) : NonEmptyString.validate(input, context), + t.identity +); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_version_number/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/default_version_number/index.test.ts new file mode 100644 index 0000000000000..b9e9a3ff367e4 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/default_version_number/index.test.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { DefaultVersionNumber } from '.'; +import { foldLeftRight, getPaths } from '../test_utils'; + +describe('default_version_number', () => { + test('it should validate a version number', () => { + const payload = 5; + const decoded = DefaultVersionNumber.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate a 0', () => { + const payload = 0; + const decoded = DefaultVersionNumber.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "0" supplied to "DefaultVersionNumber"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not validate a -1', () => { + const payload = -1; + const decoded = DefaultVersionNumber.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "-1" supplied to "DefaultVersionNumber"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not validate a string', () => { + const payload = '5'; + const decoded = DefaultVersionNumber.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "5" supplied to "DefaultVersionNumber"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should return a default of 1', () => { + const payload = null; + const decoded = DefaultVersionNumber.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(1); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/default_version_number/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/default_version_number/index.ts new file mode 100644 index 0000000000000..245ff9d0db7dd --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/default_version_number/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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; +import { version, Version } from '../version'; + +/** + * Types the DefaultVersionNumber as: + * - If null or undefined, then a default of the number 1 will be used + */ +export const DefaultVersionNumber = new t.Type( + 'DefaultVersionNumber', + version.is, + (input, context): Either => + input == null ? t.success(1) : version.validate(input, context), + t.identity +); + +export type DefaultVersionNumberDecoded = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/description/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/description/index.ts new file mode 100644 index 0000000000000..eda821f4e54ec --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/description/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; + +export const description = t.string; +export type Description = t.TypeOf; +export const descriptionOrUndefined = t.union([description, t.undefined]); +export type DescriptionOrUndefined = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/empty_string_array/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/empty_string_array/index.test.ts new file mode 100644 index 0000000000000..86ffba6eeb60a --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/empty_string_array/index.test.ts @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { EmptyStringArray, EmptyStringArrayEncoded } from '.'; +import { foldLeftRight, getPaths } from '../test_utils'; + +describe('empty_string_array', () => { + test('it should validate "null" and create an empty array', () => { + const payload: EmptyStringArrayEncoded = null; + const decoded = EmptyStringArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual([]); + }); + + test('it should validate "undefined" and create an empty array', () => { + const payload: EmptyStringArrayEncoded = undefined; + const decoded = EmptyStringArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual([]); + }); + + test('it should validate a single value of "a" into an array of size 1 of ["a"]', () => { + const payload: EmptyStringArrayEncoded = 'a'; + const decoded = EmptyStringArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(['a']); + }); + + test('it should validate 2 values of "a,b" into an array of size 2 of ["a", "b"]', () => { + const payload: EmptyStringArrayEncoded = 'a,b'; + const decoded = EmptyStringArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(['a', 'b']); + }); + + test('it should validate 3 values of "a,b,c" into an array of size 3 of ["a", "b", "c"]', () => { + const payload: EmptyStringArrayEncoded = 'a,b,c'; + const decoded = EmptyStringArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(['a', 'b', 'c']); + }); + + test('it should FAIL validation of number', () => { + const payload: number = 5; + const decoded = EmptyStringArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "5" supplied to "EmptyStringArray"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should validate 3 values of " a, b, c " into an array of size 3 of ["a", "b", "c"] even though they have spaces', () => { + const payload: EmptyStringArrayEncoded = ' a, b, c '; + const decoded = EmptyStringArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(['a', 'b', 'c']); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/empty_string_array/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/empty_string_array/index.ts new file mode 100644 index 0000000000000..c9c0c7b4113a5 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/empty_string_array/index.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; + +/** + * Types the EmptyStringArray as: + * - A value that can be undefined, or null (which will be turned into an empty array) + * - A comma separated string that can turn into an array by splitting on it + * - Example input converted to output: undefined -> [] + * - Example input converted to output: null -> [] + * - Example input converted to output: "a,b,c" -> ["a", "b", "c"] + */ +export const EmptyStringArray = new t.Type( + 'EmptyStringArray', + t.array(t.string).is, + (input, context): Either => { + if (input == null) { + return t.success([]); + } else if (typeof input === 'string' && input.trim() !== '') { + const arrayValues = input + .trim() + .split(',') + .map((value) => value.trim()); + const emptyValueFound = arrayValues.some((value) => value === ''); + if (emptyValueFound) { + return t.failure(input, context); + } else { + return t.success(arrayValues); + } + } else { + return t.failure(input, context); + } + }, + String +); + +export type EmptyStringArrayEncoded = t.OutputOf; +export type EmptyStringArrayDecoded = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/exact_check/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/exact_check/index.test.ts new file mode 100644 index 0000000000000..512075b319739 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/exact_check/index.test.ts @@ -0,0 +1,177 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { left, right, Either } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { exactCheck, findDifferencesRecursive } from '.'; +import { foldLeftRight, getPaths } from '../test_utils'; + +describe('exact_check', () => { + test('it returns an error if given extra object properties', () => { + const someType = t.exact( + t.type({ + a: t.string, + }) + ); + const payload = { a: 'test', b: 'test' }; + const decoded = someType.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['invalid keys "b"']); + expect(message.schema).toEqual({}); + }); + + test('it returns an error if the data type is not as expected', () => { + type UnsafeCastForTest = Either< + t.Errors, + { + a: number; + } + >; + + const someType = t.exact( + t.type({ + a: t.string, + }) + ); + + const payload = { a: 1 }; + const decoded = someType.decode(payload); + const checked = exactCheck(payload, decoded as UnsafeCastForTest); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['Invalid value "1" supplied to "a"']); + expect(message.schema).toEqual({}); + }); + + test('it does NOT return an error if given normal object properties', () => { + const someType = t.exact( + t.type({ + a: t.string, + }) + ); + const payload = { a: 'test' }; + const decoded = someType.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it will return an existing error and not validate', () => { + const payload = { a: 'test' }; + const validationError: t.ValidationError = { + value: 'Some existing error', + context: [], + message: 'some error', + }; + const error: t.Errors = [validationError]; + const leftValue = left(error); + const checked = exactCheck(payload, leftValue); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['some error']); + expect(message.schema).toEqual({}); + }); + + test('it will work with a regular "right" payload without any decoding', () => { + const payload = { a: 'test' }; + const rightValue = right(payload); + const checked = exactCheck(payload, rightValue); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual({ a: 'test' }); + }); + + test('it will work with decoding a null payload when the schema expects a null', () => { + const someType = t.union([ + t.exact( + t.type({ + a: t.string, + }) + ), + t.null, + ]); + const payload = null; + const decoded = someType.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(null); + }); + + test('it should find no differences recursively with two empty objects', () => { + const difference = findDifferencesRecursive({}, {}); + expect(difference).toEqual([]); + }); + + test('it should find a single difference with two objects with different keys', () => { + const difference = findDifferencesRecursive({ a: 1 }, { b: 1 }); + expect(difference).toEqual(['a']); + }); + + test('it should find a two differences with two objects with multiple different keys', () => { + const difference = findDifferencesRecursive({ a: 1, c: 1 }, { b: 1 }); + expect(difference).toEqual(['a', 'c']); + }); + + test('it should find no differences with two objects with the same keys', () => { + const difference = findDifferencesRecursive({ a: 1, b: 1 }, { a: 1, b: 1 }); + expect(difference).toEqual([]); + }); + + test('it should find a difference with two deep objects with different same keys', () => { + const difference = findDifferencesRecursive({ a: 1, b: { c: 1 } }, { a: 1, b: { d: 1 } }); + expect(difference).toEqual(['c']); + }); + + test('it should find a difference within an array', () => { + const difference = findDifferencesRecursive({ a: 1, b: [{ c: 1 }] }, { a: 1, b: [{ a: 1 }] }); + expect(difference).toEqual(['c']); + }); + + test('it should find a no difference when using arrays that are identical', () => { + const difference = findDifferencesRecursive({ a: 1, b: [{ c: 1 }] }, { a: 1, b: [{ c: 1 }] }); + expect(difference).toEqual([]); + }); + + test('it should find differences when one has an array and the other does not', () => { + const difference = findDifferencesRecursive({ a: 1, b: [{ c: 1 }] }, { a: 1 }); + expect(difference).toEqual(['b', '[{"c":1}]']); + }); + + test('it should find differences when one has an deep object and the other does not', () => { + const difference = findDifferencesRecursive({ a: 1, b: { c: 1 } }, { a: 1 }); + expect(difference).toEqual(['b', '{"c":1}']); + }); + + test('it should find differences when one has a deep object with multiple levels and the other does not', () => { + const difference = findDifferencesRecursive({ a: 1, b: { c: { d: 1 } } }, { a: 1 }); + expect(difference).toEqual(['b', '{"c":{"d":1}}']); + }); + + test('it tests two deep objects as the same with no key differences', () => { + const difference = findDifferencesRecursive( + { a: 1, b: { c: { d: 1 } } }, + { a: 1, b: { c: { d: 1 } } } + ); + expect(difference).toEqual([]); + }); + + test('it tests two deep objects with just one deep key difference', () => { + const difference = findDifferencesRecursive( + { a: 1, b: { c: { d: 1 } } }, + { a: 1, b: { c: { e: 1 } } } + ); + expect(difference).toEqual(['d']); + }); + + test('it should not find any differences when the original and decoded are both null', () => { + const difference = findDifferencesRecursive(null, null); + expect(difference).toEqual([]); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/exact_check/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/exact_check/index.ts new file mode 100644 index 0000000000000..23931b393990a --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/exact_check/index.ts @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { left, Either, fold, right } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { isObject, get } from 'lodash/fp'; + +/** + * Given an original object and a decoded object this will return an error + * if and only if the original object has additional keys that the decoded + * object does not have. If the original decoded already has an error, then + * this will return the error as is and not continue. + * + * NOTE: You MUST use t.exact(...) for this to operate correctly as your schema + * needs to remove additional keys before the compare + * + * You might not need this in the future if the below issue is solved: + * https://github.com/gcanti/io-ts/issues/322 + * + * @param original The original to check if it has additional keys + * @param decoded The decoded either which has either an existing error or the + * decoded object which could have additional keys stripped from it. + */ +export const exactCheck = ( + original: unknown, + decoded: Either +): Either => { + const onLeft = (errors: t.Errors): Either => left(errors); + const onRight = (decodedValue: T): Either => { + const differences = findDifferencesRecursive(original, decodedValue); + if (differences.length !== 0) { + const validationError: t.ValidationError = { + value: differences, + context: [], + message: `invalid keys "${differences.join(',')}"`, + }; + const error: t.Errors = [validationError]; + return left(error); + } else { + return right(decodedValue); + } + }; + return pipe(decoded, fold(onLeft, onRight)); +}; + +export const findDifferencesRecursive = (original: unknown, decodedValue: T): string[] => { + if (decodedValue === null && original === null) { + // both the decodedValue and the original are null which indicates that they are equal + // so do not report differences + return []; + } else if (decodedValue == null) { + try { + // It is null and painful when the original contains an object or an array + // the the decoded value does not have. + return [JSON.stringify(original)]; + } catch (err) { + return ['circular reference']; + } + } else if (typeof original !== 'object' || original == null) { + // We are not an object or null so do not report differences + return []; + } else { + const decodedKeys = Object.keys(decodedValue); + const differences = Object.keys(original).flatMap((originalKey) => { + const foundKey = decodedKeys.some((key) => key === originalKey); + const topLevelKey = foundKey ? [] : [originalKey]; + // I use lodash to cheat and get an any (not going to lie ;-)) + const valueObjectOrArrayOriginal = get(originalKey, original); + const valueObjectOrArrayDecoded = get(originalKey, decodedValue); + if (isObject(valueObjectOrArrayOriginal)) { + return [ + ...topLevelKey, + ...findDifferencesRecursive(valueObjectOrArrayOriginal, valueObjectOrArrayDecoded), + ]; + } else if (Array.isArray(valueObjectOrArrayOriginal)) { + return [ + ...topLevelKey, + ...valueObjectOrArrayOriginal.flatMap((arrayElement, index) => + findDifferencesRecursive(arrayElement, get(index, valueObjectOrArrayDecoded)) + ), + ]; + } else { + return topLevelKey; + } + }); + return differences; + } +}; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/format_errors/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/format_errors/index.test.ts new file mode 100644 index 0000000000000..f47f8f87f6a7e --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/format_errors/index.test.ts @@ -0,0 +1,189 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { formatErrors } from '.'; + +describe('utils', () => { + test('returns an empty error message string if there are no errors', () => { + const errors: t.Errors = []; + const output = formatErrors(errors); + expect(output).toEqual([]); + }); + + test('returns a single error message if given one', () => { + const validationError: t.ValidationError = { + value: 'Some existing error', + context: [], + message: 'some error', + }; + const errors: t.Errors = [validationError]; + const output = formatErrors(errors); + expect(output).toEqual(['some error']); + }); + + test('returns a two error messages if given two', () => { + const validationError1: t.ValidationError = { + value: 'Some existing error 1', + context: [], + message: 'some error 1', + }; + const validationError2: t.ValidationError = { + value: 'Some existing error 2', + context: [], + message: 'some error 2', + }; + const errors: t.Errors = [validationError1, validationError2]; + const output = formatErrors(errors); + expect(output).toEqual(['some error 1', 'some error 2']); + }); + + test('it filters out duplicate error messages', () => { + const validationError1: t.ValidationError = { + value: 'Some existing error 1', + context: [], + message: 'some error 1', + }; + const validationError2: t.ValidationError = { + value: 'Some existing error 1', + context: [], + message: 'some error 1', + }; + const errors: t.Errors = [validationError1, validationError2]; + const output = formatErrors(errors); + expect(output).toEqual(['some error 1']); + }); + + test('will use message before context if it is set', () => { + const context: t.Context = ([{ key: 'some string key' }] as unknown) as t.Context; + const validationError1: t.ValidationError = { + value: 'Some existing error 1', + context, + message: 'I should be used first', + }; + const errors: t.Errors = [validationError1]; + const output = formatErrors(errors); + expect(output).toEqual(['I should be used first']); + }); + + test('will use context entry of a single string', () => { + const context: t.Context = ([{ key: 'some string key' }] as unknown) as t.Context; + const validationError1: t.ValidationError = { + value: 'Some existing error 1', + context, + }; + const errors: t.Errors = [validationError1]; + const output = formatErrors(errors); + expect(output).toEqual(['Invalid value "Some existing error 1" supplied to "some string key"']); + }); + + test('will use two context entries of two strings', () => { + const context: t.Context = ([ + { key: 'some string key 1' }, + { key: 'some string key 2' }, + ] as unknown) as t.Context; + const validationError1: t.ValidationError = { + value: 'Some existing error 1', + context, + }; + const errors: t.Errors = [validationError1]; + const output = formatErrors(errors); + expect(output).toEqual([ + 'Invalid value "Some existing error 1" supplied to "some string key 1,some string key 2"', + ]); + }); + + test('will filter out and not use any strings of numbers', () => { + const context: t.Context = ([ + { key: '5' }, + { key: 'some string key 2' }, + ] as unknown) as t.Context; + const validationError1: t.ValidationError = { + value: 'Some existing error 1', + context, + }; + const errors: t.Errors = [validationError1]; + const output = formatErrors(errors); + expect(output).toEqual([ + 'Invalid value "Some existing error 1" supplied to "some string key 2"', + ]); + }); + + test('will filter out and not use null', () => { + const context: t.Context = ([ + { key: null }, + { key: 'some string key 2' }, + ] as unknown) as t.Context; + const validationError1: t.ValidationError = { + value: 'Some existing error 1', + context, + }; + const errors: t.Errors = [validationError1]; + const output = formatErrors(errors); + expect(output).toEqual([ + 'Invalid value "Some existing error 1" supplied to "some string key 2"', + ]); + }); + + test('will filter out and not use empty strings', () => { + const context: t.Context = ([ + { key: '' }, + { key: 'some string key 2' }, + ] as unknown) as t.Context; + const validationError1: t.ValidationError = { + value: 'Some existing error 1', + context, + }; + const errors: t.Errors = [validationError1]; + const output = formatErrors(errors); + expect(output).toEqual([ + 'Invalid value "Some existing error 1" supplied to "some string key 2"', + ]); + }); + + test('will use a name context if it cannot find a keyContext', () => { + const context: t.Context = ([ + { key: '' }, + { key: '', type: { name: 'someName' } }, + ] as unknown) as t.Context; + const validationError1: t.ValidationError = { + value: 'Some existing error 1', + context, + }; + const errors: t.Errors = [validationError1]; + const output = formatErrors(errors); + expect(output).toEqual(['Invalid value "Some existing error 1" supplied to "someName"']); + }); + + test('will return an empty string if name does not exist but type does', () => { + const context: t.Context = ([{ key: '' }, { key: '', type: {} }] as unknown) as t.Context; + const validationError1: t.ValidationError = { + value: 'Some existing error 1', + context, + }; + const errors: t.Errors = [validationError1]; + const output = formatErrors(errors); + expect(output).toEqual(['Invalid value "Some existing error 1" supplied to ""']); + }); + + test('will stringify an error value', () => { + const context: t.Context = ([ + { key: '' }, + { key: 'some string key 2' }, + ] as unknown) as t.Context; + const validationError1: t.ValidationError = { + value: { foo: 'some error' }, + context, + }; + const errors: t.Errors = [validationError1]; + const output = formatErrors(errors); + expect(output).toEqual([ + 'Invalid value "{"foo":"some error"}" supplied to "some string key 2"', + ]); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/format_errors/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/format_errors/index.ts new file mode 100644 index 0000000000000..7df66dcd13596 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/format_errors/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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { isObject } from 'lodash/fp'; + +export const formatErrors = (errors: t.Errors): string[] => { + const err = errors.map((error) => { + if (error.message != null) { + return error.message; + } else { + const keyContext = error.context + .filter( + (entry) => entry.key != null && !Number.isInteger(+entry.key) && entry.key.trim() !== '' + ) + .map((entry) => entry.key) + .join(','); + + const nameContext = error.context.find((entry) => entry.type?.name?.length > 0); + const suppliedValue = + keyContext !== '' ? keyContext : nameContext != null ? nameContext.type.name : ''; + const value = isObject(error.value) ? JSON.stringify(error.value) : error.value; + return `Invalid value "${value}" supplied to "${suppliedValue}"`; + } + }); + + return [...new Set(err)]; +}; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/from/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/from/index.ts new file mode 100644 index 0000000000000..963e2fa0444f0 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/from/index.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Either } from 'fp-ts/lib/Either'; +import * as t from 'io-ts'; +import { parseScheduleDates } from '../parse_schedule_dates'; + +const stringValidator = (input: unknown): input is string => typeof input === 'string'; + +export const from = new t.Type( + 'From', + t.string.is, + (input, context): Either => { + if (stringValidator(input) && parseScheduleDates(input) == null) { + return t.failure(input, context, 'Failed to parse "from" on rule param'); + } + return t.string.validate(input, context); + }, + t.identity +); +export type From = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/id/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/id/index.ts new file mode 100644 index 0000000000000..7b187d7730f73 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/id/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { NonEmptyString } from '../non_empty_string'; + +export const id = NonEmptyString; +export type Id = t.TypeOf; +export const idOrUndefined = t.union([id, t.undefined]); +export type IdOrUndefined = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/index.ts new file mode 100644 index 0000000000000..bae90fed29dea --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/index.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './format_errors'; +export * from './actions'; +export * from './constants'; +export * from './created_at'; +export * from './created_by'; +export * from './default_version_number'; +export * from './default_actions_array'; +export * from './default_array'; +export * from './default_boolean_false'; +export * from './default_boolean_true'; +export * from './default_empty_string'; +export * from './default_export_file_name'; +export * from './default_from_string'; +export * from './default_interval_string'; +export * from './default_language_string'; +export * from './default_max_signals_number'; +export * from './default_page'; +export * from './default_per_page'; +export * from './default_risk_score_mapping_array'; +export * from './default_severity_mapping_array'; +export * from './default_string_array'; +export * from './default_string_boolean_false'; +export * from './default_threat_array'; +export * from './default_throttle_null'; +export * from './default_to_string'; +export * from './default_uuid'; +export * from './default_version_number'; +export * from './description'; +export * from './empty_string_array'; +export * from './exact_check'; +export * from './format_errors'; +export * from './from'; +export * from './id'; +export * from './iso_date_string'; +export * from './language'; +export * from './max_signals'; +export * from './meta'; +export * from './name'; +export * from './non_empty_array'; +export * from './non_empty_or_nullable_string_array'; +export * from './non_empty_string'; +export * from './normalized_ml_job_id'; +export * from './only_false_allowed'; +export * from './operator'; +export * from './parse_schedule_dates'; +export * from './positive_integer'; +export * from './positive_integer_greater_than_zero'; +export * from './references_default_array'; +export * from './risk_score'; +export * from './risk_score_mapping'; +export * from './saved_object_attributes'; +export * from './severity'; +export * from './severity_mapping'; +export * from './string_to_positive_number'; +export * from './tags'; +export * from './threat'; +export * from './threat_mapping'; +export * from './threat_subtechnique'; +export * from './threat_tactic'; +export * from './threat_technique'; +export * from './throttle'; +export * from './updated_at'; +export * from './updated_by'; +export * from './uuid'; +export * from './validate'; +export * from './version'; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/iso_date_string/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/iso_date_string/index.test.ts new file mode 100644 index 0000000000000..4b73ed1b136dc --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/iso_date_string/index.test.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { IsoDateString } from '.'; +import { foldLeftRight, getPaths } from '../test_utils'; + +describe('ios_date_string', () => { + test('it should validate a iso string', () => { + const payload = '2020-02-26T00:32:34.541Z'; + const decoded = IsoDateString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate an epoch number', () => { + const payload = '1582677283067'; + const decoded = IsoDateString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1582677283067" supplied to "IsoDateString"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not validate a number such as 2000', () => { + const payload = '2000'; + const decoded = IsoDateString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "2000" supplied to "IsoDateString"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not validate a UTC', () => { + const payload = 'Wed, 26 Feb 2020 00:36:20 GMT'; + const decoded = IsoDateString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "Wed, 26 Feb 2020 00:36:20 GMT" supplied to "IsoDateString"', + ]); + expect(message.schema).toEqual({}); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/iso_date_string/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/iso_date_string/index.ts new file mode 100644 index 0000000000000..f59b07c554b65 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/iso_date_string/index.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; + +/** + * Types the IsoDateString as: + * - A string that is an ISOString + */ +export const IsoDateString = new t.Type( + 'IsoDateString', + t.string.is, + (input, context): Either => { + if (typeof input === 'string') { + try { + const parsed = new Date(input); + if (parsed.toISOString() === input) { + return t.success(input); + } else { + return t.failure(input, context); + } + } catch (err) { + return t.failure(input, context); + } + } else { + return t.failure(input, context); + } + }, + t.identity +); + +export type IsoDateStringC = typeof IsoDateString; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/language/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/language/index.ts new file mode 100644 index 0000000000000..fc3f70f1f2d88 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/language/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; + +export const language = t.keyof({ eql: null, kuery: null, lucene: null }); +export type Language = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/comment/index.mock.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/comment/index.mock.ts new file mode 100644 index 0000000000000..56440d628e4aa --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/comment/index.mock.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Comment, CommentsArray } from '.'; +import { DATE_NOW, ID, USER } from '../../constants/index.mock'; + +export const getCommentsMock = (): Comment => ({ + comment: 'some old comment', + created_at: DATE_NOW, + created_by: USER, + id: ID, +}); + +export const getCommentsArrayMock = (): CommentsArray => [getCommentsMock(), getCommentsMock()]; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/comment/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/comment/index.test.ts new file mode 100644 index 0000000000000..0f0bfac5e2068 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/comment/index.test.ts @@ -0,0 +1,237 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { getCommentsArrayMock, getCommentsMock } from './index.mock'; +import { + Comment, + comment, + CommentsArray, + commentsArray, + CommentsArrayOrUndefined, + commentsArrayOrUndefined, +} from '.'; +import { foldLeftRight, getPaths } from '../../test_utils'; +import { DATE_NOW } from '../../constants/index.mock'; + +describe('Comment', () => { + describe('comment', () => { + test('it fails validation when "id" is undefined', () => { + const payload = { ...getCommentsMock(), id: undefined }; + const decoded = comment.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "id"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it passes validation with a typical comment', () => { + const payload = getCommentsMock(); + const decoded = comment.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it passes validation with "updated_at" and "updated_by" fields included', () => { + const payload = getCommentsMock(); + payload.updated_at = DATE_NOW; + payload.updated_by = 'someone'; + const decoded = comment.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it fails validation when undefined', () => { + const payload = undefined; + const decoded = comment.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "({| comment: NonEmptyString, created_at: string, created_by: string, id: NonEmptyString |} & Partial<{| updated_at: string, updated_by: string |}>)"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it fails validation when "comment" is an empty string', () => { + const payload: Omit & { comment: string } = { + ...getCommentsMock(), + comment: '', + }; + const decoded = comment.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "comment"']); + expect(message.schema).toEqual({}); + }); + + test('it fails validation when "comment" is not a string', () => { + const payload: Omit & { comment: string[] } = { + ...getCommentsMock(), + comment: ['some value'], + }; + const decoded = comment.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "["some value"]" supplied to "comment"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it fails validation when "created_at" is not a string', () => { + const payload: Omit & { created_at: number } = { + ...getCommentsMock(), + created_at: 1, + }; + const decoded = comment.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1" supplied to "created_at"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it fails validation when "created_by" is not a string', () => { + const payload: Omit & { created_by: number } = { + ...getCommentsMock(), + created_by: 1, + }; + const decoded = comment.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1" supplied to "created_by"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it fails validation when "updated_at" is not a string', () => { + const payload: Omit & { updated_at: number } = { + ...getCommentsMock(), + updated_at: 1, + }; + const decoded = comment.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1" supplied to "updated_at"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it fails validation when "updated_by" is not a string', () => { + const payload: Omit & { updated_by: number } = { + ...getCommentsMock(), + updated_by: 1, + }; + const decoded = comment.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1" supplied to "updated_by"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should strip out extra keys', () => { + const payload: Comment & { + extraKey?: string; + } = getCommentsMock(); + payload.extraKey = 'some value'; + const decoded = comment.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(getCommentsMock()); + }); + }); + + describe('commentsArray', () => { + test('it passes validation an array of Comment', () => { + const payload = getCommentsArrayMock(); + const decoded = commentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it passes validation when a Comment includes "updated_at" and "updated_by"', () => { + const commentsPayload = getCommentsMock(); + commentsPayload.updated_at = DATE_NOW; + commentsPayload.updated_by = 'someone'; + const payload = [{ ...commentsPayload }, ...getCommentsArrayMock()]; + const decoded = commentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it fails validation when undefined', () => { + const payload = undefined; + const decoded = commentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "Array<({| comment: NonEmptyString, created_at: string, created_by: string, id: NonEmptyString |} & Partial<{| updated_at: string, updated_by: string |}>)>"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it fails validation when array includes non Comment types', () => { + const payload = ([1] as unknown) as CommentsArray; + const decoded = commentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1" supplied to "Array<({| comment: NonEmptyString, created_at: string, created_by: string, id: NonEmptyString |} & Partial<{| updated_at: string, updated_by: string |}>)>"', + ]); + expect(message.schema).toEqual({}); + }); + }); + + describe('commentsArrayOrUndefined', () => { + test('it passes validation an array of Comment', () => { + const payload = getCommentsArrayMock(); + const decoded = commentsArrayOrUndefined.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it passes validation when undefined', () => { + const payload = undefined; + const decoded = commentsArrayOrUndefined.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it fails validation when array includes non Comment types', () => { + const payload = ([1] as unknown) as CommentsArrayOrUndefined; + const decoded = commentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1" supplied to "Array<({| comment: NonEmptyString, created_at: string, created_by: string, id: NonEmptyString |} & Partial<{| updated_at: string, updated_by: string |}>)>"', + ]); + expect(message.schema).toEqual({}); + }); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/comment/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/comment/index.ts new file mode 100644 index 0000000000000..783d8606b8a96 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/comment/index.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; + +import { NonEmptyString } from '../../non_empty_string'; +import { created_at } from '../../created_at'; +import { created_by } from '../../created_by'; +import { id } from '../../id'; +import { updated_at } from '../../updated_at'; +import { updated_by } from '../../updated_by'; + +export const comment = t.intersection([ + t.exact( + t.type({ + comment: NonEmptyString, + created_at, + created_by, + id, + }) + ), + t.exact( + t.partial({ + updated_at, + updated_by, + }) + ), +]); + +export const commentsArray = t.array(comment); +export type CommentsArray = t.TypeOf; +export type Comment = t.TypeOf; +export const commentsArrayOrUndefined = t.union([commentsArray, t.undefined]); +export type CommentsArrayOrUndefined = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/create_comment/index.mock.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/create_comment/index.mock.ts new file mode 100644 index 0000000000000..0d0e2b51d003c --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/create_comment/index.mock.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { CreateComment, CreateCommentsArray } from '.'; + +export const getCreateCommentsMock = (): CreateComment => ({ + comment: 'some comments', +}); + +export const getCreateCommentsArrayMock = (): CreateCommentsArray => [getCreateCommentsMock()]; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/create_comment/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/create_comment/index.test.ts new file mode 100644 index 0000000000000..1ac605e232ea1 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/create_comment/index.test.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { getCreateCommentsArrayMock, getCreateCommentsMock } from './index.mock'; +import { + CreateComment, + createComment, + CreateCommentsArray, + createCommentsArray, + CreateCommentsArrayOrUndefined, + createCommentsArrayOrUndefined, +} from '.'; +import { foldLeftRight, getPaths } from '../../test_utils'; + +describe('CreateComment', () => { + describe('createComment', () => { + test('it passes validation with a default comment', () => { + const payload = getCreateCommentsMock(); + const decoded = createComment.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it fails validation when undefined', () => { + const payload = undefined; + const decoded = createComment.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "{| comment: NonEmptyString |}"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it fails validation when "comment" is not a string', () => { + const payload: Omit & { comment: string[] } = { + ...getCreateCommentsMock(), + comment: ['some value'], + }; + const decoded = createComment.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "["some value"]" supplied to "comment"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should strip out extra keys', () => { + const payload: CreateComment & { + extraKey?: string; + } = getCreateCommentsMock(); + payload.extraKey = 'some value'; + const decoded = createComment.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(getCreateCommentsMock()); + }); + }); + + describe('createCommentsArray', () => { + test('it passes validation an array of comments', () => { + const payload = getCreateCommentsArrayMock(); + const decoded = createCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it fails validation when undefined', () => { + const payload = undefined; + const decoded = createCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "Array<{| comment: NonEmptyString |}>"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it fails validation when array includes non comments types', () => { + const payload = ([1] as unknown) as CreateCommentsArray; + const decoded = createCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1" supplied to "Array<{| comment: NonEmptyString |}>"', + ]); + expect(message.schema).toEqual({}); + }); + }); + + describe('createCommentsArrayOrUndefined', () => { + test('it passes validation an array of comments', () => { + const payload = getCreateCommentsArrayMock(); + const decoded = createCommentsArrayOrUndefined.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it passes validation when undefined', () => { + const payload = undefined; + const decoded = createCommentsArrayOrUndefined.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it fails validation when array includes non comments types', () => { + const payload = ([1] as unknown) as CreateCommentsArrayOrUndefined; + const decoded = createCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1" supplied to "Array<{| comment: NonEmptyString |}>"', + ]); + expect(message.schema).toEqual({}); + }); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/create_comment/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/create_comment/index.ts new file mode 100644 index 0000000000000..438f946e796d6 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/create_comment/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { NonEmptyString } from '../../non_empty_string'; + +export const createComment = t.exact( + t.type({ + comment: NonEmptyString, + }) +); + +export type CreateComment = t.TypeOf; +export const createCommentsArray = t.array(createComment); +export type CreateCommentsArray = t.TypeOf; +export type CreateComments = t.TypeOf; +export const createCommentsArrayOrUndefined = t.union([createCommentsArray, t.undefined]); +export type CreateCommentsArrayOrUndefined = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_comments_array/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_comments_array/index.test.ts new file mode 100644 index 0000000000000..5e667380e2adf --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_comments_array/index.test.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { CommentsArray } from '../comment'; +import { DefaultCommentsArray } from '.'; +import { foldLeftRight, getPaths } from '../../test_utils'; +import { getCommentsArrayMock } from '../comment/index.mock'; + +describe('default_comments_array', () => { + test('it should pass validation when supplied an empty array', () => { + const payload: CommentsArray = []; + const decoded = DefaultCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should pass validation when supplied an array of comments', () => { + const payload: CommentsArray = getCommentsArrayMock(); + const decoded = DefaultCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should fail validation when supplied an array of numbers', () => { + const payload = [1]; + const decoded = DefaultCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1" supplied to "Array<({| comment: NonEmptyString, created_at: string, created_by: string, id: NonEmptyString |} & Partial<{| updated_at: string, updated_by: string |}>)>"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should fail validation when supplied an array of strings', () => { + const payload = ['some string']; + const decoded = DefaultCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "some string" supplied to "Array<({| comment: NonEmptyString, created_at: string, created_by: string, id: NonEmptyString |} & Partial<{| updated_at: string, updated_by: string |}>)>"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should return a default array entry', () => { + const payload = null; + const decoded = DefaultCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual([]); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_comments_array/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_comments_array/index.ts new file mode 100644 index 0000000000000..c2f81b82a0c0a --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_comments_array/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; +import { comment, CommentsArray } from '../comment'; + +/** + * Types the DefaultCommentsArray as: + * - If null or undefined, then a default array of type entry will be set + */ +export const DefaultCommentsArray = new t.Type( + 'DefaultCommentsArray', + t.array(comment).is, + (input): Either => + input == null ? t.success([]) : t.array(comment).decode(input), + t.identity +); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_create_comments_array/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_create_comments_array/index.test.ts new file mode 100644 index 0000000000000..a4581fabbf6a9 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_create_comments_array/index.test.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { foldLeftRight, getPaths } from '../../test_utils'; +import { CommentsArray } from '../comment'; +import { DefaultCommentsArray } from '../default_comments_array'; +import { getCommentsArrayMock } from '../comment/index.mock'; + +describe('default_comments_array', () => { + test('it should pass validation when supplied an empty array', () => { + const payload: CommentsArray = []; + const decoded = DefaultCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should pass validation when supplied an array of comments', () => { + const payload: CommentsArray = getCommentsArrayMock(); + const decoded = DefaultCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should fail validation when supplied an array of numbers', () => { + const payload = [1]; + const decoded = DefaultCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1" supplied to "Array<({| comment: NonEmptyString, created_at: string, created_by: string, id: NonEmptyString |} & Partial<{| updated_at: string, updated_by: string |}>)>"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should fail validation when supplied an array of strings', () => { + const payload = ['some string']; + const decoded = DefaultCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "some string" supplied to "Array<({| comment: NonEmptyString, created_at: string, created_by: string, id: NonEmptyString |} & Partial<{| updated_at: string, updated_by: string |}>)>"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should return a default array entry', () => { + const payload = null; + const decoded = DefaultCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual([]); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_create_comments_array/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_create_comments_array/index.ts new file mode 100644 index 0000000000000..ba3babdabf267 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_create_comments_array/index.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; +import { createComment, CreateCommentsArray } from '../create_comment'; + +/** + * Types the DefaultCreateComments as: + * - If null or undefined, then a default array of type entry will be set + */ +export const DefaultCreateCommentsArray = new t.Type< + CreateCommentsArray, + CreateCommentsArray, + unknown +>( + 'DefaultCreateComments', + t.array(createComment).is, + (input): Either => + input == null ? t.success([]) : t.array(createComment).decode(input), + t.identity +); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_namespace/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_namespace/index.test.ts new file mode 100644 index 0000000000000..1decca0de6c50 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_namespace/index.test.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { DefaultNamespace } from '.'; +import { foldLeftRight, getPaths } from '../../test_utils'; + +describe('default_namespace', () => { + test('it should validate "single"', () => { + const payload = 'single'; + const decoded = DefaultNamespace.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate "agnostic"', () => { + const payload = 'agnostic'; + const decoded = DefaultNamespace.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it defaults to "single" if "undefined"', () => { + const payload = undefined; + const decoded = DefaultNamespace.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual('single'); + }); + + test('it defaults to "single" if "null"', () => { + const payload = null; + const decoded = DefaultNamespace.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual('single'); + }); + + test('it should FAIL validation if not "single" or "agnostic"', () => { + const payload = 'something else'; + const decoded = DefaultNamespace.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + `Invalid value "something else" supplied to "DefaultNamespace"`, + ]); + expect(message.schema).toEqual({}); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_namespace/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_namespace/index.ts new file mode 100644 index 0000000000000..14c516da0b6f0 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_namespace/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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; + +export const namespaceType = t.keyof({ agnostic: null, single: null }); +export type NamespaceType = t.TypeOf; + +/** + * Types the DefaultNamespace as: + * - If null or undefined, then a default string/enumeration of "single" will be used. + */ +export const DefaultNamespace = new t.Type( + 'DefaultNamespace', + namespaceType.is, + (input, context): Either => + input == null ? t.success('single') : namespaceType.validate(input, context), + t.identity +); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_namespace_array/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_namespace_array/index.test.ts new file mode 100644 index 0000000000000..8bc7a16b96097 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_namespace_array/index.test.ts @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { DefaultNamespaceArray, DefaultNamespaceArrayType } from '.'; +import { foldLeftRight, getPaths } from '../../test_utils'; + +describe('default_namespace_array', () => { + test('it should validate "null" single item as an array with a "single" value', () => { + const payload: DefaultNamespaceArrayType = null; + const decoded = DefaultNamespaceArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(['single']); + }); + + test('it should FAIL validation of numeric value', () => { + const payload = 5; + const decoded = DefaultNamespaceArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "5" supplied to "DefaultNamespaceArray"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should validate "undefined" item as an array with a "single" value', () => { + const payload: DefaultNamespaceArrayType = undefined; + const decoded = DefaultNamespaceArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(['single']); + }); + + test('it should validate "single" as an array of a "single" value', () => { + const payload: DefaultNamespaceArrayType = 'single'; + const decoded = DefaultNamespaceArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual([payload]); + }); + + test('it should validate "agnostic" as an array of a "agnostic" value', () => { + const payload: DefaultNamespaceArrayType = 'agnostic'; + const decoded = DefaultNamespaceArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual([payload]); + }); + + test('it should validate "single,agnostic" as an array of 2 values of ["single", "agnostic"] values', () => { + const payload: DefaultNamespaceArrayType = 'agnostic,single'; + const decoded = DefaultNamespaceArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(['agnostic', 'single']); + }); + + test('it should validate 3 elements of "single,agnostic,single" as an array of 3 values of ["single", "agnostic", "single"] values', () => { + const payload: DefaultNamespaceArrayType = 'single,agnostic,single'; + const decoded = DefaultNamespaceArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(['single', 'agnostic', 'single']); + }); + + test('it should validate 3 elements of "single,agnostic, single" as an array of 3 values of ["single", "agnostic", "single"] values when there are spaces', () => { + const payload: DefaultNamespaceArrayType = ' single, agnostic, single '; + const decoded = DefaultNamespaceArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(['single', 'agnostic', 'single']); + }); + + test('it should FAIL validation when given 3 elements of "single,agnostic,junk" since the 3rd value is junk', () => { + const payload: DefaultNamespaceArrayType = 'single,agnostic,junk'; + const decoded = DefaultNamespaceArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "junk" supplied to "DefaultNamespaceArray"', + ]); + expect(message.schema).toEqual({}); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_namespace_array/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_namespace_array/index.ts new file mode 100644 index 0000000000000..75f0cb459119b --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_namespace_array/index.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; +import { namespaceType } from '../default_namespace'; + +export const namespaceTypeArray = t.array(namespaceType); +export type NamespaceTypeArray = t.TypeOf; + +/** + * Types the DefaultNamespaceArray as: + * - If null or undefined, then a default string array of "single" will be used. + * - If it contains a string, then it is split along the commas and puts them into an array and validates it + */ +export const DefaultNamespaceArray = new t.Type< + NamespaceTypeArray, + string | undefined | null, + unknown +>( + 'DefaultNamespaceArray', + namespaceTypeArray.is, + (input, context): Either => { + if (input == null) { + return t.success(['single']); + } else if (typeof input === 'string') { + const commaSeparatedValues = input + .trim() + .split(',') + .map((value) => value.trim()); + return namespaceTypeArray.validate(commaSeparatedValues, context); + } + return t.failure(input, context); + }, + String +); + +export type DefaultNamespaceArrayType = t.OutputOf; +export type DefaultNamespaceArrayTypeDecoded = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_update_comments_array/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_update_comments_array/index.test.ts new file mode 100644 index 0000000000000..f52baa49530ec --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_update_comments_array/index.test.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { UpdateCommentsArray } from '../update_comment'; +import { DefaultUpdateCommentsArray } from '.'; +import { foldLeftRight, getPaths } from '../../test_utils'; +import { getUpdateCommentsArrayMock } from '../update_comment/index.mock'; + +describe('default_update_comments_array', () => { + test('it should pass validation when supplied an empty array', () => { + const payload: UpdateCommentsArray = []; + const decoded = DefaultUpdateCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should pass validation when supplied an array of comments', () => { + const payload: UpdateCommentsArray = getUpdateCommentsArrayMock(); + const decoded = DefaultUpdateCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should fail validation when supplied an array of numbers', () => { + const payload = [1]; + const decoded = DefaultUpdateCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1" supplied to "Array<({| comment: NonEmptyString |} & Partial<{| id: NonEmptyString |}>)>"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should fail validation when supplied an array of strings', () => { + const payload = ['some string']; + const decoded = DefaultUpdateCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "some string" supplied to "Array<({| comment: NonEmptyString |} & Partial<{| id: NonEmptyString |}>)>"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should return a default array entry', () => { + const payload = null; + const decoded = DefaultUpdateCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual([]); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_update_comments_array/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_update_comments_array/index.ts new file mode 100644 index 0000000000000..26bfdad597100 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/default_update_comments_array/index.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; +import { updateCommentsArray, UpdateCommentsArray } from '../update_comment'; + +/** + * Types the DefaultCommentsUpdate as: + * - If null or undefined, then a default array of type entry will be set + */ +export const DefaultUpdateCommentsArray = new t.Type< + UpdateCommentsArray, + UpdateCommentsArray, + unknown +>( + 'DefaultCreateComments', + updateCommentsArray.is, + (input): Either => + input == null ? t.success([]) : updateCommentsArray.decode(input), + t.identity +); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entries/index.mock.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entries/index.mock.ts new file mode 100644 index 0000000000000..e491b50b0f9c8 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entries/index.mock.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { EndpointEntriesArray } from '.'; +import { getEndpointEntryMatchMock } from '../entry_match/index.mock'; +import { getEndpointEntryMatchAnyMock } from '../entry_match_any/index.mock'; +import { getEndpointEntryNestedMock } from '../entry_nested/index.mock'; + +export const getEndpointEntriesArrayMock = (): EndpointEntriesArray => [ + getEndpointEntryMatchMock(), + getEndpointEntryMatchAnyMock(), + getEndpointEntryNestedMock(), +]; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entries/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entries/index.test.ts new file mode 100644 index 0000000000000..f5cb89ee79607 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entries/index.test.ts @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { getEndpointEntryMatchMock } from '../entry_match/index.mock'; +import { + endpointEntriesArray, + nonEmptyEndpointEntriesArray, + NonEmptyEndpointEntriesArray, +} from '.'; +import { foldLeftRight, getPaths } from '../../../test_utils'; +import { getEndpointEntryMatchAnyMock } from '../entry_match_any/index.mock'; +import { getEndpointEntryNestedMock } from '../entry_nested/index.mock'; +import { getEndpointEntriesArrayMock } from './index.mock'; +import { getEntryListMock } from '../../entries_list/index.mock'; +import { getEntryExistsMock } from '../../entries_exist/index.mock'; + +describe('Endpoint', () => { + describe('entriesArray', () => { + test('it should validate an array with match entry', () => { + const payload = [getEndpointEntryMatchMock()]; + const decoded = endpointEntriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate an array with match_any entry', () => { + const payload = [getEndpointEntryMatchAnyMock()]; + const decoded = endpointEntriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should NOT validate an empty array', () => { + const payload: NonEmptyEndpointEntriesArray = []; + const decoded = nonEmptyEndpointEntriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "[]" supplied to "NonEmptyEndpointEntriesArray"', + ]); + expect(message.schema).toEqual({}); + }); + + test('type guard for nonEmptyEndpointNestedEntries should allow array of endpoint entries', () => { + const payload: NonEmptyEndpointEntriesArray = [getEndpointEntryMatchAnyMock()]; + const guarded = nonEmptyEndpointEntriesArray.is(payload); + expect(guarded).toBeTruthy(); + }); + + test('type guard for nonEmptyEndpointNestedEntries should disallow empty arrays', () => { + const payload: NonEmptyEndpointEntriesArray = []; + const guarded = nonEmptyEndpointEntriesArray.is(payload); + expect(guarded).toBeFalsy(); + }); + + test('it should NOT validate an array with exists entry', () => { + const payload = [getEntryExistsMock()]; + const decoded = endpointEntriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "exists" supplied to "type"', + 'Invalid value "undefined" supplied to "value"', + 'Invalid value "undefined" supplied to "entries"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate an array with list entry', () => { + const payload = [getEntryListMock()]; + const decoded = endpointEntriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "list" supplied to "type"', + 'Invalid value "undefined" supplied to "value"', + 'Invalid value "undefined" supplied to "entries"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should validate an array with nested entry', () => { + const payload = [getEndpointEntryNestedMock()]; + const decoded = endpointEntriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate an array with all types of entries', () => { + const payload = getEndpointEntriesArrayMock(); + const decoded = endpointEntriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entries/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entries/index.ts new file mode 100644 index 0000000000000..451131dafc459 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entries/index.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; +import { endpointEntryMatch } from '../entry_match'; +import { endpointEntryMatchAny } from '../entry_match_any'; +import { endpointEntryNested } from '../entry_nested'; + +export const endpointEntriesArray = t.array( + t.union([endpointEntryMatch, endpointEntryMatchAny, endpointEntryNested]) +); +export type EndpointEntriesArray = t.TypeOf; + +/** + * Types the nonEmptyEndpointEntriesArray as: + * - An array of entries of length 1 or greater + * + */ +export const nonEmptyEndpointEntriesArray = new t.Type< + EndpointEntriesArray, + EndpointEntriesArray, + unknown +>( + 'NonEmptyEndpointEntriesArray', + (u: unknown): u is EndpointEntriesArray => endpointEntriesArray.is(u) && u.length > 0, + (input, context): Either => { + if (Array.isArray(input) && input.length === 0) { + return t.failure(input, context); + } else { + return endpointEntriesArray.validate(input, context); + } + }, + t.identity +); + +export type NonEmptyEndpointEntriesArray = t.OutputOf; +export type NonEmptyEndpointEntriesArrayDecoded = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match/index.mock.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match/index.mock.ts new file mode 100644 index 0000000000000..7104406c4869c --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match/index.mock.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { EndpointEntryMatch } from '.'; +import { ENTRY_VALUE, FIELD, MATCH, OPERATOR } from '../../../constants/index.mock'; + +export const getEndpointEntryMatchMock = (): EndpointEntryMatch => ({ + field: FIELD, + operator: OPERATOR, + type: MATCH, + value: ENTRY_VALUE, +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match/index.test.ts new file mode 100644 index 0000000000000..cc0423fc119c7 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match/index.test.ts @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { getEndpointEntryMatchMock } from './index.mock'; +import { EndpointEntryMatch, endpointEntryMatch } from '.'; +import { foldLeftRight, getPaths } from '../../../test_utils'; +import { getEntryMatchMock } from '../../entry_match/index.mock'; + +describe('endpointEntryMatch', () => { + test('it should validate an entry', () => { + const payload = getEndpointEntryMatchMock(); + const decoded = endpointEntryMatch.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should NOT validate when "operator" is "excluded"', () => { + // Use the generic entry mock so we can test operator: excluded + const payload = getEntryMatchMock(); + payload.operator = 'excluded'; + const decoded = endpointEntryMatch.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "excluded" supplied to "operator"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when "field" is empty string', () => { + const payload: Omit & { field: string } = { + ...getEndpointEntryMatchMock(), + field: '', + }; + const decoded = endpointEntryMatch.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "field"']); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when "value" is not string', () => { + const payload: Omit & { value: string[] } = { + ...getEndpointEntryMatchMock(), + value: ['some value'], + }; + const decoded = endpointEntryMatch.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "["some value"]" supplied to "value"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when "value" is empty string', () => { + const payload: Omit & { value: string } = { + ...getEndpointEntryMatchMock(), + value: '', + }; + const decoded = endpointEntryMatch.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "value"']); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when "type" is not "match"', () => { + const payload: Omit & { type: string } = { + ...getEndpointEntryMatchMock(), + type: 'match_any', + }; + const decoded = endpointEntryMatch.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "match_any" supplied to "type"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should strip out extra keys', () => { + const payload: EndpointEntryMatch & { + extraKey?: string; + } = getEndpointEntryMatchMock(); + payload.extraKey = 'some value'; + const decoded = endpointEntryMatch.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(getEntryMatchMock()); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match/index.ts new file mode 100644 index 0000000000000..83e2a0f61bb4a --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match/index.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { operatorIncluded } from '../../../operator'; +import { NonEmptyString } from '../../../non_empty_string'; + +export const endpointEntryMatch = t.exact( + t.type({ + field: NonEmptyString, + operator: operatorIncluded, + type: t.keyof({ match: null }), + value: NonEmptyString, + }) +); +export type EndpointEntryMatch = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match_any/index.mock.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match_any/index.mock.ts new file mode 100644 index 0000000000000..95bd6008f1d7c --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match_any/index.mock.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ENTRY_VALUE, FIELD, MATCH_ANY, OPERATOR } from '../../../constants/index.mock'; +import { EndpointEntryMatchAny } from '.'; + +export const getEndpointEntryMatchAnyMock = (): EndpointEntryMatchAny => ({ + field: FIELD, + operator: OPERATOR, + type: MATCH_ANY, + value: [ENTRY_VALUE], +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match_any/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match_any/index.test.ts new file mode 100644 index 0000000000000..0fd878986d5a2 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match_any/index.test.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { getEndpointEntryMatchAnyMock } from './index.mock'; +import { EndpointEntryMatchAny, endpointEntryMatchAny } from '.'; +import { foldLeftRight, getPaths } from '../../../test_utils'; +import { getEntryMatchAnyMock } from '../../entry_match_any/index.mock'; + +describe('endpointEntryMatchAny', () => { + test('it should validate an entry', () => { + const payload = getEndpointEntryMatchAnyMock(); + const decoded = endpointEntryMatchAny.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should NOT validate when operator is "excluded"', () => { + // Use the generic entry mock so we can test operator: excluded + const payload = getEntryMatchAnyMock(); + payload.operator = 'excluded'; + const decoded = endpointEntryMatchAny.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "excluded" supplied to "operator"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when field is empty string', () => { + const payload: Omit & { field: string } = { + ...getEndpointEntryMatchAnyMock(), + field: '', + }; + const decoded = endpointEntryMatchAny.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "field"']); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when value is empty array', () => { + const payload: Omit & { value: string[] } = { + ...getEndpointEntryMatchAnyMock(), + value: [], + }; + const decoded = endpointEntryMatchAny.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "[]" supplied to "value"']); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when value is not string array', () => { + const payload: Omit & { value: string } = { + ...getEndpointEntryMatchAnyMock(), + value: 'some string', + }; + const decoded = endpointEntryMatchAny.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "some string" supplied to "value"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when "type" is not "match_any"', () => { + const payload: Omit & { type: string } = { + ...getEndpointEntryMatchAnyMock(), + type: 'match', + }; + const decoded = endpointEntryMatchAny.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "match" supplied to "type"']); + expect(message.schema).toEqual({}); + }); + + test('it should strip out extra keys', () => { + const payload: EndpointEntryMatchAny & { + extraKey?: string; + } = getEndpointEntryMatchAnyMock(); + payload.extraKey = 'some extra key'; + const decoded = endpointEntryMatchAny.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(getEntryMatchAnyMock()); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match_any/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match_any/index.ts new file mode 100644 index 0000000000000..b39a428bb49dd --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match_any/index.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { nonEmptyOrNullableStringArray } from '../../../non_empty_or_nullable_string_array'; +import { operatorIncluded } from '../../../operator'; +import { NonEmptyString } from '../../../non_empty_string'; + +export const endpointEntryMatchAny = t.exact( + t.type({ + field: NonEmptyString, + operator: operatorIncluded, + type: t.keyof({ match_any: null }), + value: nonEmptyOrNullableStringArray, + }) +); +export type EndpointEntryMatchAny = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match_wildcard/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match_wildcard/index.ts new file mode 100644 index 0000000000000..b66c5a2588eef --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_match_wildcard/index.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { operatorIncluded } from '../../../operator'; +import { NonEmptyString } from '../../../non_empty_string'; + +export const endpointEntryMatchWildcard = t.exact( + t.type({ + field: NonEmptyString, + operator: operatorIncluded, + type: t.keyof({ wildcard: null }), + value: NonEmptyString, + }) +); +export type EndpointEntryMatchWildcard = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_nested/index.mock.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_nested/index.mock.ts new file mode 100644 index 0000000000000..f59e29c8ce526 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_nested/index.mock.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { EndpointEntryNested } from '.'; +import { FIELD, NESTED } from '../../../constants/index.mock'; +import { getEndpointEntryMatchMock } from '../entry_match/index.mock'; +import { getEndpointEntryMatchAnyMock } from '../entry_match_any/index.mock'; + +export const getEndpointEntryNestedMock = (): EndpointEntryNested => ({ + entries: [getEndpointEntryMatchMock(), getEndpointEntryMatchAnyMock()], + field: FIELD, + type: NESTED, +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_nested/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_nested/index.test.ts new file mode 100644 index 0000000000000..03c02f67b71ad --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_nested/index.test.ts @@ -0,0 +1,137 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { EndpointEntryNested, endpointEntryNested } from '.'; +import { foldLeftRight, getPaths } from '../../../test_utils'; +import { getEndpointEntryNestedMock } from './index.mock'; +import { getEndpointEntryMatchAnyMock } from '../entry_match_any/index.mock'; +import { + nonEmptyEndpointNestedEntriesArray, + NonEmptyEndpointNestedEntriesArray, +} from '../non_empty_nested_entries_array'; +import { getEndpointEntryMatchMock } from '../entry_match/index.mock'; +import { getEntryExistsMock } from '../../entries_exist/index.mock'; + +describe('endpointEntryNested', () => { + test('it should validate a nested entry', () => { + const payload = getEndpointEntryNestedMock(); + const decoded = endpointEntryNested.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should FAIL validation when "type" is not "nested"', () => { + const payload: Omit & { type: 'match' } = { + ...getEndpointEntryNestedMock(), + type: 'match', + }; + const decoded = endpointEntryNested.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "match" supplied to "type"']); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when "field" is empty string', () => { + const payload: Omit & { + field: string; + } = { ...getEndpointEntryNestedMock(), field: '' }; + const decoded = endpointEntryNested.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "field"']); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when "field" is not a string', () => { + const payload: Omit & { + field: number; + } = { ...getEndpointEntryNestedMock(), field: 1 }; + const decoded = endpointEntryNested.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "1" supplied to "field"']); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when "entries" is not an array', () => { + const payload: Omit & { + entries: string; + } = { ...getEndpointEntryNestedMock(), entries: 'im a string' }; + const decoded = endpointEntryNested.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "im a string" supplied to "entries"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should validate when "entries" contains an entry item that is type "match"', () => { + const payload = { ...getEndpointEntryNestedMock(), entries: [getEndpointEntryMatchAnyMock()] }; + const decoded = endpointEntryNested.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual({ + entries: [ + { + field: 'host.name', + operator: 'included', + type: 'match_any', + value: ['some host name'], + }, + ], + field: 'host.name', + type: 'nested', + }); + }); + + test('it should NOT validate when "entries" contains an entry item that is type "exists"', () => { + const payload = { ...getEndpointEntryNestedMock(), entries: [getEntryExistsMock()] }; + const decoded = endpointEntryNested.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "exists" supplied to "entries,type"', + 'Invalid value "undefined" supplied to "entries,value"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should strip out extra keys', () => { + const payload: EndpointEntryNested & { + extraKey?: string; + } = getEndpointEntryNestedMock(); + payload.extraKey = 'some extra key'; + const decoded = endpointEntryNested.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(getEndpointEntryNestedMock()); + }); + + test('type guard for nonEmptyEndpointNestedEntries should allow array of endpoint entries', () => { + const payload: NonEmptyEndpointNestedEntriesArray = [ + getEndpointEntryMatchMock(), + getEndpointEntryMatchAnyMock(), + ]; + const guarded = nonEmptyEndpointNestedEntriesArray.is(payload); + expect(guarded).toBeTruthy(); + }); + + test('type guard for nonEmptyEndpointNestedEntries should disallow empty arrays', () => { + const payload: NonEmptyEndpointNestedEntriesArray = []; + const guarded = nonEmptyEndpointNestedEntriesArray.is(payload); + expect(guarded).toBeFalsy(); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_nested/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_nested/index.ts new file mode 100644 index 0000000000000..249dcc9077b34 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/entry_nested/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { NonEmptyString } from '../../../non_empty_string'; +import { nonEmptyEndpointNestedEntriesArray } from '../non_empty_nested_entries_array'; + +export const endpointEntryNested = t.exact( + t.type({ + entries: nonEmptyEndpointNestedEntriesArray, + field: NonEmptyString, + type: t.keyof({ nested: null }), + }) +); +export type EndpointEntryNested = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/index.ts new file mode 100644 index 0000000000000..212b5de1470ff --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +export * from './entries'; +export * from './entry_match'; +export * from './entry_match_any'; +export * from './entry_match_wildcard'; +export * from './entry_nested'; +export * from './non_empty_nested_entries_array'; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/non_empty_nested_entries_array/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/non_empty_nested_entries_array/index.ts new file mode 100644 index 0000000000000..7714476921e68 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/endpoint/non_empty_nested_entries_array/index.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; +import { endpointEntryMatch } from '../entry_match'; +import { endpointEntryMatchAny } from '../entry_match_any'; + +export const endpointNestedEntriesArray = t.array( + t.union([endpointEntryMatch, endpointEntryMatchAny]) +); +export type EndpointNestedEntriesArray = t.TypeOf; + +/** + * Types the nonEmptyNestedEntriesArray as: + * - An array of entries of length 1 or greater + * + */ +export const nonEmptyEndpointNestedEntriesArray = new t.Type< + EndpointNestedEntriesArray, + EndpointNestedEntriesArray, + unknown +>( + 'NonEmptyEndpointNestedEntriesArray', + (u: unknown): u is EndpointNestedEntriesArray => endpointNestedEntriesArray.is(u) && u.length > 0, + (input, context): Either => { + if (Array.isArray(input) && input.length === 0) { + return t.failure(input, context); + } else { + return endpointNestedEntriesArray.validate(input, context); + } + }, + t.identity +); + +export type NonEmptyEndpointNestedEntriesArray = t.OutputOf< + typeof nonEmptyEndpointNestedEntriesArray +>; +export type NonEmptyEndpointNestedEntriesArrayDecoded = t.TypeOf< + typeof nonEmptyEndpointNestedEntriesArray +>; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries/index.mock.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries/index.mock.ts new file mode 100644 index 0000000000000..77d577b7aed96 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries/index.mock.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { EntriesArray } from '.'; +import { getEntryExistsMock } from '../entries_exist/index.mock'; +import { getEntryListMock } from '../entries_list/index.mock'; +import { getEntryMatchMock } from '../entry_match/index.mock'; +import { getEntryMatchAnyMock } from '../entry_match_any/index.mock'; +import { getEntryNestedMock } from '../entry_nested/index.mock'; + +export const getListAndNonListEntriesArrayMock = (): EntriesArray => [ + getEntryMatchMock(), + getEntryMatchAnyMock(), + getEntryListMock(), + getEntryExistsMock(), + getEntryNestedMock(), +]; + +export const getListEntriesArrayMock = (): EntriesArray => [getEntryListMock(), getEntryListMock()]; + +export const getEntriesArrayMock = (): EntriesArray => [ + getEntryMatchMock(), + getEntryMatchAnyMock(), + getEntryExistsMock(), + getEntryNestedMock(), +]; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries/index.test.ts new file mode 100644 index 0000000000000..b6e448f94ce6a --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries/index.test.ts @@ -0,0 +1,148 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { getEntryMatchMock } from '../entry_match/index.mock'; +import { entriesArray, entriesArrayOrUndefined, entry } from '.'; +import { foldLeftRight, getPaths } from '../../test_utils'; +import { getEntryMatchAnyMock } from '../entry_match_any/index.mock'; +import { getEntryExistsMock } from '../entries_exist/index.mock'; +import { getEntryListMock } from '../entries_list/index.mock'; +import { getEntryNestedMock } from '../entry_nested/index.mock'; +import { getEntriesArrayMock } from './index.mock'; + +describe('Entries', () => { + describe('entry', () => { + test('it should validate a match entry', () => { + const payload = getEntryMatchMock(); + const decoded = entry.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate a match_any entry', () => { + const payload = getEntryMatchAnyMock(); + const decoded = entry.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate a exists entry', () => { + const payload = getEntryExistsMock(); + const decoded = entry.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate a list entry', () => { + const payload = getEntryListMock(); + const decoded = entry.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should FAIL validation of nested entry', () => { + const payload = getEntryNestedMock(); + const decoded = entry.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "operator"', + 'Invalid value "nested" supplied to "type"', + 'Invalid value "undefined" supplied to "value"', + 'Invalid value "undefined" supplied to "list"', + ]); + expect(message.schema).toEqual({}); + }); + }); + + describe('entriesArray', () => { + test('it should validate an array with match entry', () => { + const payload = [getEntryMatchMock()]; + const decoded = entriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate an array with match_any entry', () => { + const payload = [getEntryMatchAnyMock()]; + const decoded = entriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate an array with exists entry', () => { + const payload = [getEntryExistsMock()]; + const decoded = entriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate an array with list entry', () => { + const payload = [getEntryListMock()]; + const decoded = entriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate an array with nested entry', () => { + const payload = [getEntryNestedMock()]; + const decoded = entriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate an array with all types of entries', () => { + const payload = [...getEntriesArrayMock()]; + const decoded = entriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + }); + + describe('entriesArrayOrUndefined', () => { + test('it should validate undefined', () => { + const payload = undefined; + const decoded = entriesArrayOrUndefined.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate an array with nested entry', () => { + const payload = [getEntryNestedMock()]; + const decoded = entriesArrayOrUndefined.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries/index.ts new file mode 100644 index 0000000000000..5164c62e3f5e0 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries/index.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { entriesExists } from '../entries_exist'; +import { entriesList } from '../entries_list'; +import { entriesMatch } from '../entry_match'; +import { entriesMatchAny } from '../entry_match_any'; +import { entriesMatchWildcard } from '../entry_match_wildcard'; +import { entriesNested } from '../entry_nested'; + +// NOTE: Type nested is not included here to denote it's non-recursive nature. +// So a nested entry is really just a collection of `Entry` types. +export const entry = t.union([ + entriesMatch, + entriesMatchAny, + entriesList, + entriesExists, + entriesMatchWildcard, +]); +export type Entry = t.TypeOf; + +export const entriesArray = t.array( + t.union([ + entriesMatch, + entriesMatchAny, + entriesList, + entriesExists, + entriesNested, + entriesMatchWildcard, + ]) +); +export type EntriesArray = t.TypeOf; + +export const entriesArrayOrUndefined = t.union([entriesArray, t.undefined]); +export type EntriesArrayOrUndefined = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_exist/index.mock.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_exist/index.mock.ts new file mode 100644 index 0000000000000..0882883f4d239 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_exist/index.mock.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { EntryExists } from '.'; +import { EXISTS, FIELD, OPERATOR } from '../../constants/index.mock'; + +export const getEntryExistsMock = (): EntryExists => ({ + field: FIELD, + operator: OPERATOR, + type: EXISTS, +}); + +export const getEntryExistsExcludedMock = (): EntryExists => ({ + ...getEntryExistsMock(), + operator: 'excluded', +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_exist/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_exist/index.test.ts new file mode 100644 index 0000000000000..db4edb54dfc29 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_exist/index.test.ts @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { getEntryExistsMock } from './index.mock'; +import { entriesExists, EntryExists } from '.'; +import { foldLeftRight, getPaths } from '../../test_utils'; + +describe('entriesExists', () => { + test('it should validate an entry', () => { + const payload = getEntryExistsMock(); + const decoded = entriesExists.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate when "operator" is "included"', () => { + const payload = getEntryExistsMock(); + const decoded = entriesExists.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate when "operator" is "excluded"', () => { + const payload = getEntryExistsMock(); + payload.operator = 'excluded'; + const decoded = entriesExists.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should FAIL validation when "field" is empty string', () => { + const payload: Omit & { field: string } = { + ...getEntryExistsMock(), + field: '', + }; + const decoded = entriesExists.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "field"']); + expect(message.schema).toEqual({}); + }); + + test('it should strip out extra keys', () => { + const payload: EntryExists & { + extraKey?: string; + } = getEntryExistsMock(); + payload.extraKey = 'some extra key'; + const decoded = entriesExists.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(getEntryExistsMock()); + }); + + test('it should FAIL validation when "type" is not "exists"', () => { + const payload: Omit & { type: string } = { + ...getEntryExistsMock(), + type: 'match', + }; + const decoded = entriesExists.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "match" supplied to "type"']); + expect(message.schema).toEqual({}); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_exist/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_exist/index.ts new file mode 100644 index 0000000000000..79c58944ea3f5 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_exist/index.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; + +import { operator } from '../operator'; +import { NonEmptyString } from '../../non_empty_string'; + +export const entriesExists = t.exact( + t.type({ + field: NonEmptyString, + operator, + type: t.keyof({ exists: null }), + }) +); +export type EntryExists = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_list/index.mock.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_list/index.mock.ts new file mode 100644 index 0000000000000..c4afb28f5ac54 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_list/index.mock.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { EntryList } from '.'; +import { FIELD, LIST, LIST_ID, OPERATOR, TYPE } from '../../constants/index.mock'; + +export const getEntryListMock = (): EntryList => ({ + field: FIELD, + list: { id: LIST_ID, type: TYPE }, + operator: OPERATOR, + type: LIST, +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_list/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_list/index.test.ts new file mode 100644 index 0000000000000..2be3803c356de --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_list/index.test.ts @@ -0,0 +1,96 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { getEntryListMock } from './index.mock'; +import { entriesList, EntryList } from '.'; + +import { foldLeftRight, getPaths } from '../../test_utils'; + +describe('entriesList', () => { + test('it should validate an entry', () => { + const payload = getEntryListMock(); + const decoded = entriesList.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate when operator is "included"', () => { + const payload = getEntryListMock(); + const decoded = entriesList.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate when "operator" is "excluded"', () => { + const payload = getEntryListMock(); + payload.operator = 'excluded'; + const decoded = entriesList.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should FAIL validation when "list" is not expected value', () => { + const payload: Omit & { list: string } = { + ...getEntryListMock(), + list: 'someListId', + }; + const decoded = entriesList.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "someListId" supplied to "list"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when "list.id" is empty string', () => { + const payload: Omit & { list: { id: string; type: 'ip' } } = { + ...getEntryListMock(), + list: { id: '', type: 'ip' }, + }; + const decoded = entriesList.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "list,id"']); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when "type" is not "lists"', () => { + const payload: Omit & { type: 'match_any' } = { + ...getEntryListMock(), + type: 'match_any', + }; + const decoded = entriesList.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "match_any" supplied to "type"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should strip out extra keys', () => { + const payload: EntryList & { + extraKey?: string; + } = getEntryListMock(); + payload.extraKey = 'some extra key'; + const decoded = entriesList.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(getEntryListMock()); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_list/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_list/index.ts new file mode 100644 index 0000000000000..f5c662a67158b --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entries_list/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { NonEmptyString } from '../../non_empty_string'; + +import { type } from '../type'; +import { operator } from '../operator'; + +export const entriesList = t.exact( + t.type({ + field: NonEmptyString, + list: t.exact(t.type({ id: NonEmptyString, type })), + operator, + type: t.keyof({ list: null }), + }) +); +export type EntryList = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match/index.mock.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match/index.mock.ts new file mode 100644 index 0000000000000..4fdd8d915fe04 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match/index.mock.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { EntryMatch } from '.'; +import { ENTRY_VALUE, FIELD, MATCH, OPERATOR } from '../../constants/index.mock'; + +export const getEntryMatchMock = (): EntryMatch => ({ + field: FIELD, + operator: OPERATOR, + type: MATCH, + value: ENTRY_VALUE, +}); + +export const getEntryMatchExcludeMock = (): EntryMatch => ({ + ...getEntryMatchMock(), + operator: 'excluded', +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match/index.test.ts new file mode 100644 index 0000000000000..744c74c1223df --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match/index.test.ts @@ -0,0 +1,107 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { getEntryMatchMock } from './index.mock'; +import { entriesMatch, EntryMatch } from '.'; +import { foldLeftRight, getPaths } from '../../test_utils'; + +describe('entriesMatch', () => { + test('it should validate an entry', () => { + const payload = getEntryMatchMock(); + const decoded = entriesMatch.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate when operator is "included"', () => { + const payload = getEntryMatchMock(); + const decoded = entriesMatch.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate when "operator" is "excluded"', () => { + const payload = getEntryMatchMock(); + payload.operator = 'excluded'; + const decoded = entriesMatch.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should FAIL validation when "field" is empty string', () => { + const payload: Omit & { field: string } = { + ...getEntryMatchMock(), + field: '', + }; + const decoded = entriesMatch.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "field"']); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when "value" is not string', () => { + const payload: Omit & { value: string[] } = { + ...getEntryMatchMock(), + value: ['some value'], + }; + const decoded = entriesMatch.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "["some value"]" supplied to "value"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when "value" is empty string', () => { + const payload: Omit & { value: string } = { + ...getEntryMatchMock(), + value: '', + }; + const decoded = entriesMatch.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "value"']); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when "type" is not "match"', () => { + const payload: Omit & { type: string } = { + ...getEntryMatchMock(), + type: 'match_any', + }; + const decoded = entriesMatch.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "match_any" supplied to "type"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should strip out extra keys', () => { + const payload: EntryMatch & { + extraKey?: string; + } = getEntryMatchMock(); + payload.extraKey = 'some value'; + const decoded = entriesMatch.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(getEntryMatchMock()); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match/index.ts new file mode 100644 index 0000000000000..668da1a398093 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match/index.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { NonEmptyString } from '../../non_empty_string'; +import { operator } from '../operator'; + +export const entriesMatch = t.exact( + t.type({ + field: NonEmptyString, + operator, + type: t.keyof({ match: null }), + value: NonEmptyString, + }) +); +export type EntryMatch = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_any/index.mock.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_any/index.mock.ts new file mode 100644 index 0000000000000..0022b00c604b0 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_any/index.mock.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { EntryMatchAny } from '.'; +import { ENTRY_VALUE, FIELD, MATCH_ANY, OPERATOR } from '../../constants/index.mock'; + +export const getEntryMatchAnyMock = (): EntryMatchAny => ({ + field: FIELD, + operator: OPERATOR, + type: MATCH_ANY, + value: [ENTRY_VALUE], +}); + +export const getEntryMatchAnyExcludeMock = (): EntryMatchAny => ({ + ...getEntryMatchAnyMock(), + operator: 'excluded', + value: [ENTRY_VALUE, 'some other host name'], +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_any/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_any/index.test.ts new file mode 100644 index 0000000000000..60fc4cdc26005 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_any/index.test.ts @@ -0,0 +1,105 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { getEntryMatchAnyMock } from './index.mock'; +import { entriesMatchAny, EntryMatchAny } from '.'; +import { foldLeftRight, getPaths } from '../../test_utils'; + +describe('entriesMatchAny', () => { + test('it should validate an entry', () => { + const payload = getEntryMatchAnyMock(); + const decoded = entriesMatchAny.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate when operator is "included"', () => { + const payload = getEntryMatchAnyMock(); + const decoded = entriesMatchAny.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate when operator is "excluded"', () => { + const payload = getEntryMatchAnyMock(); + payload.operator = 'excluded'; + const decoded = entriesMatchAny.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should FAIL validation when field is empty string', () => { + const payload: Omit & { field: string } = { + ...getEntryMatchAnyMock(), + field: '', + }; + const decoded = entriesMatchAny.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "field"']); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when value is empty array', () => { + const payload: Omit & { value: string[] } = { + ...getEntryMatchAnyMock(), + value: [], + }; + const decoded = entriesMatchAny.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "[]" supplied to "value"']); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when value is not string array', () => { + const payload: Omit & { value: string } = { + ...getEntryMatchAnyMock(), + value: 'some string', + }; + const decoded = entriesMatchAny.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "some string" supplied to "value"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when "type" is not "match_any"', () => { + const payload: Omit & { type: string } = { + ...getEntryMatchAnyMock(), + type: 'match', + }; + const decoded = entriesMatchAny.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "match" supplied to "type"']); + expect(message.schema).toEqual({}); + }); + + test('it should strip out extra keys', () => { + const payload: EntryMatchAny & { + extraKey?: string; + } = getEntryMatchAnyMock(); + payload.extraKey = 'some extra key'; + const decoded = entriesMatchAny.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(getEntryMatchAnyMock()); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_any/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_any/index.ts new file mode 100644 index 0000000000000..4e7690a80f470 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_any/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; + +import { operator } from '../operator'; +import { nonEmptyOrNullableStringArray } from '../../non_empty_or_nullable_string_array'; +import { NonEmptyString } from '../../non_empty_string'; + +export const entriesMatchAny = t.exact( + t.type({ + field: NonEmptyString, + operator, + type: t.keyof({ match_any: null }), + value: nonEmptyOrNullableStringArray, + }) +); +export type EntryMatchAny = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_wildcard/index.mock.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_wildcard/index.mock.ts new file mode 100644 index 0000000000000..9810fe5e9875b --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_wildcard/index.mock.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { EntryMatchWildcard } from '.'; +import { ENTRY_VALUE, FIELD, OPERATOR, WILDCARD } from '../../constants/index.mock'; + +export const getEntryMatchWildcardMock = (): EntryMatchWildcard => ({ + field: FIELD, + operator: OPERATOR, + type: WILDCARD, + value: ENTRY_VALUE, +}); + +export const getEntryMatchWildcardExcludeMock = (): EntryMatchWildcard => ({ + ...getEntryMatchWildcardMock(), + operator: 'excluded', +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_wildcard/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_wildcard/index.test.ts new file mode 100644 index 0000000000000..d9170dd60ab40 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_wildcard/index.test.ts @@ -0,0 +1,105 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { getEntryMatchWildcardMock } from './index.mock'; +import { entriesMatchWildcard, EntryMatchWildcard } from '.'; +import { foldLeftRight, getPaths } from '../../test_utils'; + +describe('entriesMatchWildcard', () => { + test('it should validate an entry', () => { + const payload = getEntryMatchWildcardMock(); + const decoded = entriesMatchWildcard.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate when operator is "included"', () => { + const payload = getEntryMatchWildcardMock(); + const decoded = entriesMatchWildcard.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate when "operator" is "excluded"', () => { + const payload = getEntryMatchWildcardMock(); + payload.operator = 'excluded'; + const decoded = entriesMatchWildcard.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should FAIL validation when "field" is empty string', () => { + const payload: Omit & { field: string } = { + ...getEntryMatchWildcardMock(), + field: '', + }; + const decoded = entriesMatchWildcard.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "field"']); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when "value" is not string', () => { + const payload: Omit & { value: string[] } = { + ...getEntryMatchWildcardMock(), + value: ['some value'], + }; + const decoded = entriesMatchWildcard.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "["some value"]" supplied to "value"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when "value" is empty string', () => { + const payload: Omit & { value: string } = { + ...getEntryMatchWildcardMock(), + value: '', + }; + const decoded = entriesMatchWildcard.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "value"']); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when "type" is not "wildcard"', () => { + const payload: Omit & { type: string } = { + ...getEntryMatchWildcardMock(), + type: 'match', + }; + const decoded = entriesMatchWildcard.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "match" supplied to "type"']); + expect(message.schema).toEqual({}); + }); + + test('it should strip out extra keys', () => { + const payload: EntryMatchWildcard & { + extraKey?: string; + } = getEntryMatchWildcardMock(); + payload.extraKey = 'some value'; + const decoded = entriesMatchWildcard.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(getEntryMatchWildcardMock()); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_wildcard/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_wildcard/index.ts new file mode 100644 index 0000000000000..100b0c665d91b --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_match_wildcard/index.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { NonEmptyString } from '../../non_empty_string'; +import { operator } from '../operator'; + +export const entriesMatchWildcard = t.exact( + t.type({ + field: NonEmptyString, + operator, + type: t.keyof({ wildcard: null }), + value: NonEmptyString, + }) +); +export type EntryMatchWildcard = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_nested/index.mock.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_nested/index.mock.ts new file mode 100644 index 0000000000000..acde4443cccb7 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_nested/index.mock.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { EntryNested } from '.'; +import { NESTED, NESTED_FIELD } from '../../constants/index.mock'; +import { getEntryExistsMock } from '../entries_exist/index.mock'; +import { getEntryMatchExcludeMock, getEntryMatchMock } from '../entry_match/index.mock'; +import { getEntryMatchAnyExcludeMock, getEntryMatchAnyMock } from '../entry_match_any/index.mock'; + +export const getEntryNestedMock = (): EntryNested => ({ + entries: [getEntryMatchMock(), getEntryMatchAnyMock()], + field: NESTED_FIELD, + type: NESTED, +}); + +export const getEntryNestedExcludeMock = (): EntryNested => ({ + ...getEntryNestedMock(), + entries: [getEntryMatchExcludeMock(), getEntryMatchAnyExcludeMock()], +}); + +export const getEntryNestedMixedEntries = (): EntryNested => ({ + ...getEntryNestedMock(), + entries: [getEntryMatchMock(), getEntryMatchAnyExcludeMock(), getEntryExistsMock()], +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_nested/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_nested/index.test.ts new file mode 100644 index 0000000000000..b6bbc4dbef4a3 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_nested/index.test.ts @@ -0,0 +1,124 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { getEntryNestedMock } from './index.mock'; +import { entriesNested, EntryNested } from '.'; +import { foldLeftRight, getPaths } from '../../test_utils'; +import { getEntryMatchAnyMock } from '../entry_match_any/index.mock'; +import { getEntryExistsMock } from '../entries_exist/index.mock'; + +describe('entriesNested', () => { + test('it should validate a nested entry', () => { + const payload = getEntryNestedMock(); + const decoded = entriesNested.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should FAIL validation when "type" is not "nested"', () => { + const payload: Omit & { type: 'match' } = { + ...getEntryNestedMock(), + type: 'match', + }; + const decoded = entriesNested.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "match" supplied to "type"']); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when "field" is empty string', () => { + const payload: Omit & { + field: string; + } = { ...getEntryNestedMock(), field: '' }; + const decoded = entriesNested.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "field"']); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when "field" is not a string', () => { + const payload: Omit & { + field: number; + } = { ...getEntryNestedMock(), field: 1 }; + const decoded = entriesNested.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "1" supplied to "field"']); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when "entries" is not a an array', () => { + const payload: Omit & { + entries: string; + } = { ...getEntryNestedMock(), entries: 'im a string' }; + const decoded = entriesNested.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "im a string" supplied to "entries"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should validate when "entries" contains an entry item that is type "match"', () => { + const payload = { ...getEntryNestedMock(), entries: [getEntryMatchAnyMock()] }; + const decoded = entriesNested.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual({ + entries: [ + { + field: 'host.name', + operator: 'included', + type: 'match_any', + value: ['some host name'], + }, + ], + field: 'parent.field', + type: 'nested', + }); + }); + + test('it should validate when "entries" contains an entry item that is type "exists"', () => { + const payload = { ...getEntryNestedMock(), entries: [getEntryExistsMock()] }; + const decoded = entriesNested.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual({ + entries: [ + { + field: 'host.name', + operator: 'included', + type: 'exists', + }, + ], + field: 'parent.field', + type: 'nested', + }); + }); + + test('it should strip out extra keys', () => { + const payload: EntryNested & { + extraKey?: string; + } = getEntryNestedMock(); + payload.extraKey = 'some extra key'; + const decoded = entriesNested.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(getEntryNestedMock()); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_nested/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_nested/index.ts new file mode 100644 index 0000000000000..ff224dd836a19 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/entry_nested/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { NonEmptyString } from '../../non_empty_string'; +import { nonEmptyNestedEntriesArray } from '../non_empty_nested_entries_array'; + +export const entriesNested = t.exact( + t.type({ + entries: nonEmptyNestedEntriesArray, + field: NonEmptyString, + type: t.keyof({ nested: null }), + }) +); +export type EntryNested = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/exception_list/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/exception_list/index.ts new file mode 100644 index 0000000000000..a68dc2fc76a96 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/exception_list/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; + +export const exceptionListType = t.keyof({ + detection: null, + endpoint: null, + endpoint_events: null, +}); +export const exceptionListTypeOrUndefined = t.union([exceptionListType, t.undefined]); +export type ExceptionListType = t.TypeOf; +export type ExceptionListTypeOrUndefined = t.TypeOf; +export enum ExceptionListTypeEnum { + DETECTION = 'detection', + ENDPOINT = 'endpoint', + ENDPOINT_EVENTS = 'endpoint_events', +} diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/exception_list_item_type/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/exception_list_item_type/index.ts new file mode 100644 index 0000000000000..d527f01825f4b --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/exception_list_item_type/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; + +export const exceptionListItemType = t.keyof({ simple: null }); +export const exceptionListItemTypeOrUndefined = t.union([exceptionListItemType, t.undefined]); +export type ExceptionListItemType = t.TypeOf; +export type ExceptionListItemTypeOrUndefined = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/index.ts new file mode 100644 index 0000000000000..652395fa651ea --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/index.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +export * from './comment'; +export * from './create_comment'; +export * from './default_comments_array'; +export * from './default_comments_array'; +export * from './default_namespace'; +export * from './default_namespace_array'; +export * from './default_update_comments_array'; +export * from './entries'; +export * from './entries_exist'; +export * from './entries_list'; +export * from './entry_match'; +export * from './entry_match_any'; +export * from './entry_match_wildcard'; +export * from './entry_nested'; +export * from './exception_list'; +export * from './exception_list_item_type'; +export * from './item_id'; +export * from './lists'; +export * from './lists_default_array'; +export * from './non_empty_entries_array'; +export * from './non_empty_nested_entries_array'; +export * from './operator'; +export * from './os_type'; +export * from './type'; +export * from './update_comment'; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/item_id/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/item_id/index.ts new file mode 100644 index 0000000000000..171db8fd60fd1 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/item_id/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/* eslint-disable @typescript-eslint/naming-convention */ + +import * as t from 'io-ts'; +import { NonEmptyString } from '../../non_empty_string'; + +export const item_id = NonEmptyString; +export type ItemId = t.TypeOf; +export const itemIdOrUndefined = t.union([item_id, t.undefined]); +export type ItemIdOrUndefined = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/lists/index.mock.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/lists/index.mock.ts new file mode 100644 index 0000000000000..c6f54b57d937b --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/lists/index.mock.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { List, ListArray } from '.'; +import { ENDPOINT_LIST_ID } from '../../constants'; + +export const getListMock = (): List => ({ + id: 'some_uuid', + list_id: 'list_id_single', + namespace_type: 'single', + type: 'detection', +}); + +export const getEndpointListMock = (): List => ({ + id: ENDPOINT_LIST_ID, + list_id: ENDPOINT_LIST_ID, + namespace_type: 'agnostic', + type: 'endpoint', +}); + +export const getListArrayMock = (): ListArray => [getListMock(), getEndpointListMock()]; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/lists/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/lists/index.test.ts new file mode 100644 index 0000000000000..77d5e72ef8bc8 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/lists/index.test.ts @@ -0,0 +1,126 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { getEndpointListMock, getListArrayMock, getListMock } from './index.mock'; +import { List, list, ListArray, listArray, ListArrayOrUndefined, listArrayOrUndefined } from '.'; +import { foldLeftRight, getPaths } from '../../test_utils'; + +describe('Lists', () => { + describe('list', () => { + test('it should validate a list', () => { + const payload = getListMock(); + const decoded = list.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate a list with "namespace_type" of "agnostic"', () => { + const payload = getEndpointListMock(); + const decoded = list.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should NOT validate a list without an "id"', () => { + const payload = getListMock(); + // @ts-expect-error + delete payload.id; + const decoded = list.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "id"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate a list without "namespace_type"', () => { + const payload = getListMock(); + // @ts-expect-error + delete payload.namespace_type; + const decoded = list.decode(payload); + const message = pipe(decoded, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "namespace_type"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should strip out extra keys', () => { + const payload: List & { + extraKey?: string; + } = getListMock(); + payload.extraKey = 'some value'; + const decoded = list.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(getListMock()); + }); + }); + + describe('listArray', () => { + test('it should validate an array of lists', () => { + const payload = getListArrayMock(); + const decoded = listArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate when unexpected type found in array', () => { + const payload = ([1] as unknown) as ListArray; + const decoded = listArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1" supplied to "Array<{| id: NonEmptyString, list_id: NonEmptyString, type: "detection" | "endpoint" | "endpoint_events", namespace_type: "agnostic" | "single" |}>"', + ]); + expect(message.schema).toEqual({}); + }); + }); + + describe('listArrayOrUndefined', () => { + test('it should validate an array of lists', () => { + const payload = getListArrayMock(); + const decoded = listArrayOrUndefined.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate when undefined', () => { + const payload = undefined; + const decoded = listArrayOrUndefined.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not allow an item that is not of type "list" in array', () => { + const payload = ([1] as unknown) as ListArrayOrUndefined; + const decoded = listArrayOrUndefined.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1" supplied to "(Array<{| id: NonEmptyString, list_id: NonEmptyString, type: "detection" | "endpoint" | "endpoint_events", namespace_type: "agnostic" | "single" |}> | undefined)"', + 'Invalid value "[1]" supplied to "(Array<{| id: NonEmptyString, list_id: NonEmptyString, type: "detection" | "endpoint" | "endpoint_events", namespace_type: "agnostic" | "single" |}> | undefined)"', + ]); + expect(message.schema).toEqual({}); + }); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/lists/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/lists/index.ts new file mode 100644 index 0000000000000..1bd1806564856 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/lists/index.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { exceptionListType } from '../exception_list'; +import { namespaceType } from '../default_namespace'; +import { NonEmptyString } from '../../non_empty_string'; + +export const list = t.exact( + t.type({ + id: NonEmptyString, + list_id: NonEmptyString, + type: exceptionListType, + namespace_type: namespaceType, + }) +); + +export type List = t.TypeOf; +export const listArray = t.array(list); +export type ListArray = t.TypeOf; +export const listArrayOrUndefined = t.union([listArray, t.undefined]); +export type ListArrayOrUndefined = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/lists_default_array/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/lists_default_array/index.test.ts new file mode 100644 index 0000000000000..03d16d8e1b5ca --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/lists_default_array/index.test.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { DefaultListArray } from '.'; +import { foldLeftRight, getPaths } from '../../test_utils'; +import { getListArrayMock } from '../lists/index.mock'; + +describe('lists_default_array', () => { + test('it should return a default array when null', () => { + const payload = null; + const decoded = DefaultListArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual([]); + }); + + test('it should return a default array when undefined', () => { + const payload = undefined; + const decoded = DefaultListArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual([]); + }); + + test('it should validate an empty array', () => { + const payload: string[] = []; + const decoded = DefaultListArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate an array of lists', () => { + const payload = getListArrayMock(); + const decoded = DefaultListArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate an array of non accepted types', () => { + // Terrible casting for purpose of tests + const payload = [1] as unknown; + const decoded = DefaultListArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1" supplied to "DefaultListArray"', + ]); + expect(message.schema).toEqual({}); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/lists_default_array/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/lists_default_array/index.ts new file mode 100644 index 0000000000000..ac0174db1eed1 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/lists_default_array/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; +import { list, ListArray } from '../lists'; + +/** + * Types the DefaultListArray as: + * - If null or undefined, then a default array of type list will be set + */ +export const DefaultListArray = new t.Type( + 'DefaultListArray', + t.array(list).is, + (input, context): Either => + input == null ? t.success([]) : t.array(list).validate(input, context), + t.identity +); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/non_empty_entries_array/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/non_empty_entries_array/index.test.ts new file mode 100644 index 0000000000000..11e6e54b344a9 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/non_empty_entries_array/index.test.ts @@ -0,0 +1,131 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { EntriesArray } from '../entries'; +import { nonEmptyEntriesArray } from '.'; +import { foldLeftRight, getPaths } from '../../test_utils'; +import { getEntryMatchMock } from '../entry_match/index.mock'; +import { getEntryMatchAnyMock } from '../entry_match_any/index.mock'; +import { getEntryExistsMock } from '../entries_exist/index.mock'; +import { + getEntriesArrayMock, + getListAndNonListEntriesArrayMock, + getListEntriesArrayMock, +} from '../entries/index.mock'; +import { getEntryNestedMock } from '../entry_nested/index.mock'; + +describe('non_empty_entries_array', () => { + test('it should FAIL validation when given an empty array', () => { + const payload: EntriesArray = []; + const decoded = nonEmptyEntriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "[]" supplied to "NonEmptyEntriesArray"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when given "undefined"', () => { + const payload = undefined; + const decoded = nonEmptyEntriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "NonEmptyEntriesArray"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when given "null"', () => { + const payload = null; + const decoded = nonEmptyEntriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "null" supplied to "NonEmptyEntriesArray"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should validate an array of "match" entries', () => { + const payload: EntriesArray = [getEntryMatchMock(), getEntryMatchMock()]; + const decoded = nonEmptyEntriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate an array of "match_any" entries', () => { + const payload: EntriesArray = [getEntryMatchAnyMock(), getEntryMatchAnyMock()]; + const decoded = nonEmptyEntriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate an array of "exists" entries', () => { + const payload: EntriesArray = [getEntryExistsMock(), getEntryExistsMock()]; + const decoded = nonEmptyEntriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate an array of "list" entries', () => { + const payload: EntriesArray = [...getListEntriesArrayMock()]; + const decoded = nonEmptyEntriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate an array of "nested" entries', () => { + const payload: EntriesArray = [getEntryNestedMock(), getEntryNestedMock()]; + const decoded = nonEmptyEntriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate an array of entries', () => { + const payload: EntriesArray = [...getEntriesArrayMock()]; + const decoded = nonEmptyEntriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should FAIL validation when given an array of entries of value list and non-value list entries', () => { + const payload: EntriesArray = [...getListAndNonListEntriesArrayMock()]; + const decoded = nonEmptyEntriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Cannot have entry of type list and other']); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when given an array of non entries', () => { + const payload = [1]; + const decoded = nonEmptyEntriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1" supplied to "NonEmptyEntriesArray"', + ]); + expect(message.schema).toEqual({}); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/non_empty_entries_array/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/non_empty_entries_array/index.ts new file mode 100644 index 0000000000000..1b1ea8806471d --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/non_empty_entries_array/index.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; +import { entriesArray, EntriesArray } from '../entries'; +import { entriesList } from '../entries_list'; + +/** + * Types the nonEmptyEntriesArray as: + * - An array of entries of length 1 or greater + * + */ +export const nonEmptyEntriesArray = new t.Type( + 'NonEmptyEntriesArray', + entriesArray.is, + (input, context): Either => { + if (Array.isArray(input) && input.length === 0) { + return t.failure(input, context); + } else { + if ( + Array.isArray(input) && + input.some((entry) => entriesList.is(entry)) && + input.some((entry) => !entriesList.is(entry)) + ) { + // fail when an exception item contains both a value list entry and a non-value list entry + return t.failure(input, context, 'Cannot have entry of type list and other'); + } + return entriesArray.validate(input, context); + } + }, + t.identity +); + +export type NonEmptyEntriesArray = t.OutputOf; +export type NonEmptyEntriesArrayDecoded = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/non_empty_nested_entries_array/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/non_empty_nested_entries_array/index.test.ts new file mode 100644 index 0000000000000..95b74a6d4fe43 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/non_empty_nested_entries_array/index.test.ts @@ -0,0 +1,116 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { EntriesArray } from '../entries'; +import { nonEmptyNestedEntriesArray } from '.'; +import { foldLeftRight, getPaths } from '../../test_utils'; +import { getEntryMatchMock } from '../entry_match/index.mock'; +import { getEntryMatchAnyMock } from '../entry_match_any/index.mock'; +import { getEntryExistsMock } from '../entries_exist/index.mock'; +import { getEntryNestedMock } from '../entry_nested/index.mock'; + +describe('non_empty_nested_entries_array', () => { + test('it should FAIL validation when given an empty array', () => { + const payload: EntriesArray = []; + const decoded = nonEmptyNestedEntriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "[]" supplied to "NonEmptyNestedEntriesArray"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when given "undefined"', () => { + const payload = undefined; + const decoded = nonEmptyNestedEntriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "NonEmptyNestedEntriesArray"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when given "null"', () => { + const payload = null; + const decoded = nonEmptyNestedEntriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "null" supplied to "NonEmptyNestedEntriesArray"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should validate an array of "match" entries', () => { + const payload: EntriesArray = [getEntryMatchMock(), getEntryMatchMock()]; + const decoded = nonEmptyNestedEntriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate an array of "match_any" entries', () => { + const payload: EntriesArray = [getEntryMatchAnyMock(), getEntryMatchAnyMock()]; + const decoded = nonEmptyNestedEntriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate an array of "exists" entries', () => { + const payload: EntriesArray = [getEntryExistsMock(), getEntryExistsMock()]; + const decoded = nonEmptyNestedEntriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should FAIL validation when given an array of "nested" entries', () => { + const payload: EntriesArray = [getEntryNestedMock(), getEntryNestedMock()]; + const decoded = nonEmptyNestedEntriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "operator"', + 'Invalid value "nested" supplied to "type"', + 'Invalid value "undefined" supplied to "value"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should validate an array of entries', () => { + const payload: EntriesArray = [ + getEntryExistsMock(), + getEntryMatchAnyMock(), + getEntryMatchMock(), + ]; + const decoded = nonEmptyNestedEntriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should FAIL validation when given an array of non entries', () => { + const payload = [1]; + const decoded = nonEmptyNestedEntriesArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1" supplied to "NonEmptyNestedEntriesArray"', + ]); + expect(message.schema).toEqual({}); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/non_empty_nested_entries_array/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/non_empty_nested_entries_array/index.ts new file mode 100644 index 0000000000000..2f18697d36d81 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/non_empty_nested_entries_array/index.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; +import { entriesMatch } from '../entry_match'; +import { entriesMatchAny } from '../entry_match_any'; +import { entriesExists } from '../entries_exist'; + +export const nestedEntryItem = t.union([entriesMatch, entriesMatchAny, entriesExists]); +export const nestedEntriesArray = t.array(nestedEntryItem); +export type NestedEntriesArray = t.TypeOf; + +/** + * Types the nonEmptyNestedEntriesArray as: + * - An array of entries of length 1 or greater + * + */ +export const nonEmptyNestedEntriesArray = new t.Type< + NestedEntriesArray, + NestedEntriesArray, + unknown +>( + 'NonEmptyNestedEntriesArray', + nestedEntriesArray.is, + (input, context): Either => { + if (Array.isArray(input) && input.length === 0) { + return t.failure(input, context); + } else { + return nestedEntriesArray.validate(input, context); + } + }, + t.identity +); + +export type NonEmptyNestedEntriesArray = t.OutputOf; +export type NonEmptyNestedEntriesArrayDecoded = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/operator/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/operator/index.ts new file mode 100644 index 0000000000000..7fe2b6e4d8ba7 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/operator/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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; + +export const operator = t.keyof({ excluded: null, included: null }); +export type Operator = t.TypeOf; +export enum OperatorEnum { + INCLUDED = 'included', + EXCLUDED = 'excluded', +} + +export enum OperatorTypeEnum { + NESTED = 'nested', + MATCH = 'match', + MATCH_ANY = 'match_any', + WILDCARD = 'wildcard', + EXISTS = 'exists', + LIST = 'list', +} diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/os_type/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/os_type/index.ts new file mode 100644 index 0000000000000..5ff60e05817d5 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/os_type/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { DefaultArray } from '../../default_array'; + +export const osType = t.keyof({ + linux: null, + macos: null, + windows: null, +}); +export type OsType = t.TypeOf; + +export const osTypeArray = DefaultArray(osType); +export type OsTypeArray = t.TypeOf; + +export const osTypeArrayOrUndefined = t.union([osTypeArray, t.undefined]); +export type OsTypeArrayOrUndefined = t.OutputOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/type/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/type/index.ts new file mode 100644 index 0000000000000..0eebf2eeaace1 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/type/index.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; + +/** + * Types of all the regular single value list items but not exception list + * or exception list types. Those types are in the list_types folder. + */ +export const type = t.keyof({ + binary: null, + boolean: null, + byte: null, + date: null, + date_nanos: null, + date_range: null, + double: null, + double_range: null, + float: null, + float_range: null, + geo_point: null, + geo_shape: null, + half_float: null, + integer: null, + integer_range: null, + ip: null, + ip_range: null, + keyword: null, + long: null, + long_range: null, + shape: null, + short: null, + text: null, +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/update_comment/index.mock.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/update_comment/index.mock.ts new file mode 100644 index 0000000000000..3b5cb256b28bf --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/update_comment/index.mock.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { UpdateComment, UpdateCommentsArray } from '.'; +import { ID } from '../../constants/index.mock'; + +export const getUpdateCommentMock = (): UpdateComment => ({ + comment: 'some comment', + id: ID, +}); + +export const getUpdateCommentsArrayMock = (): UpdateCommentsArray => [ + getUpdateCommentMock(), + getUpdateCommentMock(), +]; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/update_comment/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/update_comment/index.test.ts new file mode 100644 index 0000000000000..a6fc285f05465 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/update_comment/index.test.ts @@ -0,0 +1,149 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { getUpdateCommentMock, getUpdateCommentsArrayMock } from './index.mock'; +import { + UpdateComment, + updateComment, + UpdateCommentsArray, + updateCommentsArray, + UpdateCommentsArrayOrUndefined, + updateCommentsArrayOrUndefined, +} from '.'; +import { foldLeftRight, getPaths } from '../../test_utils'; + +describe('CommentsUpdate', () => { + describe('updateComment', () => { + test('it should pass validation when supplied typical comment update', () => { + const payload = getUpdateCommentMock(); + const decoded = updateComment.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should fail validation when supplied an undefined for "comment"', () => { + const payload = getUpdateCommentMock(); + // @ts-expect-error + delete payload.comment; + const decoded = updateComment.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "comment"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should fail validation when supplied an empty string for "comment"', () => { + const payload = { ...getUpdateCommentMock(), comment: '' }; + const decoded = updateComment.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "comment"']); + expect(message.schema).toEqual({}); + }); + + test('it should pass validation when supplied an undefined for "id"', () => { + const payload = getUpdateCommentMock(); + delete payload.id; + const decoded = updateComment.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should fail validation when supplied an empty string for "id"', () => { + const payload = { ...getUpdateCommentMock(), id: '' }; + const decoded = updateComment.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "id"']); + expect(message.schema).toEqual({}); + }); + + test('it should strip out extra key passed in', () => { + const payload: UpdateComment & { + extraKey?: string; + } = { ...getUpdateCommentMock(), extraKey: 'some new value' }; + const decoded = updateComment.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(getUpdateCommentMock()); + }); + }); + + describe('updateCommentsArray', () => { + test('it should pass validation when supplied an array of comments', () => { + const payload = getUpdateCommentsArrayMock(); + const decoded = updateCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should fail validation when undefined', () => { + const payload = undefined; + const decoded = updateCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "Array<({| comment: NonEmptyString |} & Partial<{| id: NonEmptyString |}>)>"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should fail validation when array includes non comments types', () => { + const payload = ([1] as unknown) as UpdateCommentsArray; + const decoded = updateCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1" supplied to "Array<({| comment: NonEmptyString |} & Partial<{| id: NonEmptyString |}>)>"', + ]); + expect(message.schema).toEqual({}); + }); + }); + + describe('updateCommentsArrayOrUndefined', () => { + test('it should pass validation when supplied an array of comments', () => { + const payload = getUpdateCommentsArrayMock(); + const decoded = updateCommentsArrayOrUndefined.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should pass validation when supplied when undefined', () => { + const payload = undefined; + const decoded = updateCommentsArrayOrUndefined.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should fail validation when array includes non comments types', () => { + const payload = ([1] as unknown) as UpdateCommentsArrayOrUndefined; + const decoded = updateCommentsArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "1" supplied to "Array<({| comment: NonEmptyString |} & Partial<{| id: NonEmptyString |}>)>"', + ]); + expect(message.schema).toEqual({}); + }); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/list_types/update_comment/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/list_types/update_comment/index.ts new file mode 100644 index 0000000000000..496ff07c5616f --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/list_types/update_comment/index.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { NonEmptyString } from '../../non_empty_string'; +import { id } from '../../id'; + +export const updateComment = t.intersection([ + t.exact( + t.type({ + comment: NonEmptyString, + }) + ), + t.exact( + t.partial({ + id, + }) + ), +]); + +export type UpdateComment = t.TypeOf; +export const updateCommentsArray = t.array(updateComment); +export type UpdateCommentsArray = t.TypeOf; +export const updateCommentsArrayOrUndefined = t.union([updateCommentsArray, t.undefined]); +export type UpdateCommentsArrayOrUndefined = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/max_signals/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/max_signals/index.ts new file mode 100644 index 0000000000000..4c68cb01cf00f --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/max_signals/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/* eslint-disable @typescript-eslint/naming-convention */ + +import * as t from 'io-ts'; +import { PositiveIntegerGreaterThanZero } from '../positive_integer_greater_than_zero'; + +export const max_signals = PositiveIntegerGreaterThanZero; +export type MaxSignals = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/meta/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/meta/index.ts new file mode 100644 index 0000000000000..93c38facd8f4e --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/meta/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; + +export const meta = t.object; +export type Meta = t.TypeOf; +export const metaOrUndefined = t.union([meta, t.undefined]); +export type MetaOrUndefined = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/name/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/name/index.ts new file mode 100644 index 0000000000000..585d903c861c0 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/name/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; + +export const name = t.string; +export type Name = t.TypeOf; +export const nameOrUndefined = t.union([name, t.undefined]); +export type NameOrUndefined = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/non_empty_array/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/non_empty_array/index.test.ts new file mode 100644 index 0000000000000..0ea7eb5539ba9 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/non_empty_array/index.test.ts @@ -0,0 +1,96 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { NonEmptyArray } from '.'; +import { foldLeftRight, getPaths } from '../test_utils'; + +const testSchema = t.keyof({ + valid: true, + also_valid: true, +}); +type TestSchema = t.TypeOf; + +const nonEmptyArraySchema = NonEmptyArray(testSchema, 'TestSchemaArray'); + +describe('non empty array', () => { + test('it should generate the correct name for non empty array', () => { + const newTestSchema = NonEmptyArray(testSchema); + expect(newTestSchema.name).toEqual('NonEmptyArray<"valid" | "also_valid">'); + }); + + test('it should use a supplied name override', () => { + const newTestSchema = NonEmptyArray(testSchema, 'someName'); + expect(newTestSchema.name).toEqual('someName'); + }); + + test('it should NOT validate an empty array', () => { + const payload: string[] = []; + const decoded = nonEmptyArraySchema.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "[]" supplied to "TestSchemaArray"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should validate an array of testSchema', () => { + const payload: TestSchema[] = ['valid']; + const decoded = nonEmptyArraySchema.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate an array of valid testSchema strings', () => { + const payload: TestSchema[] = ['valid', 'also_valid']; + const decoded = nonEmptyArraySchema.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate an array with a number', () => { + const payload = ['valid', 123]; + const decoded = nonEmptyArraySchema.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "123" supplied to "TestSchemaArray"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not validate an array with an invalid string', () => { + const payload = ['valid', 'invalid']; + const decoded = nonEmptyArraySchema.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "invalid" supplied to "TestSchemaArray"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not validate a null value', () => { + const payload = null; + const decoded = nonEmptyArraySchema.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "null" supplied to "TestSchemaArray"', + ]); + expect(message.schema).toEqual({}); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/non_empty_array/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/non_empty_array/index.ts new file mode 100644 index 0000000000000..5bdcbcc049683 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/non_empty_array/index.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; + +export const NonEmptyArray = ( + codec: C, + name: string = `NonEmptyArray<${codec.name}>` +) => { + const arrType = t.array(codec); + type ArrType = t.TypeOf; + return new t.Type( + name, + arrType.is, + (input, context): Either => { + if (Array.isArray(input) && input.length === 0) { + return t.failure(input, context); + } else { + return arrType.validate(input, context); + } + }, + t.identity + ); +}; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/non_empty_or_nullable_string_array/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/non_empty_or_nullable_string_array/index.test.ts new file mode 100644 index 0000000000000..fb2e91862d91e --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/non_empty_or_nullable_string_array/index.test.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { nonEmptyOrNullableStringArray } from '.'; +import { foldLeftRight, getPaths } from '../test_utils'; + +describe('nonEmptyOrNullableStringArray', () => { + test('it should FAIL validation when given an empty array', () => { + const payload: string[] = []; + const decoded = nonEmptyOrNullableStringArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "[]" supplied to "NonEmptyOrNullableStringArray"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when given "undefined"', () => { + const payload = undefined; + const decoded = nonEmptyOrNullableStringArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "NonEmptyOrNullableStringArray"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when given "null"', () => { + const payload = null; + const decoded = nonEmptyOrNullableStringArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "null" supplied to "NonEmptyOrNullableStringArray"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when given an array of with an empty string', () => { + const payload: string[] = ['im good', '']; + const decoded = nonEmptyOrNullableStringArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "["im good",""]" supplied to "NonEmptyOrNullableStringArray"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should FAIL validation when given an array of non strings', () => { + const payload = [1]; + const decoded = nonEmptyOrNullableStringArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "[1]" supplied to "NonEmptyOrNullableStringArray"', + ]); + expect(message.schema).toEqual({}); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/non_empty_or_nullable_string_array/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/non_empty_or_nullable_string_array/index.ts new file mode 100644 index 0000000000000..d6de08789b1df --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/non_empty_or_nullable_string_array/index.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; + +/** + * Types the nonEmptyOrNullableStringArray as: + * - An array of non empty strings of length 1 or greater + * - This differs from NonEmptyStringArray in that both input and output are type array + * + */ +export const nonEmptyOrNullableStringArray = new t.Type( + 'NonEmptyOrNullableStringArray', + t.array(t.string).is, + (input, context): Either => { + const emptyValueFound = Array.isArray(input) && input.some((value) => value === ''); + const nonStringValueFound = + Array.isArray(input) && input.some((value) => typeof value !== 'string'); + + if (Array.isArray(input) && (emptyValueFound || nonStringValueFound || input.length === 0)) { + return t.failure(input, context); + } else { + return t.array(t.string).validate(input, context); + } + }, + t.identity +); + +export type NonEmptyOrNullableStringArray = t.OutputOf; +export type NonEmptyOrNullableStringArrayDecoded = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/non_empty_string/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/non_empty_string/index.test.ts new file mode 100644 index 0000000000000..15c8ced8c915f --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/non_empty_string/index.test.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { foldLeftRight, getPaths } from '../test_utils'; +import { NonEmptyString } from '.'; + +describe('non_empty_string', () => { + test('it should validate a regular string', () => { + const payload = '1'; + const decoded = NonEmptyString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate a number', () => { + const payload = 5; + const decoded = NonEmptyString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "5" supplied to "NonEmptyString"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not validate an empty string', () => { + const payload = ''; + const decoded = NonEmptyString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "" supplied to "NonEmptyString"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not validate empty spaces', () => { + const payload = ' '; + const decoded = NonEmptyString.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value " " supplied to "NonEmptyString"', + ]); + expect(message.schema).toEqual({}); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/non_empty_string/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/non_empty_string/index.ts new file mode 100644 index 0000000000000..88f251af0013e --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/non_empty_string/index.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; + +/** + * Types the NonEmptyString as: + * - A string that is not empty + */ +export const NonEmptyString = new t.Type( + 'NonEmptyString', + t.string.is, + (input, context): Either => { + if (typeof input === 'string' && input.trim() !== '') { + return t.success(input); + } else { + return t.failure(input, context); + } + }, + t.identity +); + +export type NonEmptyStringC = typeof NonEmptyString; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/normalized_ml_job_id/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/normalized_ml_job_id/index.ts new file mode 100644 index 0000000000000..6c7eb0ae33ab9 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/normalized_ml_job_id/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/* eslint-disable @typescript-eslint/naming-convention */ + +import * as t from 'io-ts'; + +import { NonEmptyArray } from '../non_empty_array'; + +export const machine_learning_job_id_normalized = NonEmptyArray(t.string); +export type MachineLearningJobIdNormalized = t.TypeOf; + +export const machineLearningJobIdNormalizedOrUndefined = t.union([ + machine_learning_job_id_normalized, + t.undefined, +]); +export type MachineLearningJobIdNormalizedOrUndefined = t.TypeOf< + typeof machineLearningJobIdNormalizedOrUndefined +>; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/only_false_allowed/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/only_false_allowed/index.test.ts new file mode 100644 index 0000000000000..7f06ec2153a50 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/only_false_allowed/index.test.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { OnlyFalseAllowed } from '.'; +import { foldLeftRight, getPaths } from '../test_utils'; + +describe('only_false_allowed', () => { + test('it should validate a boolean false as false', () => { + const payload = false; + const decoded = OnlyFalseAllowed.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate a boolean true', () => { + const payload = true; + const decoded = OnlyFalseAllowed.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "true" supplied to "DefaultBooleanTrue"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not validate a number', () => { + const payload = 5; + const decoded = OnlyFalseAllowed.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "5" supplied to "DefaultBooleanTrue"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should return a default false', () => { + const payload = null; + const decoded = OnlyFalseAllowed.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(false); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/only_false_allowed/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/only_false_allowed/index.ts new file mode 100644 index 0000000000000..7a11e1e1c1c31 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/only_false_allowed/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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; + +/** + * Types the OnlyFalseAllowed as: + * - If null or undefined, then a default false will be set + * - If true is sent in then this will return an error + * - If false is sent in then this will allow it only false + */ +export const OnlyFalseAllowed = new t.Type( + 'DefaultBooleanTrue', + t.boolean.is, + (input, context): Either => { + if (input == null) { + return t.success(false); + } else { + if (typeof input === 'boolean' && input === false) { + return t.success(false); + } else { + return t.failure(input, context); + } + } + }, + t.identity +); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/operator/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/operator/index.ts new file mode 100644 index 0000000000000..516cb05c2d969 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/operator/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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; + +export const operatorIncluded = t.keyof({ included: null }); + +export const operator = t.keyof({ + equals: null, +}); +export type Operator = t.TypeOf; +export enum OperatorEnum { + EQUALS = 'equals', +} diff --git a/packages/kbn-securitysolution-io-ts-utils/src/parse_schedule_dates/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/parse_schedule_dates/index.ts new file mode 100644 index 0000000000000..d8aa9d2939589 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/parse_schedule_dates/index.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import moment from 'moment'; +import dateMath from '@elastic/datemath'; + +/** + * TODO: Move this to kbn-securitysolution-utils + * @deprecated Use the parseScheduleDates from the kbn-securitysolution-utils. + */ +export const parseScheduleDates = (time: string): moment.Moment | null => { + const isValidDateString = !isNaN(Date.parse(time)); + const isValidInput = isValidDateString || time.trim().startsWith('now'); + const formattedDate = isValidDateString + ? moment(time) + : isValidInput + ? dateMath.parse(time) + : null; + + return formattedDate ?? null; +}; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/positive_integer/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/positive_integer/index.test.ts new file mode 100644 index 0000000000000..c6c841b746089 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/positive_integer/index.test.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { PositiveInteger } from '.'; +import { foldLeftRight, getPaths } from '../test_utils'; + +describe('positive_integer_greater_than_zero', () => { + test('it should validate a positive number', () => { + const payload = 1; + const decoded = PositiveInteger.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate a zero', () => { + const payload = 0; + const decoded = PositiveInteger.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should NOT validate a negative number', () => { + const payload = -1; + const decoded = PositiveInteger.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "-1" supplied to "PositiveInteger"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate a string', () => { + const payload = 'some string'; + const decoded = PositiveInteger.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "some string" supplied to "PositiveInteger"', + ]); + expect(message.schema).toEqual({}); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/positive_integer/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/positive_integer/index.ts new file mode 100644 index 0000000000000..6cfd458766b69 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/positive_integer/index.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; + +/** + * Types the positive integer are: + * - Natural Number (positive integer and not a float), + * - zero or greater + */ +export const PositiveInteger = new t.Type( + 'PositiveInteger', + t.number.is, + (input, context): Either => { + return typeof input === 'number' && Number.isSafeInteger(input) && input >= 0 + ? t.success(input) + : t.failure(input, context); + }, + t.identity +); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/positive_integer_greater_than_zero/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/positive_integer_greater_than_zero/index.test.ts new file mode 100644 index 0000000000000..4655207a6448e --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/positive_integer_greater_than_zero/index.test.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { PositiveIntegerGreaterThanZero } from '.'; +import { foldLeftRight, getPaths } from '../test_utils'; + +describe('positive_integer_greater_than_zero', () => { + test('it should validate a positive number', () => { + const payload = 1; + const decoded = PositiveIntegerGreaterThanZero.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should NOT validate a zero', () => { + const payload = 0; + const decoded = PositiveIntegerGreaterThanZero.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "0" supplied to "PositiveIntegerGreaterThanZero"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate a negative number', () => { + const payload = -1; + const decoded = PositiveIntegerGreaterThanZero.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "-1" supplied to "PositiveIntegerGreaterThanZero"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate a string', () => { + const payload = 'some string'; + const decoded = PositiveIntegerGreaterThanZero.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "some string" supplied to "PositiveIntegerGreaterThanZero"', + ]); + expect(message.schema).toEqual({}); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/positive_integer_greater_than_zero/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/positive_integer_greater_than_zero/index.ts new file mode 100644 index 0000000000000..b94bd0af5b485 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/positive_integer_greater_than_zero/index.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; + +/** + * Types the positive integer greater than zero is: + * - Natural Number (positive integer and not a float), + * - 1 or greater + */ +export const PositiveIntegerGreaterThanZero = new t.Type( + 'PositiveIntegerGreaterThanZero', + t.number.is, + (input, context): Either => { + return typeof input === 'number' && Number.isSafeInteger(input) && input >= 1 + ? t.success(input) + : t.failure(input, context); + }, + t.identity +); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/references_default_array/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/references_default_array/index.test.ts new file mode 100644 index 0000000000000..41754a7ce0606 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/references_default_array/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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { DefaultStringArray } from '../default_string_array'; +import { foldLeftRight, getPaths } from '../test_utils'; + +describe('default_string_array', () => { + test('it should validate an empty array', () => { + const payload: string[] = []; + const decoded = DefaultStringArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate an array of strings', () => { + const payload = ['value 1', 'value 2']; + const decoded = DefaultStringArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate an array with a number', () => { + const payload = ['value 1', 5]; + const decoded = DefaultStringArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "5" supplied to "DefaultStringArray"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should return a default array entry', () => { + const payload = null; + const decoded = DefaultStringArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual([]); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/references_default_array/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/references_default_array/index.ts new file mode 100644 index 0000000000000..9e7708e8dc5f6 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/references_default_array/index.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; + +/** + * Types the ReferencesDefaultArray as: + * - If null or undefined, then a default array will be set + */ +export const ReferencesDefaultArray = new t.Type( + 'referencesWithDefaultArray', + t.array(t.string).is, + (input, context): Either => + input == null ? t.success([]) : t.array(t.string).validate(input, context), + t.identity +); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/risk_score/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/risk_score/index.test.ts new file mode 100644 index 0000000000000..bca8b92134928 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/risk_score/index.test.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { foldLeftRight, getPaths } from '../test_utils'; +import { RiskScore } from '.'; + +describe('risk_score', () => { + test('it should validate a positive number', () => { + const payload = 1; + const decoded = RiskScore.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate a zero', () => { + const payload = 0; + const decoded = RiskScore.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should NOT validate a negative number', () => { + const payload = -1; + const decoded = RiskScore.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "-1" supplied to "RiskScore"']); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate a string', () => { + const payload = 'some string'; + const decoded = RiskScore.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "some string" supplied to "RiskScore"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should NOT validate a risk score greater than 100', () => { + const payload = 101; + const decoded = RiskScore.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "101" supplied to "RiskScore"']); + expect(message.schema).toEqual({}); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/risk_score/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/risk_score/index.ts new file mode 100644 index 0000000000000..0aca7dd70ba1d --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/risk_score/index.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; + +/** + * Types the risk score as: + * - Natural Number (positive integer and not a float), + * - Between the values [0 and 100] inclusive. + */ +export const RiskScore = new t.Type( + 'RiskScore', + t.number.is, + (input, context): Either => { + return typeof input === 'number' && Number.isSafeInteger(input) && input >= 0 && input <= 100 + ? t.success(input) + : t.failure(input, context); + }, + t.identity +); + +export type RiskScoreC = typeof RiskScore; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/risk_score_mapping/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/risk_score_mapping/index.ts new file mode 100644 index 0000000000000..1d7ca20e80b3b --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/risk_score_mapping/index.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/* eslint-disable @typescript-eslint/naming-convention */ + +import * as t from 'io-ts'; +import { RiskScore } from '../risk_score'; + +import { operator } from '../operator'; + +export const riskScoreOrUndefined = t.union([RiskScore, t.undefined]); +export type RiskScoreOrUndefined = t.TypeOf; + +export const risk_score_mapping_field = t.string; +export const risk_score_mapping_value = t.string; +export const risk_score_mapping_item = t.exact( + t.type({ + field: risk_score_mapping_field, + value: risk_score_mapping_value, + operator, + risk_score: riskScoreOrUndefined, + }) +); + +export const risk_score_mapping = t.array(risk_score_mapping_item); +export type RiskScoreMapping = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/saved_object_attributes/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/saved_object_attributes/index.ts new file mode 100644 index 0000000000000..4d1d39bcd6a97 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/saved_object_attributes/index.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/* eslint-disable @typescript-eslint/naming-convention */ + +import * as t from 'io-ts'; + +/** + * TODO: This type are originally from "src/core/types/saved_objects.ts", once that is package friendly remove + * this copied type. + * + * Don't use this type, it's simply a helper type for {@link SavedObjectAttribute} + * + * @public + */ +export type SavedObjectAttributeSingle = + | string + | number + | boolean + | null + | undefined + | SavedObjectAttributes; + +/** + * TODO: This type are originally from "src/core/types/saved_objects.ts", once that is package friendly remove + * this copied type. + * + * Type definition for a Saved Object attribute value + * + * @public + */ +export type SavedObjectAttribute = SavedObjectAttributeSingle | SavedObjectAttributeSingle[]; + +/** + * TODO: This type are originally from "src/core/types/saved_objects.ts", once that is package friendly remove + * this copied type. + * + * The data for a Saved Object is stored as an object in the `attributes` + * property. + * + * @public + */ +export interface SavedObjectAttributes { + [key: string]: SavedObjectAttribute; +} + +export const saved_object_attribute_single: t.Type = t.recursion( + 'saved_object_attribute_single', + () => t.union([t.string, t.number, t.boolean, t.null, t.undefined, saved_object_attributes]) +); +export const saved_object_attribute: t.Type = t.recursion( + 'saved_object_attribute', + () => t.union([saved_object_attribute_single, t.array(saved_object_attribute_single)]) +); +export const saved_object_attributes: t.Type = t.recursion( + 'saved_object_attributes', + () => t.record(t.string, saved_object_attribute) +); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/severity/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/severity/index.ts new file mode 100644 index 0000000000000..4caafa6b6ecb2 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/severity/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; + +export const severity = t.keyof({ low: null, medium: null, high: null, critical: null }); +export type Severity = t.TypeOf; + +export const severityOrUndefined = t.union([severity, t.undefined]); +export type SeverityOrUndefined = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/severity_mapping/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/severity_mapping/index.ts new file mode 100644 index 0000000000000..9e7ee7d2831cd --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/severity_mapping/index.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/* eslint-disable @typescript-eslint/naming-convention */ + +import * as t from 'io-ts'; + +import { operator } from '../operator'; +import { severity } from '../severity'; + +export const severity_mapping_field = t.string; +export const severity_mapping_value = t.string; +export const severity_mapping_item = t.exact( + t.type({ + field: severity_mapping_field, + operator, + value: severity_mapping_value, + severity, + }) +); +export type SeverityMappingItem = t.TypeOf; + +export const severity_mapping = t.array(severity_mapping_item); +export type SeverityMapping = t.TypeOf; + +export const severityMappingOrUndefined = t.union([severity_mapping, t.undefined]); +export type SeverityMappingOrUndefined = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/string_to_positive_number/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/string_to_positive_number/index.ts new file mode 100644 index 0000000000000..c1db6dd4647a5 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/string_to_positive_number/index.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { Either, either } from 'fp-ts/lib/Either'; + +export type StringToPositiveNumberC = t.Type; + +/** + * Types the StrongToPositiveNumber as: + * - If a string this converts the string into a number + * - Ensures it is a number (and not NaN) + * - Ensures it is positive number + */ +export const StringToPositiveNumber: StringToPositiveNumberC = new t.Type( + 'StringToPositiveNumber', + t.number.is, + (input, context): Either => { + return either.chain( + t.string.validate(input, context), + (numberAsString): Either => { + const stringAsNumber = +numberAsString; + if (numberAsString.trim().length === 0 || isNaN(stringAsNumber) || stringAsNumber <= 0) { + return t.failure(input, context); + } else { + return t.success(stringAsNumber); + } + } + ); + }, + String +); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/tags/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/tags/index.ts new file mode 100644 index 0000000000000..48bcca0551352 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/tags/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; + +import { DefaultStringArray } from '../default_string_array'; + +export const tags = DefaultStringArray; +export type Tags = t.TypeOf; +export const tagsOrUndefined = t.union([tags, t.undefined]); +export type TagsOrUndefined = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/test_utils/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/test_utils/index.ts new file mode 100644 index 0000000000000..ec57671791c6b --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/test_utils/index.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { fold } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { formatErrors } from '../format_errors'; + +/* + * FUNFACT: These don't have tests as these are used within test utilities. However, I could see + * someone adding tests. If one day we see these escape tests, then they should have unit tests. + */ + +interface Message { + errors: t.Errors; + schema: T | {}; +} + +const onLeft = (errors: t.Errors): Message => { + return { schema: {}, errors }; +}; + +const onRight = (schema: T): Message => { + return { + schema, + errors: [], + }; +}; + +export const foldLeftRight = fold(onLeft, onRight); + +/** + * Convenience utility to keep the error message handling within tests to be + * very concise. + * @param validation The validation to get the errors from + */ +export const getPaths = (validation: t.Validation): string[] => { + return pipe( + validation, + fold( + (errors) => formatErrors(errors), + () => ['no errors'] + ) + ); +}; + +/** + * Convenience utility to remove text appended to links by EUI + */ +export const removeExternalLinkText = (str: string) => + str.replace(/\(opens in a new tab or window\)/g, ''); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/threat/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/threat/index.ts new file mode 100644 index 0000000000000..0e4022e3ec26e --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/threat/index.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/* eslint-disable @typescript-eslint/naming-convention */ + +import * as t from 'io-ts'; +import { threat_tactic } from '../threat_tactic'; +import { threat_techniques } from '../threat_technique'; + +export const threat_framework = t.string; + +export const threat = t.intersection([ + t.exact( + t.type({ + framework: threat_framework, + tactic: threat_tactic, + }) + ), + t.exact( + t.partial({ + technique: threat_techniques, + }) + ), +]); + +export type Threat = t.TypeOf; + +export const threats = t.array(threat); +export type Threats = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/threat_mapping/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/threat_mapping/index.test.ts new file mode 100644 index 0000000000000..7f754fb2d87de --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/threat_mapping/index.test.ts @@ -0,0 +1,237 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { + concurrent_searches, + items_per_search, + ThreatMapping, + threatMappingEntries, + ThreatMappingEntries, + threat_mapping, +} from '.'; +import { foldLeftRight, getPaths } from '../test_utils'; +import { exactCheck } from '../exact_check'; + +describe('threat_mapping', () => { + describe('threatMappingEntries', () => { + test('it should validate an entry', () => { + const payload: ThreatMappingEntries = [ + { + field: 'field.one', + type: 'mapping', + value: 'field.one', + }, + ]; + const decoded = threatMappingEntries.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should fail validation with an extra entry item', () => { + const payload: ThreatMappingEntries & Array<{ extra: string }> = [ + { + field: 'field.one', + type: 'mapping', + value: 'field.one', + extra: 'blah', + }, + ]; + const decoded = threatMappingEntries.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['invalid keys "extra"']); + expect(message.schema).toEqual({}); + }); + + test('it should fail validation with a non string', () => { + const payload = ([ + { + field: 5, + type: 'mapping', + value: 'field.one', + }, + ] as unknown) as ThreatMappingEntries[]; + const decoded = threatMappingEntries.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to "field"']); + expect(message.schema).toEqual({}); + }); + + test('it should fail validation with a wrong type', () => { + const payload = ([ + { + field: 'field.one', + type: 'invalid', + value: 'field.one', + }, + ] as unknown) as ThreatMappingEntries[]; + const decoded = threatMappingEntries.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "invalid" supplied to "type"', + ]); + expect(message.schema).toEqual({}); + }); + }); + + describe('threat_mapping', () => { + test('it should validate a threat mapping', () => { + const payload: ThreatMapping = [ + { + entries: [ + { + field: 'field.one', + type: 'mapping', + value: 'field.one', + }, + ], + }, + ]; + const decoded = threat_mapping.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + }); + + test('it should fail validate with an extra key', () => { + const payload: ThreatMapping & Array<{ extra: string }> = [ + { + entries: [ + { + field: 'field.one', + type: 'mapping', + value: 'field.one', + }, + ], + extra: 'invalid', + }, + ]; + + const decoded = threat_mapping.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['invalid keys "extra"']); + expect(message.schema).toEqual({}); + }); + + test('it should fail validate with an extra inner entry', () => { + const payload: ThreatMapping & Array<{ entries: Array<{ extra: string }> }> = [ + { + entries: [ + { + field: 'field.one', + type: 'mapping', + value: 'field.one', + extra: 'blah', + }, + ], + }, + ]; + + const decoded = threat_mapping.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['invalid keys "extra"']); + expect(message.schema).toEqual({}); + }); + + test('it should fail validate with an extra inner entry with the wrong data type', () => { + const payload = ([ + { + entries: [ + { + field: 5, + type: 'mapping', + value: 'field.one', + }, + ], + }, + ] as unknown) as ThreatMapping; + + const decoded = threat_mapping.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "5" supplied to "entries,field"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should fail validate with empty array', () => { + const payload: string[] = []; + + const decoded = threat_mapping.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "[]" supplied to "NonEmptyArray"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should fail validation when concurrent_searches is < 0', () => { + const payload = -1; + const decoded = concurrent_searches.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "-1" supplied to "PositiveIntegerGreaterThanZero"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should fail validation when concurrent_searches is 0', () => { + const payload = 0; + const decoded = concurrent_searches.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "0" supplied to "PositiveIntegerGreaterThanZero"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should fail validation when items_per_search is 0', () => { + const payload = 0; + const decoded = items_per_search.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "0" supplied to "PositiveIntegerGreaterThanZero"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should fail validation when items_per_search is < 0', () => { + const payload = -1; + const decoded = items_per_search.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "-1" supplied to "PositiveIntegerGreaterThanZero"', + ]); + expect(message.schema).toEqual({}); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/threat_mapping/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/threat_mapping/index.ts new file mode 100644 index 0000000000000..4fc64fe1e0982 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/threat_mapping/index.ts @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/* eslint-disable @typescript-eslint/naming-convention */ + +import * as t from 'io-ts'; +import { language } from '../language'; +import { NonEmptyArray } from '../non_empty_array'; +import { NonEmptyString } from '../non_empty_string'; +import { PositiveIntegerGreaterThanZero } from '../positive_integer_greater_than_zero'; + +export const threat_query = t.string; +export type ThreatQuery = t.TypeOf; +export const threatQueryOrUndefined = t.union([threat_query, t.undefined]); +export type ThreatQueryOrUndefined = t.TypeOf; + +export const threat_indicator_path = t.string; +export type ThreatIndicatorPath = t.TypeOf; +export const threatIndicatorPathOrUndefined = t.union([threat_indicator_path, t.undefined]); +export type ThreatIndicatorPathOrUndefined = t.TypeOf; + +export const threat_filters = t.array(t.unknown); // Filters are not easily type-able yet +export type ThreatFilters = t.TypeOf; +export const threatFiltersOrUndefined = t.union([threat_filters, t.undefined]); +export type ThreatFiltersOrUndefined = t.TypeOf; + +export const threatMapEntry = t.exact( + t.type({ + field: NonEmptyString, + type: t.keyof({ mapping: null }), + value: NonEmptyString, + }) +); + +export type ThreatMapEntry = t.TypeOf; + +export const threatMappingEntries = t.array(threatMapEntry); +export type ThreatMappingEntries = t.TypeOf; + +export const threatMap = t.exact( + t.type({ + entries: threatMappingEntries, + }) +); +export type ThreatMap = t.TypeOf; + +export const threat_mapping = NonEmptyArray(threatMap, 'NonEmptyArray'); +export type ThreatMapping = t.TypeOf; + +export const threatMappingOrUndefined = t.union([threat_mapping, t.undefined]); +export type ThreatMappingOrUndefined = t.TypeOf; + +export const threat_index = t.array(t.string); +export type ThreatIndex = t.TypeOf; +export const threatIndexOrUndefined = t.union([threat_index, t.undefined]); +export type ThreatIndexOrUndefined = t.TypeOf; + +export const threat_language = t.union([language, t.undefined]); +export type ThreatLanguage = t.TypeOf; +export const threatLanguageOrUndefined = t.union([threat_language, t.undefined]); +export type ThreatLanguageOrUndefined = t.TypeOf; + +export const concurrent_searches = PositiveIntegerGreaterThanZero; +export type ConcurrentSearches = t.TypeOf; +export const concurrentSearchesOrUndefined = t.union([concurrent_searches, t.undefined]); +export type ConcurrentSearchesOrUndefined = t.TypeOf; + +export const items_per_search = PositiveIntegerGreaterThanZero; +export type ItemsPerSearch = t.TypeOf; +export const itemsPerSearchOrUndefined = t.union([items_per_search, t.undefined]); +export type ItemsPerSearchOrUndefined = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/threat_subtechnique/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/threat_subtechnique/index.ts new file mode 100644 index 0000000000000..8d64f53cb1623 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/threat_subtechnique/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/* eslint-disable @typescript-eslint/naming-convention */ + +import * as t from 'io-ts'; + +export const threat_subtechnique_id = t.string; +export const threat_subtechnique_name = t.string; +export const threat_subtechnique_reference = t.string; + +export const threat_subtechnique = t.type({ + id: threat_subtechnique_id, + name: threat_subtechnique_name, + reference: threat_subtechnique_reference, +}); + +export const threat_subtechniques = t.array(threat_subtechnique); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/threat_tactic/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/threat_tactic/index.ts new file mode 100644 index 0000000000000..151c138c7e0da --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/threat_tactic/index.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/* eslint-disable @typescript-eslint/naming-convention */ + +import * as t from 'io-ts'; + +export const threat_tactic_id = t.string; +export const threat_tactic_name = t.string; +export const threat_tactic_reference = t.string; + +export const threat_tactic = t.type({ + id: threat_tactic_id, + name: threat_tactic_name, + reference: threat_tactic_reference, +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/threat_technique/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/threat_technique/index.ts new file mode 100644 index 0000000000000..ed2e771e1e118 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/threat_technique/index.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/* eslint-disable @typescript-eslint/naming-convention */ + +import * as t from 'io-ts'; +import { threat_subtechniques } from '../threat_subtechnique'; + +export const threat_technique_id = t.string; +export const threat_technique_name = t.string; +export const threat_technique_reference = t.string; + +export const threat_technique = t.intersection([ + t.exact( + t.type({ + id: threat_technique_id, + name: threat_technique_name, + reference: threat_technique_reference, + }) + ), + t.exact( + t.partial({ + subtechnique: threat_subtechniques, + }) + ), +]); +export const threat_techniques = t.array(threat_technique); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/throttle/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/throttle/index.ts new file mode 100644 index 0000000000000..19e75dcc3be07 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/throttle/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; + +export const throttle = t.string; +export type Throttle = t.TypeOf; + +export const throttleOrNull = t.union([throttle, t.null]); +export type ThrottleOrNull = t.TypeOf; + +export const throttleOrNullOrUndefined = t.union([throttle, t.null, t.undefined]); +export type ThrottleOrUndefinedOrNull = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/updated_at/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/updated_at/index.ts new file mode 100644 index 0000000000000..0ba0062c5dbeb --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/updated_at/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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/* eslint-disable @typescript-eslint/naming-convention */ + +import * as t from 'io-ts'; + +export const updated_at = t.string; // TODO: Make this into an ISO Date string check diff --git a/packages/kbn-securitysolution-io-ts-utils/src/updated_by/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/updated_by/index.ts new file mode 100644 index 0000000000000..2c0b2d523f1e3 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/updated_by/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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/* eslint-disable @typescript-eslint/naming-convention */ + +import * as t from 'io-ts'; + +export const updated_by = t.string; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/uuid/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/uuid/index.test.ts new file mode 100644 index 0000000000000..e8214ac60313f --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/uuid/index.test.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; +import { UUID } from '.'; +import { foldLeftRight, getPaths } from '../test_utils'; + +describe('uuid', () => { + test('it should validate a uuid', () => { + const payload = '4656dc92-5832-11ea-8e2d-0242ac130003'; + const decoded = UUID.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate a non uuid', () => { + const payload = '4656dc92-5832-11ea-8e2d'; + const decoded = UUID.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "4656dc92-5832-11ea-8e2d" supplied to "UUID"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not validate an empty string', () => { + const payload = ''; + const decoded = UUID.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "" supplied to "UUID"']); + expect(message.schema).toEqual({}); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/uuid/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/uuid/index.ts new file mode 100644 index 0000000000000..0bdfecc1c57db --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/uuid/index.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; + +const regex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + +/** + * Types the risk score as: + * - Natural Number (positive integer and not a float), + * - Between the values [0 and 100] inclusive. + */ +export const UUID = new t.Type( + 'UUID', + t.string.is, + (input, context): Either => { + return typeof input === 'string' && regex.test(input) + ? t.success(input) + : t.failure(input, context); + }, + t.identity +); + +export type UUIDC = typeof UUID; diff --git a/packages/kbn-securitysolution-io-ts-utils/src/validate/index.test.ts b/packages/kbn-securitysolution-io-ts-utils/src/validate/index.test.ts new file mode 100644 index 0000000000000..45014ecebb33a --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/validate/index.test.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { left, right } from 'fp-ts/lib/Either'; +import * as t from 'io-ts'; +import { validate, validateEither } from '.'; + +describe('validate', () => { + test('it should do a validation correctly', () => { + const schema = t.exact(t.type({ a: t.number })); + const payload = { a: 1 }; + const [validated, errors] = validate(payload, schema); + + expect(validated).toEqual(payload); + expect(errors).toEqual(null); + }); + + test('it should do an in-validation correctly', () => { + const schema = t.exact(t.type({ a: t.number })); + const payload = { a: 'some other value' }; + const [validated, errors] = validate(payload, schema); + + expect(validated).toEqual(null); + expect(errors).toEqual('Invalid value "some other value" supplied to "a"'); + }); +}); + +describe('validateEither', () => { + it('returns the ORIGINAL payload as right if valid', () => { + const schema = t.exact(t.type({ a: t.number })); + const payload = { a: 1 }; + const result = validateEither(schema, payload); + + expect(result).toEqual(right(payload)); + }); + + it('returns an error string if invalid', () => { + const schema = t.exact(t.type({ a: t.number })); + const payload = { a: 'some other value' }; + const result = validateEither(schema, payload); + + expect(result).toEqual(left(new Error('Invalid value "some other value" supplied to "a"'))); + }); +}); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/validate/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/validate/index.ts new file mode 100644 index 0000000000000..38f9ee2d0b227 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/validate/index.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { fold, Either, mapLeft } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { fromEither, TaskEither } from 'fp-ts/lib/TaskEither'; +import * as t from 'io-ts'; +import { exactCheck } from '../exact_check'; +import { formatErrors } from '../format_errors'; + +export const validate = ( + obj: object, + schema: T +): [t.TypeOf | null, string | null] => { + const decoded = schema.decode(obj); + const checked = exactCheck(obj, decoded); + const left = (errors: t.Errors): [T | null, string | null] => [ + null, + formatErrors(errors).join(','), + ]; + const right = (output: T): [T | null, string | null] => [output, null]; + return pipe(checked, fold(left, right)); +}; + +export const validateNonExact = ( + obj: unknown, + schema: T +): [t.TypeOf | null, string | null] => { + const decoded = schema.decode(obj); + const left = (errors: t.Errors): [T | null, string | null] => [ + null, + formatErrors(errors).join(','), + ]; + const right = (output: T): [T | null, string | null] => [output, null]; + return pipe(decoded, fold(left, right)); +}; + +export const validateEither = ( + schema: T, + obj: A +): Either => + pipe( + obj, + (a) => schema.validate(a, t.getDefaultContext(schema.asDecoder())), + mapLeft((errors) => new Error(formatErrors(errors).join(','))) + ); + +export const validateTaskEither = ( + schema: T, + obj: A +): TaskEither => fromEither(validateEither(schema, obj)); diff --git a/packages/kbn-securitysolution-io-ts-utils/src/version/index.ts b/packages/kbn-securitysolution-io-ts-utils/src/version/index.ts new file mode 100644 index 0000000000000..38cb47ebce53e --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/src/version/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as t from 'io-ts'; +import { PositiveIntegerGreaterThanZero } from '../positive_integer_greater_than_zero'; + +/** + * Note this is just a positive number, but we use it as a type here which is still ok. + * This type was originally from "x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts" + * but is moved here to make things more portable. No unit tests, but see PositiveIntegerGreaterThanZero integer for unit tests. + */ +export const version = PositiveIntegerGreaterThanZero; +export type Version = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-utils/tsconfig.json b/packages/kbn-securitysolution-io-ts-utils/tsconfig.json new file mode 100644 index 0000000000000..22718276926f0 --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-utils/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "incremental": true, + "outDir": "target", + "rootDir": "src", + "sourceMap": true, + "sourceRoot": "../../../../packages/kbn-securitysolution-io-ts-utils/src", + "types": [ + "jest", + "node" + ] + }, + "include": [ + "src/**/*" + ] +} diff --git a/packages/kbn-securitysolution-utils/BUILD.bazel b/packages/kbn-securitysolution-utils/BUILD.bazel new file mode 100644 index 0000000000000..33c25a3091c24 --- /dev/null +++ b/packages/kbn-securitysolution-utils/BUILD.bazel @@ -0,0 +1,86 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") + +PKG_BASE_NAME = "kbn-securitysolution-utils" + +PKG_REQUIRE_NAME = "@kbn/securitysolution-utils" + +SOURCE_FILES = glob( + [ + "src/**/*.ts", + ], + exclude = [ + "**/*.test.*", + "**/*.mock.*", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", + "README.md", +] + +SRC_DEPS = [ + "@npm//tslib", + "@npm//uuid", +] + +TYPES_DEPS = [ + "@npm//@types/jest", + "@npm//@types/node", + "@npm//@types/uuid" +] + +DEPS = SRC_DEPS + TYPES_DEPS + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + ], +) + +ts_project( + name = "tsc", + srcs = SRCS, + args = ["--pretty"], + declaration = True, + declaration_map = True, + incremental = True, + out_dir = "target", + root_dir = "src", + source_map = True, + tsconfig = ":tsconfig", + deps = DEPS, +) + +js_library( + name = PKG_BASE_NAME, + package_name = PKG_REQUIRE_NAME, + srcs = NPM_MODULE_EXTRA_FILES, + visibility = ["//visibility:public"], + deps = [":tsc"] + DEPS, +) + +pkg_npm( + name = "npm_module", + deps = [ + ":%s" % PKG_BASE_NAME, + ], +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) \ No newline at end of file diff --git a/packages/kbn-securitysolution-utils/README.md b/packages/kbn-securitysolution-utils/README.md new file mode 100644 index 0000000000000..bf0ab96e5c659 --- /dev/null +++ b/packages/kbn-securitysolution-utils/README.md @@ -0,0 +1,4 @@ +# kbn-securitysolution-utils + +This is where shared utils for security solution should go that are going to be shared among plugins. +This was originally created to remove the dependencies between security_solution and other projects such as lists. diff --git a/packages/kbn-securitysolution-utils/jest.config.js b/packages/kbn-securitysolution-utils/jest.config.js new file mode 100644 index 0000000000000..12213b6777245 --- /dev/null +++ b/packages/kbn-securitysolution-utils/jest.config.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/kbn-securitysolution-utils'], +}; diff --git a/packages/kbn-securitysolution-utils/package.json b/packages/kbn-securitysolution-utils/package.json new file mode 100644 index 0000000000000..d4b46ed07bfdd --- /dev/null +++ b/packages/kbn-securitysolution-utils/package.json @@ -0,0 +1,9 @@ +{ + "name": "@kbn/securitysolution-utils", + "version": "1.0.0", + "description": "security solution utilities to use across plugins such lists, security_solution, cases, etc...", + "license": "SSPL-1.0 OR Elastic License 2.0", + "main": "./target/index.js", + "types": "./target/index.d.ts", + "private": true +} diff --git a/packages/kbn-securitysolution-utils/src/add_remove_id_to_item/index.test.ts b/packages/kbn-securitysolution-utils/src/add_remove_id_to_item/index.test.ts new file mode 100644 index 0000000000000..30656bc75d115 --- /dev/null +++ b/packages/kbn-securitysolution-utils/src/add_remove_id_to_item/index.test.ts @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { addIdToItem, removeIdFromItem } from '.'; + +jest.mock('uuid', () => ({ + v4: jest.fn().mockReturnValue('123'), +})); + +describe('add_remove_id_to_item', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('addIdToItem', () => { + test('it adds an id to an empty item', () => { + expect(addIdToItem({})).toEqual({ id: '123' }); + }); + + test('it adds a complex object', () => { + expect( + addIdToItem({ + field: '', + type: 'mapping', + value: '', + }) + ).toEqual({ + id: '123', + field: '', + type: 'mapping', + value: '', + }); + }); + + test('it adds an id to an existing item', () => { + expect(addIdToItem({ test: '456' })).toEqual({ id: '123', test: '456' }); + }); + + test('it does not change the id if it already exists', () => { + expect(addIdToItem({ id: '456' })).toEqual({ id: '456' }); + }); + + test('it returns the same reference if it has an id already', () => { + const obj = { id: '456' }; + expect(addIdToItem(obj)).toBe(obj); + }); + + test('it returns a new reference if it adds an id to an item', () => { + const obj = { test: '456' }; + expect(addIdToItem(obj)).not.toBe(obj); + }); + }); + + describe('removeIdFromItem', () => { + test('it removes an id from an item', () => { + expect(removeIdFromItem({ id: '456' })).toEqual({}); + }); + + test('it returns a new reference if it removes an id from an item', () => { + const obj = { id: '123', test: '456' }; + expect(removeIdFromItem(obj)).not.toBe(obj); + }); + + test('it does not effect an item without an id', () => { + expect(removeIdFromItem({ test: '456' })).toEqual({ test: '456' }); + }); + + test('it returns the same reference if it does not have an id already', () => { + const obj = { test: '456' }; + expect(removeIdFromItem(obj)).toBe(obj); + }); + }); +}); diff --git a/packages/kbn-securitysolution-utils/src/add_remove_id_to_item/index.ts b/packages/kbn-securitysolution-utils/src/add_remove_id_to_item/index.ts new file mode 100644 index 0000000000000..682e38ae2b8bf --- /dev/null +++ b/packages/kbn-securitysolution-utils/src/add_remove_id_to_item/index.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import uuid from 'uuid'; + +/** + * This is useful for when you have arrays without an ID and need to add one for + * ReactJS keys. I break the types slightly by introducing an id to an arbitrary item + * but then cast it back to the regular type T. + * Usage of this could be considered tech debt as I am adding an ID when the backend + * could be doing the same thing but it depends on how you want to model your data and + * if you view modeling your data with id's to please ReactJS a good or bad thing. + * @param item The item to add an id to. + */ +type NotArray = T extends unknown[] ? never : T; +export const addIdToItem = (item: NotArray): T => { + const maybeId: typeof item & { id?: string } = item; + if (maybeId.id != null) { + return item; + } else { + return { ...item, id: uuid.v4() }; + } +}; + +/** + * This is to reverse the id you added to your arrays for ReactJS keys. + * @param item The item to remove the id from. + */ +export const removeIdFromItem = ( + item: NotArray +): + | T + | Pick< + T & { + id?: string | undefined; + }, + Exclude + > => { + const maybeId: typeof item & { id?: string } = item; + if (maybeId.id != null) { + const { id, ...noId } = maybeId; + return noId; + } else { + return item; + } +}; diff --git a/packages/kbn-securitysolution-utils/src/index.ts b/packages/kbn-securitysolution-utils/src/index.ts new file mode 100644 index 0000000000000..0bb36c590ffdf --- /dev/null +++ b/packages/kbn-securitysolution-utils/src/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './add_remove_id_to_item'; diff --git a/packages/kbn-securitysolution-utils/tsconfig.json b/packages/kbn-securitysolution-utils/tsconfig.json new file mode 100644 index 0000000000000..783e67666d8b8 --- /dev/null +++ b/packages/kbn-securitysolution-utils/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "incremental": true, + "outDir": "target", + "rootDir": "src", + "sourceMap": true, + "sourceRoot": "../../../../packages/kbn-securitysolution-utils/src", + "types": [ + "jest", + "node" + ] + }, + "include": [ + "src/**/*" + ] +} diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index e69006911e7f4..581d614c9a371 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -199,6 +199,9 @@ export class DocLinksService { percolate: `${ELASTICSEARCH_DOCS}query-dsl-percolate-query.html`, queryDsl: `${ELASTICSEARCH_DOCS}query-dsl.html`, }, + search: { + sessions: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/search-sessions.html`, + }, date: { dateMath: `${ELASTICSEARCH_DOCS}common-options.html#date-math`, dateMathIndexNames: `${ELASTICSEARCH_DOCS}date-math-index-names.html`, @@ -501,6 +504,9 @@ export interface DocLinksStart { readonly painlessWalkthrough: string; readonly luceneExpressions: string; }; + readonly search: { + readonly sessions: string; + }; readonly indexPatterns: { readonly introduction: string; readonly fieldFormattersNumber: string; diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 13660da598ea0..0523c523baf6f 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -589,6 +589,9 @@ export interface DocLinksStart { readonly painlessWalkthrough: string; readonly luceneExpressions: string; }; + readonly search: { + readonly sessions: string; + }; readonly indexPatterns: { readonly introduction: string; readonly fieldFormattersNumber: string; diff --git a/src/core/server/saved_objects/migrations/core/document_migrator.ts b/src/core/server/saved_objects/migrations/core/document_migrator.ts index 8e538f6e12384..1dd4a8fbf6388 100644 --- a/src/core/server/saved_objects/migrations/core/document_migrator.ts +++ b/src/core/server/saved_objects/migrations/core/document_migrator.ts @@ -169,7 +169,7 @@ export class DocumentMigrator implements VersionedTransformer { } /** - * Gets the latest version of each migratable property. + * Gets the latest version of each migrate-able property. * * @readonly * @type {SavedObjectsMigrationVersion} diff --git a/src/core/server/saved_objects/migrations/core/elastic_index.ts b/src/core/server/saved_objects/migrations/core/elastic_index.ts index 44dd60097f1cd..76fdd5e73d804 100644 --- a/src/core/server/saved_objects/migrations/core/elastic_index.ts +++ b/src/core/server/saved_objects/migrations/core/elastic_index.ts @@ -30,7 +30,7 @@ export interface FullIndexInfo { // When migrating from the outdated index we use a read query which excludes // saved objects which are no longer used. These saved objects will still be -// kept in the outdated index for backup purposes, but won't be availble in +// kept in the outdated index for backup purposes, but won't be available in // the upgraded index. export const excludeUnusedTypesQuery: estypes.QueryContainer = { bool: { diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts index c6dfd2c2d1809..37cea5d2de3d2 100644 --- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts @@ -344,13 +344,13 @@ const mockV2MigrationOptions = () => { options.client.openPointInTime = jest .fn() - .mockImplementationOnce(() => + .mockImplementation(() => elasticsearchClientMock.createSuccessTransportRequestPromise({ id: 'pit_id' }) ); options.client.closePointInTime = jest .fn() - .mockImplementationOnce(() => + .mockImplementation(() => elasticsearchClientMock.createSuccessTransportRequestPromise({ succeeded: true }) ); diff --git a/src/core/server/saved_objects/migrationsv2/actions/index.test.ts b/src/core/server/saved_objects/migrationsv2/actions/index.test.ts index b144905cf01ad..84862839f7ed2 100644 --- a/src/core/server/saved_objects/migrationsv2/actions/index.test.ts +++ b/src/core/server/saved_objects/migrationsv2/actions/index.test.ts @@ -18,7 +18,7 @@ describe('actions', () => { jest.clearAllMocks(); }); - // Create a mock client that rejects all methods with a 503 statuscode + // Create a mock client that rejects all methods with a 503 status code // response. const retryableError = new EsErrors.ResponseError( elasticsearchClientMock.createApiResponse({ @@ -92,7 +92,7 @@ describe('actions', () => { describe('readWithPit', () => { it('calls catchRetryableEsClientErrors when the promise rejects', async () => { - const task = Actions.readWithPit(client, 'pitId', Option.none, 10_000); + const task = Actions.readWithPit(client, 'pitId', { match_all: {} }, 10_000); try { await task(); } catch (e) { @@ -134,7 +134,7 @@ describe('actions', () => { 'my_target_index', Option.none, false, - Option.none + {} ); try { await task(); @@ -263,4 +263,17 @@ describe('actions', () => { expect(catchRetryableEsClientErrors).toHaveBeenCalledWith(retryableError); }); }); + + describe('refreshIndex', () => { + it('calls catchRetryableEsClientErrors when the promise rejects', async () => { + const task = Actions.refreshIndex(client, 'target_index'); + try { + await task(); + } catch (e) { + /** ignore */ + } + + expect(catchRetryableEsClientErrors).toHaveBeenCalledWith(retryableError); + }); + }); }); diff --git a/src/core/server/saved_objects/migrationsv2/actions/index.ts b/src/core/server/saved_objects/migrationsv2/actions/index.ts index 049cdc41b7527..43afa746de140 100644 --- a/src/core/server/saved_objects/migrationsv2/actions/index.ts +++ b/src/core/server/saved_objects/migrationsv2/actions/index.ts @@ -452,21 +452,18 @@ export interface ReadWithPit { /* * Requests documents from the index using PIT mechanism. - * Filter unusedTypesToExclude documents out to exclude them from being migrated. * */ export const readWithPit = ( client: ElasticsearchClient, pitId: string, - /* When reading we use a source query to exclude saved objects types which - * are no longer used. These saved objects will still be kept in the outdated - * index for backup purposes, but won't be available in the upgraded index. - */ - unusedTypesQuery: Option.Option, + query: estypes.QueryContainer, batchSize: number, - searchAfter?: number[] + searchAfter?: number[], + seqNoPrimaryTerm?: boolean ): TaskEither.TaskEither => () => { return client .search({ + seq_no_primary_term: seqNoPrimaryTerm, body: { // Sort fields are required to use searchAfter sort: { @@ -479,8 +476,7 @@ export const readWithPit = ( // Improve performance by not calculating the total number of hits // matching the query. track_total_hits: false, - // Exclude saved object types - query: Option.isSome(unusedTypesQuery) ? unusedTypesQuery.value : undefined, + query, }, }) .then((response) => { @@ -531,6 +527,7 @@ export const transformDocs = ( transformRawDocs: TransformRawDocs, outdatedDocuments: SavedObjectsRawDoc[], index: string, + // used for testing purposes only refresh: estypes.Refresh ): TaskEither.TaskEither< RetryableEsClientError | IndexNotFound | TargetIndexHadWriteBlock, @@ -551,6 +548,22 @@ export interface ReindexResponse { taskId: string; } +/** + * Wait for Elasticsearch to reindex all the changes. + */ +export const refreshIndex = ( + client: ElasticsearchClient, + targetIndex: string +): TaskEither.TaskEither => () => { + return client.indices + .refresh({ + index: targetIndex, + }) + .then(() => { + return Either.right({ refreshed: true }); + }) + .catch(catchRetryableEsClientErrors); +}; /** * Reindex documents from the `sourceIndex` into the `targetIndex`. Returns a * task ID which can be tracked for progress. @@ -569,7 +582,7 @@ export const reindex = ( * are no longer used. These saved objects will still be kept in the outdated * index for backup purposes, but won't be available in the upgraded index. */ - unusedTypesQuery: Option.Option + unusedTypesQuery: estypes.QueryContainer ): TaskEither.TaskEither => () => { return client .reindex({ @@ -584,10 +597,7 @@ export const reindex = ( // Set reindex batch size size: BATCH_SIZE, // Exclude saved object types - query: Option.fold( - () => undefined, - (query) => query - )(unusedTypesQuery), + query: unusedTypesQuery, }, dest: { index: targetIndex, @@ -997,6 +1007,8 @@ interface SearchForOutdatedDocumentsOptions { * Search for outdated saved object documents with the provided query. Will * return one batch of documents. Searching should be repeated until no more * outdated documents can be found. + * + * Used for testing only */ export const searchForOutdatedDocuments = ( client: ElasticsearchClient, diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts index b31f20950ae77..832d322037465 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts @@ -408,7 +408,7 @@ describe('migration actions', () => { 'reindex_target', Option.none, false, - Option.none + { match_all: {} } )()) as Either.Right; const task = waitForReindexTask(client, res.right.taskId, '10s'); await expect(task()).resolves.toMatchInlineSnapshot(` @@ -440,13 +440,13 @@ describe('migration actions', () => { 'reindex_target_excluded_docs', Option.none, false, - Option.of({ + { bool: { must_not: ['f_agent_event', 'another_unused_type'].map((type) => ({ term: { type }, })), }, - }) + } )()) as Either.Right; const task = waitForReindexTask(client, res.right.taskId, '10s'); await expect(task()).resolves.toMatchInlineSnapshot(` @@ -477,7 +477,7 @@ describe('migration actions', () => { 'reindex_target_2', Option.some(`ctx._source.title = ctx._source.title + '_updated'`), false, - Option.none + { match_all: {} } )()) as Either.Right; const task = waitForReindexTask(client, res.right.taskId, '10s'); await expect(task()).resolves.toMatchInlineSnapshot(` @@ -510,7 +510,7 @@ describe('migration actions', () => { 'reindex_target_3', Option.some(`ctx._source.title = ctx._source.title + '_updated'`), false, - Option.none + { match_all: {} } )()) as Either.Right; let task = waitForReindexTask(client, res.right.taskId, '10s'); await expect(task()).resolves.toMatchInlineSnapshot(` @@ -527,7 +527,7 @@ describe('migration actions', () => { 'reindex_target_3', Option.none, false, - Option.none + { match_all: {} } )()) as Either.Right; task = waitForReindexTask(client, res.right.taskId, '10s'); await expect(task()).resolves.toMatchInlineSnapshot(` @@ -577,7 +577,7 @@ describe('migration actions', () => { 'reindex_target_4', Option.some(`ctx._source.title = ctx._source.title + '_updated'`), false, - Option.none + { match_all: {} } )()) as Either.Right; const task = waitForReindexTask(client, res.right.taskId, '10s'); await expect(task()).resolves.toMatchInlineSnapshot(` @@ -627,7 +627,7 @@ describe('migration actions', () => { 'reindex_target_5', Option.none, false, - Option.none + { match_all: {} } )()) as Either.Right; const task = waitForReindexTask(client, reindexTaskId, '10s'); @@ -662,7 +662,7 @@ describe('migration actions', () => { 'reindex_target_6', Option.none, false, - Option.none + { match_all: {} } )()) as Either.Right; const task = waitForReindexTask(client, reindexTaskId, '10s'); @@ -677,14 +677,9 @@ describe('migration actions', () => { }); it('resolves left index_not_found_exception if source index does not exist', async () => { expect.assertions(1); - const res = (await reindex( - client, - 'no_such_index', - 'reindex_target', - Option.none, - false, - Option.none - )()) as Either.Right; + const res = (await reindex(client, 'no_such_index', 'reindex_target', Option.none, false, { + match_all: {}, + })()) as Either.Right; const task = waitForReindexTask(client, res.right.taskId, '10s'); await expect(task()).resolves.toMatchInlineSnapshot(` Object { @@ -704,7 +699,7 @@ describe('migration actions', () => { 'existing_index_with_write_block', Option.none, false, - Option.none + { match_all: {} } )()) as Either.Right; const task = waitForReindexTask(client, res.right.taskId, '10s'); @@ -726,7 +721,7 @@ describe('migration actions', () => { 'existing_index_with_write_block', Option.none, true, - Option.none + { match_all: {} } )()) as Either.Right; const task = waitForReindexTask(client, res.right.taskId, '10s'); @@ -748,7 +743,7 @@ describe('migration actions', () => { 'reindex_target', Option.none, false, - Option.none + { match_all: {} } )()) as Either.Right; const task = waitForReindexTask(client, res.right.taskId, '0s'); @@ -775,7 +770,7 @@ describe('migration actions', () => { 'reindex_target_7', Option.none, false, - Option.none + { match_all: {} } )()) as Either.Right; await waitForReindexTask(client, res.right.taskId, '10s')(); @@ -840,7 +835,7 @@ describe('migration actions', () => { const readWithPitTask = readWithPit( client, pitResponse.right.pitId, - Option.none, + { match_all: {} }, 1000, undefined ); @@ -856,7 +851,7 @@ describe('migration actions', () => { const readWithPitTask = readWithPit( client, pitResponse.right.pitId, - Option.none, + { match_all: {} }, 3, undefined ); @@ -865,14 +860,14 @@ describe('migration actions', () => { await expect(docsResponse.right.outdatedDocuments.length).toBe(3); }); - it('it excludes documents not matching the provided "unusedTypesQuery"', async () => { + it('it excludes documents not matching the provided "query"', async () => { const openPitTask = openPit(client, 'existing_index_with_docs'); const pitResponse = (await openPitTask()) as Either.Right; const readWithPitTask = readWithPit( client, pitResponse.right.pitId, - Option.some({ + { bool: { must_not: [ { @@ -887,7 +882,7 @@ describe('migration actions', () => { }, ], }, - }), + }, 1000, undefined ); @@ -904,8 +899,93 @@ describe('migration actions', () => { `); }); + it('only returns documents that match the provided "query"', async () => { + const openPitTask = openPit(client, 'existing_index_with_docs'); + const pitResponse = (await openPitTask()) as Either.Right; + + const readWithPitTask = readWithPit( + client, + pitResponse.right.pitId, + { + match: { title: { query: 'doc' } }, + }, + 1000, + undefined + ); + + const docsResponse = (await readWithPitTask()) as Either.Right; + + expect(docsResponse.right.outdatedDocuments.map((doc) => doc._source.title).sort()) + .toMatchInlineSnapshot(` + Array [ + "doc 1", + "doc 2", + "doc 3", + ] + `); + }); + + it('returns docs with _seq_no and _primary_term when specified', async () => { + const openPitTask = openPit(client, 'existing_index_with_docs'); + const pitResponse = (await openPitTask()) as Either.Right; + + const readWithPitTask = readWithPit( + client, + pitResponse.right.pitId, + { + match: { title: { query: 'doc' } }, + }, + 1000, + undefined, + true + ); + + const docsResponse = (await readWithPitTask()) as Either.Right; + + expect(docsResponse.right.outdatedDocuments).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + _seq_no: expect.any(Number), + _primary_term: expect.any(Number), + }), + ]) + ); + }); + + it('does not return docs with _seq_no and _primary_term if not specified', async () => { + const openPitTask = openPit(client, 'existing_index_with_docs'); + const pitResponse = (await openPitTask()) as Either.Right; + + const readWithPitTask = readWithPit( + client, + pitResponse.right.pitId, + { + match: { title: { query: 'doc' } }, + }, + 1000, + undefined + ); + + const docsResponse = (await readWithPitTask()) as Either.Right; + + expect(docsResponse.right.outdatedDocuments).toEqual( + expect.arrayContaining([ + expect.not.objectContaining({ + _seq_no: expect.any(Number), + _primary_term: expect.any(Number), + }), + ]) + ); + }); + it('rejects if PIT does not exist', async () => { - const readWithPitTask = readWithPit(client, 'no_such_pit', Option.none, 1000, undefined); + const readWithPitTask = readWithPit( + client, + 'no_such_pit', + { match_all: {} }, + 1000, + undefined + ); await expect(readWithPitTask()).rejects.toThrow('illegal_argument_exception'); }); }); @@ -973,49 +1053,6 @@ describe('migration actions', () => { }); }); - describe('searchForOutdatedDocuments', () => { - it('only returns documents that match the outdatedDocumentsQuery', async () => { - expect.assertions(2); - const resultsWithQuery = ((await searchForOutdatedDocuments(client, { - batchSize: 1000, - targetIndex: 'existing_index_with_docs', - outdatedDocumentsQuery: { - match: { title: { query: 'doc' } }, - }, - })()) as Either.Right).right.outdatedDocuments; - expect(resultsWithQuery.length).toBe(3); - - const resultsWithoutQuery = ((await searchForOutdatedDocuments(client, { - batchSize: 1000, - targetIndex: 'existing_index_with_docs', - outdatedDocumentsQuery: undefined, - })()) as Either.Right).right.outdatedDocuments; - expect(resultsWithoutQuery.length).toBe(5); - }); - it('resolves with _id, _source, _seq_no and _primary_term', async () => { - expect.assertions(1); - const results = ((await searchForOutdatedDocuments(client, { - batchSize: 1000, - targetIndex: 'existing_index_with_docs', - outdatedDocumentsQuery: { - match: { title: { query: 'doc' } }, - }, - })()) as Either.Right).right.outdatedDocuments; - expect(results).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - _id: expect.any(String), - _seq_no: expect.any(Number), - _primary_term: expect.any(Number), - _source: expect.any(Object), - }), - ]) - ); - }); - // I haven't been able to find a way to reproduce a partial search result - // it.todo('rejects if only partial search results can be obtained'); - }); - describe('waitForPickupUpdatedMappingsTask', () => { it('rejects if there are failures', async () => { const res = (await pickupUpdatedMappings( diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/archives/8.0.0_migrated_with_outdated_docs.zip b/src/core/server/saved_objects/migrationsv2/integration_tests/archives/8.0.0_migrated_with_outdated_docs.zip new file mode 100644 index 0000000000000..ca936c6448b43 Binary files /dev/null and b/src/core/server/saved_objects/migrationsv2/integration_tests/archives/8.0.0_migrated_with_outdated_docs.zip differ diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/outdated_docs.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/outdated_docs.test.ts new file mode 100644 index 0000000000000..69a1da5c62664 --- /dev/null +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/outdated_docs.test.ts @@ -0,0 +1,140 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import Path from 'path'; +import Fs from 'fs'; +import Util from 'util'; +import { kibanaPackageJson as pkg } from '@kbn/utils'; +import * as kbnTestServer from '../../../../test_helpers/kbn_server'; +import type { ElasticsearchClient } from '../../../elasticsearch'; +import { Root } from '../../../root'; + +const logFilePath = Path.join(__dirname, 'migration_test_kibana.log'); + +const asyncUnlink = Util.promisify(Fs.unlink); +async function removeLogFile() { + // ignore errors if it doesn't exist + await asyncUnlink(logFilePath).catch(() => void 0); +} + +describe('migration v2', () => { + let esServer: kbnTestServer.TestElasticsearchUtils; + let root: Root; + + beforeAll(async () => { + await removeLogFile(); + }); + + afterAll(async () => { + if (root) { + await root.shutdown(); + } + if (esServer) { + await esServer.stop(); + } + + await new Promise((resolve) => setTimeout(resolve, 10000)); + }); + + it('migrates the documents to the highest version', async () => { + const migratedIndex = `.kibana_${pkg.version}_001`; + const { startES } = kbnTestServer.createTestServers({ + adjustTimeout: (t: number) => jest.setTimeout(t), + settings: { + es: { + license: 'basic', + // original SO: + // { + // type: 'foo', + // foo: {}, + // migrationVersion: { + // foo: '7.13.0', + // }, + // }, + // contains migrated index with 8.0 aliases to skip migration, but run outdated doc search + dataArchive: Path.join(__dirname, 'archives', '8.0.0_migrated_with_outdated_docs.zip'), + }, + }, + }); + + root = createRoot(); + + esServer = await startES(); + const coreSetup = await root.setup(); + + coreSetup.savedObjects.registerType({ + name: 'foo', + hidden: false, + mappings: { properties: {} }, + namespaceType: 'agnostic', + migrations: { + '7.14.0': (doc) => doc, + }, + }); + + const coreStart = await root.start(); + const esClient = coreStart.elasticsearch.client.asInternalUser; + + const migratedDocs = await fetchDocs(esClient, migratedIndex); + + expect(migratedDocs.length).toBe(1); + const [doc] = migratedDocs; + expect(doc._source.migrationVersion.foo).toBe('7.14.0'); + expect(doc._source.coreMigrationVersion).toBe('8.0.0'); + }); +}); + +function createRoot() { + return kbnTestServer.createRootWithCorePlugins( + { + migrations: { + skip: false, + enableV2: true, + }, + logging: { + appenders: { + file: { + type: 'file', + fileName: logFilePath, + layout: { + type: 'json', + }, + }, + }, + loggers: [ + { + name: 'root', + appenders: ['file'], + }, + ], + }, + }, + { + oss: true, + } + ); +} + +async function fetchDocs(esClient: ElasticsearchClient, index: string) { + const { body } = await esClient.search({ + index, + body: { + query: { + bool: { + should: [ + { + term: { type: 'foo' }, + }, + ], + }, + }, + }, + }); + + return body.hits.hits; +} diff --git a/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.test.ts b/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.test.ts index bffe590a39432..7a87a645a249c 100644 --- a/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.test.ts +++ b/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.test.ts @@ -296,38 +296,35 @@ describe('migrationsStateActionMachine', () => { }, }, "unusedTypesQuery": Object { - "_tag": "Some", - "value": Object { - "bool": Object { - "must_not": Array [ - Object { - "term": Object { - "type": "fleet-agent-events", - }, + "bool": Object { + "must_not": Array [ + Object { + "term": Object { + "type": "fleet-agent-events", }, - Object { - "term": Object { - "type": "tsvb-validation-telemetry", - }, + }, + Object { + "term": Object { + "type": "tsvb-validation-telemetry", }, - Object { - "bool": Object { - "must": Array [ - Object { - "match": Object { - "type": "search-session", - }, + }, + Object { + "bool": Object { + "must": Array [ + Object { + "match": Object { + "type": "search-session", }, - Object { - "match": Object { - "search-session.persisted": false, - }, + }, + Object { + "match": Object { + "search-session.persisted": false, }, - ], - }, + }, + ], }, - ], - }, + }, + ], }, }, "versionAlias": ".my-so-index_7.11.0", @@ -396,38 +393,35 @@ describe('migrationsStateActionMachine', () => { }, }, "unusedTypesQuery": Object { - "_tag": "Some", - "value": Object { - "bool": Object { - "must_not": Array [ - Object { - "term": Object { - "type": "fleet-agent-events", - }, + "bool": Object { + "must_not": Array [ + Object { + "term": Object { + "type": "fleet-agent-events", }, - Object { - "term": Object { - "type": "tsvb-validation-telemetry", - }, + }, + Object { + "term": Object { + "type": "tsvb-validation-telemetry", }, - Object { - "bool": Object { - "must": Array [ - Object { - "match": Object { - "type": "search-session", - }, + }, + Object { + "bool": Object { + "must": Array [ + Object { + "match": Object { + "type": "search-session", }, - Object { - "match": Object { - "search-session.persisted": false, - }, + }, + Object { + "match": Object { + "search-session.persisted": false, }, - ], - }, + }, + ], }, - ], - }, + }, + ], }, }, "versionAlias": ".my-so-index_7.11.0", @@ -584,38 +578,35 @@ describe('migrationsStateActionMachine', () => { }, }, "unusedTypesQuery": Object { - "_tag": "Some", - "value": Object { - "bool": Object { - "must_not": Array [ - Object { - "term": Object { - "type": "fleet-agent-events", - }, + "bool": Object { + "must_not": Array [ + Object { + "term": Object { + "type": "fleet-agent-events", }, - Object { - "term": Object { - "type": "tsvb-validation-telemetry", - }, + }, + Object { + "term": Object { + "type": "tsvb-validation-telemetry", }, - Object { - "bool": Object { - "must": Array [ - Object { - "match": Object { - "type": "search-session", - }, + }, + Object { + "bool": Object { + "must": Array [ + Object { + "match": Object { + "type": "search-session", }, - Object { - "match": Object { - "search-session.persisted": false, - }, + }, + Object { + "match": Object { + "search-session.persisted": false, }, - ], - }, + }, + ], }, - ], - }, + }, + ], }, }, "versionAlias": ".my-so-index_7.11.0", @@ -679,38 +670,35 @@ describe('migrationsStateActionMachine', () => { }, }, "unusedTypesQuery": Object { - "_tag": "Some", - "value": Object { - "bool": Object { - "must_not": Array [ - Object { - "term": Object { - "type": "fleet-agent-events", - }, + "bool": Object { + "must_not": Array [ + Object { + "term": Object { + "type": "fleet-agent-events", }, - Object { - "term": Object { - "type": "tsvb-validation-telemetry", - }, + }, + Object { + "term": Object { + "type": "tsvb-validation-telemetry", }, - Object { - "bool": Object { - "must": Array [ - Object { - "match": Object { - "type": "search-session", - }, + }, + Object { + "bool": Object { + "must": Array [ + Object { + "match": Object { + "type": "search-session", }, - Object { - "match": Object { - "search-session.persisted": false, - }, + }, + Object { + "match": Object { + "search-session.persisted": false, }, - ], - }, + }, + ], }, - ], - }, + }, + ], }, }, "versionAlias": ".my-so-index_7.11.0", diff --git a/src/core/server/saved_objects/migrationsv2/model.test.ts b/src/core/server/saved_objects/migrationsv2/model.test.ts index 57a7a7f2ea24a..213e8b43c0ea0 100644 --- a/src/core/server/saved_objects/migrationsv2/model.test.ts +++ b/src/core/server/saved_objects/migrationsv2/model.test.ts @@ -21,9 +21,12 @@ import type { ReindexSourceToTempRead, ReindexSourceToTempClosePit, ReindexSourceToTempIndex, + RefreshTarget, UpdateTargetMappingsState, UpdateTargetMappingsWaitForTaskState, - OutdatedDocumentsSearch, + OutdatedDocumentsSearchOpenPit, + OutdatedDocumentsSearchRead, + OutdatedDocumentsSearchClosePit, OutdatedDocumentsTransform, MarkVersionIndexReady, BaseState, @@ -72,7 +75,7 @@ describe('migrations v2 model', () => { versionAlias: '.kibana_7.11.0', versionIndex: '.kibana_7.11.0_001', tempIndex: '.kibana_7.11.0_reindex_temp', - unusedTypesQuery: Option.of({ + unusedTypesQuery: { bool: { must_not: [ { @@ -82,7 +85,7 @@ describe('migrations v2 model', () => { }, ], }, - }), + }, }; describe('exponential retry delays for retryable_es_client_error', () => { @@ -214,7 +217,7 @@ describe('migrations v2 model', () => { }, }; - test('INIT -> OUTDATED_DOCUMENTS_SEARCH if .kibana is already pointing to the target index', () => { + test('INIT -> OUTDATED_DOCUMENTS_SEARCH_OPEN_PIT if .kibana is already pointing to the target index', () => { const res: ResponseType<'INIT'> = Either.right({ '.kibana_7.11.0_001': { aliases: { @@ -227,7 +230,7 @@ describe('migrations v2 model', () => { }); const newState = model(initState, res); - expect(newState.controlState).toEqual('OUTDATED_DOCUMENTS_SEARCH'); + expect(newState.controlState).toEqual('OUTDATED_DOCUMENTS_SEARCH_OPEN_PIT'); // This snapshot asserts that we merge the // migrationMappingPropertyHashes of the existing index, but we leave // the mappings for the disabled_saved_object_type untouched. There @@ -888,78 +891,120 @@ describe('migrations v2 model', () => { sourceIndex: Option.some('.kibana') as Option.Some, targetIndex: '.kibana_7.11.0_001', }; - it('CLONE_TEMP_TO_TARGET -> OUTDATED_DOCUMENTS_SEARCH if response is right', () => { + it('CLONE_TEMP_TO_TARGET -> REFRESH_TARGET if response is right', () => { const res: ResponseType<'CLONE_TEMP_TO_TARGET'> = Either.right({ acknowledged: true, shardsAcknowledged: true, }); const newState = model(state, res); - expect(newState.controlState).toEqual('OUTDATED_DOCUMENTS_SEARCH'); - expect(newState.retryCount).toEqual(0); - expect(newState.retryDelay).toEqual(0); + expect(newState.controlState).toBe('REFRESH_TARGET'); + expect(newState.retryCount).toBe(0); + expect(newState.retryDelay).toBe(0); }); - it('CLONE_TEMP_TO_TARGET -> OUTDATED_DOCUMENTS_SEARCH if response is left index_not_fonud_exception', () => { + it('CLONE_TEMP_TO_TARGET -> REFRESH_TARGET if response is left index_not_fonud_exception', () => { const res: ResponseType<'CLONE_TEMP_TO_TARGET'> = Either.left({ type: 'index_not_found_exception', index: 'temp_index', }); const newState = model(state, res); - expect(newState.controlState).toEqual('OUTDATED_DOCUMENTS_SEARCH'); - expect(newState.retryCount).toEqual(0); - expect(newState.retryDelay).toEqual(0); + expect(newState.controlState).toBe('REFRESH_TARGET'); + expect(newState.retryCount).toBe(0); + expect(newState.retryDelay).toBe(0); }); }); - describe('OUTDATED_DOCUMENTS_SEARCH', () => { - const outdatedDocumentsSourchState: OutdatedDocumentsSearch = { + describe('OUTDATED_DOCUMENTS_SEARCH_OPEN_PIT', () => { + const state: OutdatedDocumentsSearchOpenPit = { ...baseState, - controlState: 'OUTDATED_DOCUMENTS_SEARCH', + controlState: 'OUTDATED_DOCUMENTS_SEARCH_OPEN_PIT', versionIndexReadyActions: Option.none, sourceIndex: Option.some('.kibana') as Option.Some, targetIndex: '.kibana_7.11.0_001', }; - test('OUTDATED_DOCUMENTS_SEARCH -> OUTDATED_DOCUMENTS_TRANSFORM if some outdated documents were found', () => { - const outdatedDocuments = ([ - Symbol('raw saved object doc'), - ] as unknown) as SavedObjectsRawDoc[]; - const res: ResponseType<'OUTDATED_DOCUMENTS_SEARCH'> = Either.right({ - outdatedDocuments, + it('OUTDATED_DOCUMENTS_SEARCH_OPEN_PIT -> OUTDATED_DOCUMENTS_SEARCH_READ if action succeeds', () => { + const res: ResponseType<'OUTDATED_DOCUMENTS_SEARCH_OPEN_PIT'> = Either.right({ + pitId: 'pit_id', }); - const newState = model(outdatedDocumentsSourchState, res) as OutdatedDocumentsTransform; - expect(newState.controlState).toEqual('OUTDATED_DOCUMENTS_TRANSFORM'); - expect(newState.outdatedDocuments).toEqual(outdatedDocuments); - expect(newState.retryCount).toEqual(0); - expect(newState.retryDelay).toEqual(0); + const newState = model(state, res) as OutdatedDocumentsSearchRead; + expect(newState.controlState).toBe('OUTDATED_DOCUMENTS_SEARCH_READ'); + expect(newState.pitId).toBe('pit_id'); + expect(newState.lastHitSortValue).toBe(undefined); + expect(newState.retryCount).toBe(0); + expect(newState.retryDelay).toBe(0); }); - test('OUTDATED_DOCUMENTS_SEARCH -> UPDATE_TARGET_MAPPINGS if none outdated documents were found and some versionIndexReadyActions', () => { - const aliasActions = ([Symbol('alias action')] as unknown) as AliasAction[]; - const outdatedDocumentsSourchStateWithSomeVersionIndexReadyActions = { - ...outdatedDocumentsSourchState, - ...{ - versionIndexReadyActions: Option.some(aliasActions), - }, - }; - const res: ResponseType<'OUTDATED_DOCUMENTS_SEARCH'> = Either.right({ - outdatedDocuments: [], + }); + + describe('OUTDATED_DOCUMENTS_SEARCH_READ', () => { + const state: OutdatedDocumentsSearchRead = { + ...baseState, + controlState: 'OUTDATED_DOCUMENTS_SEARCH_READ', + versionIndexReadyActions: Option.none, + sourceIndex: Option.some('.kibana') as Option.Some, + pitId: 'pit_id', + targetIndex: '.kibana_7.11.0_001', + lastHitSortValue: undefined, + hasTransformedDocs: false, + }; + + it('OUTDATED_DOCUMENTS_SEARCH_READ -> OUTDATED_DOCUMENTS_TRANSFORM if found documents to transform', () => { + const outdatedDocuments = [{ _id: '1', _source: { type: 'vis' } }]; + const lastHitSortValue = [123456]; + const res: ResponseType<'OUTDATED_DOCUMENTS_SEARCH_READ'> = Either.right({ + outdatedDocuments, + lastHitSortValue, }); - const newState = model( - outdatedDocumentsSourchStateWithSomeVersionIndexReadyActions, - res - ) as MarkVersionIndexReady; - expect(newState.controlState).toEqual('UPDATE_TARGET_MAPPINGS'); - expect(newState.versionIndexReadyActions.value).toEqual(aliasActions); - expect(newState.retryCount).toEqual(0); - expect(newState.retryDelay).toEqual(0); + const newState = model(state, res) as OutdatedDocumentsTransform; + expect(newState.controlState).toBe('OUTDATED_DOCUMENTS_TRANSFORM'); + expect(newState.outdatedDocuments).toBe(outdatedDocuments); + expect(newState.lastHitSortValue).toBe(lastHitSortValue); }); - test('OUTDATED_DOCUMENTS_SEARCH -> UPDATE_TARGET_MAPPINGS if none outdated documents were found and none versionIndexReadyActions', () => { - const res: ResponseType<'OUTDATED_DOCUMENTS_SEARCH'> = Either.right({ + + it('OUTDATED_DOCUMENTS_SEARCH_READ -> OUTDATED_DOCUMENTS_SEARCH_CLOSE_PIT if no outdated documents to transform', () => { + const res: ResponseType<'OUTDATED_DOCUMENTS_SEARCH_READ'> = Either.right({ outdatedDocuments: [], + lastHitSortValue: undefined, }); - const newState = model(outdatedDocumentsSourchState, res); - expect(newState.controlState).toEqual('UPDATE_TARGET_MAPPINGS'); - expect(newState.retryCount).toEqual(0); - expect(newState.retryDelay).toEqual(0); + const newState = model(state, res) as OutdatedDocumentsSearchClosePit; + expect(newState.controlState).toBe('OUTDATED_DOCUMENTS_SEARCH_CLOSE_PIT'); + expect(newState.pitId).toBe('pit_id'); + }); + }); + + describe('OUTDATED_DOCUMENTS_SEARCH_CLOSE_PIT', () => { + const state: OutdatedDocumentsSearchClosePit = { + ...baseState, + controlState: 'OUTDATED_DOCUMENTS_SEARCH_CLOSE_PIT', + versionIndexReadyActions: Option.none, + sourceIndex: Option.some('.kibana') as Option.Some, + pitId: 'pit_id', + targetIndex: '.kibana_7.11.0_001', + hasTransformedDocs: false, + }; + + it('OUTDATED_DOCUMENTS_SEARCH_CLOSE_PIT -> UPDATE_TARGET_MAPPINGS if action succeeded', () => { + const res: ResponseType<'OUTDATED_DOCUMENTS_SEARCH_CLOSE_PIT'> = Either.right({}); + const newState = model(state, res) as UpdateTargetMappingsState; + expect(newState.controlState).toBe('UPDATE_TARGET_MAPPINGS'); + // @ts-expect-error pitId shouldn't leak outside + expect(newState.pitId).toBe(undefined); + }); + }); + + describe('REFRESH_TARGET', () => { + const state: RefreshTarget = { + ...baseState, + controlState: 'REFRESH_TARGET', + versionIndexReadyActions: Option.none, + sourceIndex: Option.some('.kibana') as Option.Some, + targetIndex: '.kibana_7.11.0_001', + }; + + it('REFRESH_TARGET -> OUTDATED_DOCUMENTS_SEARCH_OPEN_PIT if action succeeded', () => { + const res: ResponseType<'REFRESH_TARGET'> = Either.right({ refreshed: true }); + const newState = model(state, res) as UpdateTargetMappingsState; + expect(newState.controlState).toBe('OUTDATED_DOCUMENTS_SEARCH_OPEN_PIT'); }); }); + describe('OUTDATED_DOCUMENTS_TRANSFORM', () => { const outdatedDocuments = ([ Symbol('raw saved object doc'), @@ -971,17 +1016,21 @@ describe('migrations v2 model', () => { sourceIndex: Option.some('.kibana') as Option.Some, targetIndex: '.kibana_7.11.0_001', outdatedDocuments, + pitId: 'pit_id', + lastHitSortValue: [3, 4], + hasTransformedDocs: false, }; - test('OUTDATED_DOCUMENTS_TRANSFORM -> OUTDATED_DOCUMENTS_SEARCH if action succeeds', () => { + test('OUTDATED_DOCUMENTS_TRANSFORM -> OUTDATED_DOCUMENTS_SEARCH_READ if action succeeds', () => { const res: ResponseType<'OUTDATED_DOCUMENTS_TRANSFORM'> = Either.right( 'bulk_index_succeeded' ); const newState = model(outdatedDocumentsTransformState, res); - expect(newState.controlState).toEqual('OUTDATED_DOCUMENTS_SEARCH'); + expect(newState.controlState).toEqual('OUTDATED_DOCUMENTS_SEARCH_READ'); expect(newState.retryCount).toEqual(0); expect(newState.retryDelay).toEqual(0); }); }); + describe('UPDATE_TARGET_MAPPINGS', () => { const updateTargetMappingsState: UpdateTargetMappingsState = { ...baseState, @@ -1236,38 +1285,35 @@ describe('migrations v2 model', () => { }, }, "unusedTypesQuery": Object { - "_tag": "Some", - "value": Object { - "bool": Object { - "must_not": Array [ - Object { - "term": Object { - "type": "fleet-agent-events", - }, + "bool": Object { + "must_not": Array [ + Object { + "term": Object { + "type": "fleet-agent-events", }, - Object { - "term": Object { - "type": "tsvb-validation-telemetry", - }, + }, + Object { + "term": Object { + "type": "tsvb-validation-telemetry", }, - Object { - "bool": Object { - "must": Array [ - Object { - "match": Object { - "type": "search-session", - }, + }, + Object { + "bool": Object { + "must": Array [ + Object { + "match": Object { + "type": "search-session", }, - Object { - "match": Object { - "search-session.persisted": false, - }, + }, + Object { + "match": Object { + "search-session.persisted": false, }, - ], - }, + }, + ], }, - ], - }, + }, + ], }, }, "versionAlias": ".kibana_task_manager_8.1.0", diff --git a/src/core/server/saved_objects/migrationsv2/model.ts b/src/core/server/saved_objects/migrationsv2/model.ts index 2097b1de88aab..318eff19d5e24 100644 --- a/src/core/server/saved_objects/migrationsv2/model.ts +++ b/src/core/server/saved_objects/migrationsv2/model.ts @@ -189,10 +189,10 @@ export const model = (currentState: State, resW: ResponseType): ) { return { ...stateP, - // Skip to 'OUTDATED_DOCUMENTS_SEARCH' so that if a new plugin was + // Skip to 'OUTDATED_DOCUMENTS_SEARCH_OPEN_PIT' so that if a new plugin was // installed / enabled we can transform any old documents and update // the mappings for this plugin's types. - controlState: 'OUTDATED_DOCUMENTS_SEARCH', + controlState: 'OUTDATED_DOCUMENTS_SEARCH_OPEN_PIT', // Source is a none because we didn't do any migration from a source // index sourceIndex: Option.none, @@ -574,51 +574,100 @@ export const model = (currentState: State, resW: ResponseType): if (Either.isRight(res)) { return { ...stateP, - controlState: 'OUTDATED_DOCUMENTS_SEARCH', + controlState: 'REFRESH_TARGET', }; } else { const left = res.left; if (isLeftTypeof(left, 'index_not_found_exception')) { - // index_not_found_exception means another instance alread completed + // index_not_found_exception means another instance already completed // the MARK_VERSION_INDEX_READY step and removed the temp index - // We still perform the OUTDATED_DOCUMENTS_* and + // We still perform the REFRESH_TARGET, OUTDATED_DOCUMENTS_* and // UPDATE_TARGET_MAPPINGS steps since we might have plugins enabled // which the other instances don't. return { ...stateP, - controlState: 'OUTDATED_DOCUMENTS_SEARCH', + controlState: 'REFRESH_TARGET', }; } else { throwBadResponse(stateP, left); } } - } else if (stateP.controlState === 'OUTDATED_DOCUMENTS_SEARCH') { + } else if (stateP.controlState === 'REFRESH_TARGET') { + const res = resW as ExcludeRetryableEsError>; + if (Either.isRight(res)) { + return { + ...stateP, + controlState: 'OUTDATED_DOCUMENTS_SEARCH_OPEN_PIT', + }; + } else { + throwBadResponse(stateP, res); + } + } else if (stateP.controlState === 'OUTDATED_DOCUMENTS_SEARCH_OPEN_PIT') { + const res = resW as ExcludeRetryableEsError>; + if (Either.isRight(res)) { + return { + ...stateP, + controlState: 'OUTDATED_DOCUMENTS_SEARCH_READ', + pitId: res.right.pitId, + lastHitSortValue: undefined, + hasTransformedDocs: false, + }; + } else { + throwBadResponse(stateP, res); + } + } else if (stateP.controlState === 'OUTDATED_DOCUMENTS_SEARCH_READ') { const res = resW as ExcludeRetryableEsError>; if (Either.isRight(res)) { - // If outdated documents were found, transform them if (res.right.outdatedDocuments.length > 0) { return { ...stateP, controlState: 'OUTDATED_DOCUMENTS_TRANSFORM', outdatedDocuments: res.right.outdatedDocuments, + lastHitSortValue: res.right.lastHitSortValue, }; } else { - // If there are no more results we have transformed all outdated - // documents and can proceed to the next step return { ...stateP, - controlState: 'UPDATE_TARGET_MAPPINGS', + controlState: 'OUTDATED_DOCUMENTS_SEARCH_CLOSE_PIT', }; } } else { throwBadResponse(stateP, res); } + } else if (stateP.controlState === 'OUTDATED_DOCUMENTS_REFRESH') { + const res = resW as ExcludeRetryableEsError>; + if (Either.isRight(res)) { + return { + ...stateP, + controlState: 'UPDATE_TARGET_MAPPINGS', + }; + } else { + throwBadResponse(stateP, res); + } + } else if (stateP.controlState === 'OUTDATED_DOCUMENTS_SEARCH_CLOSE_PIT') { + const res = resW as ExcludeRetryableEsError>; + if (Either.isRight(res)) { + const { pitId, hasTransformedDocs, ...state } = stateP; + if (hasTransformedDocs) { + return { + ...state, + controlState: 'OUTDATED_DOCUMENTS_REFRESH', + }; + } + return { + ...state, + controlState: 'UPDATE_TARGET_MAPPINGS', + }; + } else { + throwBadResponse(stateP, res); + } } else if (stateP.controlState === 'OUTDATED_DOCUMENTS_TRANSFORM') { const res = resW as ExcludeRetryableEsError>; if (Either.isRight(res)) { return { ...stateP, - controlState: 'OUTDATED_DOCUMENTS_SEARCH', + controlState: 'OUTDATED_DOCUMENTS_SEARCH_READ', + hasTransformedDocs: true, }; } else { throwBadResponse(stateP, res as never); @@ -813,7 +862,7 @@ export const createInitialState = ({ retryAttempts: migrationsConfig.retryAttempts, batchSize: migrationsConfig.batchSize, logs: [], - unusedTypesQuery: Option.of(excludeUnusedTypesQuery), + unusedTypesQuery: excludeUnusedTypesQuery, }; return initialState; }; diff --git a/src/core/server/saved_objects/migrationsv2/next.ts b/src/core/server/saved_objects/migrationsv2/next.ts index 6d61634a6948e..536c07d6a071d 100644 --- a/src/core/server/saved_objects/migrationsv2/next.ts +++ b/src/core/server/saved_objects/migrationsv2/next.ts @@ -20,7 +20,6 @@ import type { LegacyReindexState, LegacyReindexWaitForTaskState, LegacySetWriteBlockState, - OutdatedDocumentsSearch, OutdatedDocumentsTransform, SetSourceWriteBlockState, State, @@ -33,6 +32,11 @@ import type { SetTempWriteBlock, WaitForYellowSourceState, TransformRawDocs, + OutdatedDocumentsSearchOpenPit, + OutdatedDocumentsSearchRead, + OutdatedDocumentsSearchClosePit, + RefreshTarget, + OutdatedDocumentsRefresh, } from './types'; import * as Actions from './actions'; import { ElasticsearchClient } from '../../elasticsearch'; @@ -67,6 +71,10 @@ export const nextActionMap = (client: ElasticsearchClient, transformRawDocs: Tra Actions.readWithPit( client, state.sourceIndexPitId, + /* When reading we use a source query to exclude saved objects types which + * are no longer used. These saved objects will still be kept in the outdated + * index for backup purposes, but won't be available in the upgraded index. + */ state.unusedTypesQuery, state.batchSize, state.lastHitSortValue @@ -83,9 +91,8 @@ export const nextActionMap = (client: ElasticsearchClient, transformRawDocs: Tra * Since we don't run a search against the target index, we disable "refresh" to speed up * the migration process. * Although any further step must run "refresh" for the target index - * before we reach out to the OUTDATED_DOCUMENTS_SEARCH step. - * Right now, we rely on UPDATE_TARGET_MAPPINGS + UPDATE_TARGET_MAPPINGS_WAIT_FOR_TASK - * to perform refresh. + * before we reach out to the OUTDATED_DOCUMENTS_SEARCH_OPEN_PIT step. + * Right now, it's performed during REFRESH_TARGET step. */ false ), @@ -93,31 +100,40 @@ export const nextActionMap = (client: ElasticsearchClient, transformRawDocs: Tra Actions.setWriteBlock(client, state.tempIndex), CLONE_TEMP_TO_TARGET: (state: CloneTempToSource) => Actions.cloneIndex(client, state.tempIndex, state.targetIndex), + REFRESH_TARGET: (state: RefreshTarget) => Actions.refreshIndex(client, state.targetIndex), UPDATE_TARGET_MAPPINGS: (state: UpdateTargetMappingsState) => Actions.updateAndPickupMappings(client, state.targetIndex, state.targetIndexMappings), UPDATE_TARGET_MAPPINGS_WAIT_FOR_TASK: (state: UpdateTargetMappingsWaitForTaskState) => Actions.waitForPickupUpdatedMappingsTask(client, state.updateTargetMappingsTaskId, '60s'), - OUTDATED_DOCUMENTS_SEARCH: (state: OutdatedDocumentsSearch) => - Actions.searchForOutdatedDocuments(client, { - batchSize: state.batchSize, - targetIndex: state.targetIndex, - outdatedDocumentsQuery: state.outdatedDocumentsQuery, - }), + OUTDATED_DOCUMENTS_SEARCH_OPEN_PIT: (state: OutdatedDocumentsSearchOpenPit) => + Actions.openPit(client, state.targetIndex), + OUTDATED_DOCUMENTS_SEARCH_READ: (state: OutdatedDocumentsSearchRead) => + Actions.readWithPit( + client, + state.pitId, + // search for outdated documents only + state.outdatedDocumentsQuery, + state.batchSize, + state.lastHitSortValue + ), + OUTDATED_DOCUMENTS_SEARCH_CLOSE_PIT: (state: OutdatedDocumentsSearchClosePit) => + Actions.closePit(client, state.pitId), + OUTDATED_DOCUMENTS_REFRESH: (state: OutdatedDocumentsRefresh) => + Actions.refreshIndex(client, state.targetIndex), OUTDATED_DOCUMENTS_TRANSFORM: (state: OutdatedDocumentsTransform) => - // Wait for a refresh to happen before returning. This ensures that when - // this Kibana instance searches for outdated documents, it won't find - // documents that were already transformed by itself or another Kibana - // instance. However, this causes each OUTDATED_DOCUMENTS_SEARCH -> - // OUTDATED_DOCUMENTS_TRANSFORM cycle to take 1s so when batches are - // small performance will become a lot worse. - // The alternative is to use a search_after with either a tie_breaker - // field or using a Point In Time as a cursor to go through all documents. Actions.transformDocs( client, transformRawDocs, state.outdatedDocuments, state.targetIndex, - 'wait_for' + /** + * Since we don't run a search against the target index, we disable "refresh" to speed up + * the migration process. + * Although any further step must run "refresh" for the target index + * before we reach out to the MARK_VERSION_INDEX_READY step. + * Right now, it's performed during OUTDATED_DOCUMENTS_REFRESH step. + */ + false ), MARK_VERSION_INDEX_READY: (state: MarkVersionIndexReady) => Actions.updateAliases(client, state.versionIndexReadyActions.value), diff --git a/src/core/server/saved_objects/migrationsv2/types.ts b/src/core/server/saved_objects/migrationsv2/types.ts index 50664bc9398fb..ac807e9d61776 100644 --- a/src/core/server/saved_objects/migrationsv2/types.ts +++ b/src/core/server/saved_objects/migrationsv2/types.ts @@ -42,7 +42,7 @@ export interface BaseState extends ControlState { readonly tempIndexMappings: IndexMapping; /** Script to apply to a legacy index before it can be used as a migration source */ readonly preMigrationScript: Option.Option; - readonly outdatedDocumentsQuery: Record; + readonly outdatedDocumentsQuery: estypes.QueryContainer; readonly retryCount: number; readonly retryDelay: number; /** @@ -101,14 +101,14 @@ export interface BaseState extends ControlState { * are no longer used. These saved objects will still be kept in the outdated * index for backup purposes, but won't be available in the upgraded index. */ - readonly unusedTypesQuery: Option.Option; + readonly unusedTypesQuery: estypes.QueryContainer; } -export type InitState = BaseState & { +export interface InitState extends BaseState { readonly controlState: 'INIT'; -}; +} -export type PostInitState = BaseState & { +export interface PostInitState extends BaseState { /** * The source index is the index from which the migration reads. If the * Option is a none, we didn't do any migration from a source index, either: @@ -121,20 +121,20 @@ export type PostInitState = BaseState & { /** The target index is the index to which the migration writes */ readonly targetIndex: string; readonly versionIndexReadyActions: Option.Option; - readonly outdatedDocumentsQuery: Record; -}; + readonly outdatedDocumentsQuery: estypes.QueryContainer; +} -export type DoneState = PostInitState & { +export interface DoneState extends PostInitState { /** Migration completed successfully */ readonly controlState: 'DONE'; -}; +} -export type FatalState = BaseState & { +export interface FatalState extends BaseState { /** Migration terminated with a failure */ readonly controlState: 'FATAL'; /** The reason the migration was terminated */ readonly reason: string; -}; +} export interface WaitForYellowSourceState extends BaseState { /** Wait for the source index to be yellow before requesting it. */ @@ -143,27 +143,27 @@ export interface WaitForYellowSourceState extends BaseState { readonly sourceIndexMappings: IndexMapping; } -export type SetSourceWriteBlockState = PostInitState & { +export interface SetSourceWriteBlockState extends PostInitState { /** Set a write block on the source index to prevent any further writes */ readonly controlState: 'SET_SOURCE_WRITE_BLOCK'; readonly sourceIndex: Option.Some; -}; +} -export type CreateNewTargetState = PostInitState & { +export interface CreateNewTargetState extends PostInitState { /** Blank ES cluster, create a new version-specific target index */ readonly controlState: 'CREATE_NEW_TARGET'; readonly sourceIndex: Option.None; readonly versionIndexReadyActions: Option.Some; -}; +} -export type CreateReindexTempState = PostInitState & { +export interface CreateReindexTempState extends PostInitState { /** * Create a target index with mappings from the source index and registered * plugins */ readonly controlState: 'CREATE_REINDEX_TEMP'; readonly sourceIndex: Option.Some; -}; +} export interface ReindexSourceToTempOpenPit extends PostInitState { /** Open PIT to the source index */ @@ -197,37 +197,67 @@ export type SetTempWriteBlock = PostInitState & { readonly sourceIndex: Option.Some; }; -export type CloneTempToSource = PostInitState & { +export interface CloneTempToSource extends PostInitState { /** * Clone the temporary reindex index into */ readonly controlState: 'CLONE_TEMP_TO_TARGET'; readonly sourceIndex: Option.Some; -}; +} -export type UpdateTargetMappingsState = PostInitState & { +export interface RefreshTarget extends PostInitState { + /** Refresh temp index before searching for outdated docs */ + readonly controlState: 'REFRESH_TARGET'; + readonly targetIndex: string; +} + +export interface UpdateTargetMappingsState extends PostInitState { /** Update the mappings of the target index */ readonly controlState: 'UPDATE_TARGET_MAPPINGS'; -}; +} -export type UpdateTargetMappingsWaitForTaskState = PostInitState & { +export interface UpdateTargetMappingsWaitForTaskState extends PostInitState { /** Update the mappings of the target index */ readonly controlState: 'UPDATE_TARGET_MAPPINGS_WAIT_FOR_TASK'; readonly updateTargetMappingsTaskId: string; -}; +} -export type OutdatedDocumentsSearch = PostInitState & { +export interface OutdatedDocumentsSearchOpenPit extends PostInitState { + /** Open PiT for target index to search for outdated documents */ + readonly controlState: 'OUTDATED_DOCUMENTS_SEARCH_OPEN_PIT'; +} + +export interface OutdatedDocumentsSearchRead extends PostInitState { /** Search for outdated documents in the target index */ - readonly controlState: 'OUTDATED_DOCUMENTS_SEARCH'; -}; + readonly controlState: 'OUTDATED_DOCUMENTS_SEARCH_READ'; + readonly pitId: string; + readonly lastHitSortValue: number[] | undefined; + readonly hasTransformedDocs: boolean; +} + +export interface OutdatedDocumentsSearchClosePit extends PostInitState { + /** Close PiT for target index when found all outdated documents */ + readonly controlState: 'OUTDATED_DOCUMENTS_SEARCH_CLOSE_PIT'; + readonly pitId: string; + readonly hasTransformedDocs: boolean; +} + +export interface OutdatedDocumentsRefresh extends PostInitState { + /** Reindex transformed documents */ + readonly controlState: 'OUTDATED_DOCUMENTS_REFRESH'; + readonly targetIndex: string; +} -export type OutdatedDocumentsTransform = PostInitState & { +export interface OutdatedDocumentsTransform extends PostInitState { /** Transform a batch of outdated documents to their latest version and write them to the target index */ readonly controlState: 'OUTDATED_DOCUMENTS_TRANSFORM'; + readonly pitId: string; readonly outdatedDocuments: SavedObjectsRawDoc[]; -}; + readonly lastHitSortValue: number[] | undefined; + readonly hasTransformedDocs: boolean; +} -export type MarkVersionIndexReady = PostInitState & { +export interface MarkVersionIndexReady extends PostInitState { /** * Marks the version-specific index as ready. Once this step is complete, * future Kibana instances will not have to prepare a target index by e.g. @@ -239,9 +269,9 @@ export type MarkVersionIndexReady = PostInitState & { */ readonly controlState: 'MARK_VERSION_INDEX_READY'; readonly versionIndexReadyActions: Option.Some; -}; +} -export type MarkVersionIndexReadyConflict = PostInitState & { +export interface MarkVersionIndexReadyConflict extends PostInitState { /** * If the MARK_VERSION_INDEX_READY step fails another instance was * performing the migration in parallel and won the race to marking the @@ -256,13 +286,13 @@ export type MarkVersionIndexReadyConflict = PostInitState & { * start a new migration to the latest version. */ readonly controlState: 'MARK_VERSION_INDEX_READY_CONFLICT'; -}; +} /** * If we're migrating from a legacy index we need to perform some additional * steps to prepare this index so that it can be used as a migration 'source'. */ -export type LegacyBaseState = PostInitState & { +export interface LegacyBaseState extends PostInitState { readonly sourceIndex: Option.Some; readonly legacyPreMigrationDoneActions: AliasAction[]; /** @@ -270,44 +300,44 @@ export type LegacyBaseState = PostInitState & { * target index. */ readonly legacyReindexTargetMappings: IndexMapping; -}; +} -export type LegacySetWriteBlockState = LegacyBaseState & { +export interface LegacySetWriteBlockState extends LegacyBaseState { /** Set a write block on the legacy index to prevent any further writes */ readonly controlState: 'LEGACY_SET_WRITE_BLOCK'; -}; +} -export type LegacyCreateReindexTargetState = LegacyBaseState & { +export interface LegacyCreateReindexTargetState extends LegacyBaseState { /** * Create a new index into which we can reindex the legacy index. This * index will have the same mappings as the legacy index. Once the legacy * pre-migration is complete, this index will be used a migration 'source'. */ readonly controlState: 'LEGACY_CREATE_REINDEX_TARGET'; -}; +} -export type LegacyReindexState = LegacyBaseState & { +export interface LegacyReindexState extends LegacyBaseState { /** * Reindex the legacy index into the new index created in the * LEGACY_CREATE_REINDEX_TARGET step (and apply the preMigration script). */ readonly controlState: 'LEGACY_REINDEX'; -}; +} -export type LegacyReindexWaitForTaskState = LegacyBaseState & { +export interface LegacyReindexWaitForTaskState extends LegacyBaseState { /** Wait for the reindex operation to complete */ readonly controlState: 'LEGACY_REINDEX_WAIT_FOR_TASK'; readonly legacyReindexTaskId: string; -}; +} -export type LegacyDeleteState = LegacyBaseState & { +export interface LegacyDeleteState extends LegacyBaseState { /** * After reindexed has completed, delete the legacy index so that it won't * conflict with the `currentAlias` that we want to create in a later step * e.g. `.kibana`. */ readonly controlState: 'LEGACY_DELETE'; -}; +} export type State = | FatalState @@ -325,8 +355,12 @@ export type State = | CloneTempToSource | UpdateTargetMappingsState | UpdateTargetMappingsWaitForTaskState - | OutdatedDocumentsSearch + | OutdatedDocumentsSearchOpenPit + | OutdatedDocumentsSearchRead + | OutdatedDocumentsSearchClosePit | OutdatedDocumentsTransform + | RefreshTarget + | OutdatedDocumentsRefresh | MarkVersionIndexReady | MarkVersionIndexReadyConflict | LegacyCreateReindexTargetState diff --git a/src/core/server/saved_objects/service/lib/aggregations/aggs_types/bucket_aggs.ts b/src/core/server/saved_objects/service/lib/aggregations/aggs_types/bucket_aggs.ts index 599c32137c553..186962b568792 100644 --- a/src/core/server/saved_objects/service/lib/aggregations/aggs_types/bucket_aggs.ts +++ b/src/core/server/saved_objects/service/lib/aggregations/aggs_types/bucket_aggs.ts @@ -14,6 +14,7 @@ import { schema as s, ObjectType } from '@kbn/config-schema'; * Currently supported: * - filter * - histogram + * - nested * - terms * * Not implemented: @@ -32,7 +33,6 @@ import { schema as s, ObjectType } from '@kbn/config-schema'; * - ip_range * - missing * - multi_terms - * - nested * - parent * - range * - rare_terms @@ -42,6 +42,7 @@ import { schema as s, ObjectType } from '@kbn/config-schema'; * - significant_text * - variable_width_histogram */ + export const bucketAggsSchemas: Record = { filter: s.object({ term: s.recordOf(s.string(), s.oneOf([s.string(), s.boolean(), s.number()])), @@ -71,6 +72,9 @@ export const bucketAggsSchemas: Record = { }) ), }), + nested: s.object({ + path: s.string(), + }), terms: s.object({ field: s.maybe(s.string()), collect_mode: s.maybe(s.string()), diff --git a/src/core/server/saved_objects/service/lib/aggregations/validation.test.ts b/src/core/server/saved_objects/service/lib/aggregations/validation.test.ts index 8a7c1c3719eb0..57421db76f5b6 100644 --- a/src/core/server/saved_objects/service/lib/aggregations/validation.test.ts +++ b/src/core/server/saved_objects/service/lib/aggregations/validation.test.ts @@ -16,6 +16,12 @@ const mockMappings = { updated_at: { type: 'date', }, + references: { + type: 'nested', + properties: { + id: 'keyword', + }, + }, foo: { properties: { title: { @@ -182,6 +188,40 @@ describe('validateAndConvertAggregations', () => { }); }); + it('validates a nested root aggregations', () => { + expect( + validateAndConvertAggregations( + ['alert'], + { + aggName: { + nested: { + path: 'alert.references', + }, + aggregations: { + aggName2: { + terms: { field: 'alert.references.id' }, + }, + }, + }, + }, + mockMappings + ) + ).toEqual({ + aggName: { + nested: { + path: 'references', + }, + aggregations: { + aggName2: { + terms: { + field: 'references.id', + }, + }, + }, + }, + }); + }); + it('rewrites type attributes when valid', () => { const aggregations: AggsMap = { average: { @@ -428,4 +468,50 @@ describe('validateAndConvertAggregations', () => { `"[someAgg.aggs.nested.max.script]: definition for this key is missing"` ); }); + + it('throws an error when trying to access a property via {type}.{type}.attributes.{attr}', () => { + expect(() => { + validateAndConvertAggregations( + ['alert'], + { + aggName: { + cardinality: { + field: 'alert.alert.attributes.actions.group', + }, + aggs: { + aggName: { + max: { field: 'alert.alert.attributes.actions.group' }, + }, + }, + }, + }, + mockMappings + ); + }).toThrowErrorMatchingInlineSnapshot( + '"[aggName.cardinality.field] Invalid attribute path: alert.alert.attributes.actions.group"' + ); + }); + + it('throws an error when trying to access a property via {type}.{type}.{attr}', () => { + expect(() => { + validateAndConvertAggregations( + ['alert'], + { + aggName: { + cardinality: { + field: 'alert.alert.actions.group', + }, + aggs: { + aggName: { + max: { field: 'alert.alert.actions.group' }, + }, + }, + }, + }, + mockMappings + ); + }).toThrowErrorMatchingInlineSnapshot( + '"[aggName.cardinality.field] Invalid attribute path: alert.alert.actions.group"' + ); + }); }); diff --git a/src/core/server/saved_objects/service/lib/aggregations/validation.ts b/src/core/server/saved_objects/service/lib/aggregations/validation.ts index a2fd392183132..cd41a23f4a28b 100644 --- a/src/core/server/saved_objects/service/lib/aggregations/validation.ts +++ b/src/core/server/saved_objects/service/lib/aggregations/validation.ts @@ -56,10 +56,13 @@ const validateAggregations = ( aggregations: Record, context: ValidationContext ) => { - return Object.entries(aggregations).reduce((memo, [aggrName, aggrContainer]) => { - memo[aggrName] = validateAggregation(aggrContainer, childContext(context, aggrName)); - return memo; - }, {} as Record); + return Object.entries(aggregations).reduce>( + (memo, [aggrName, aggrContainer]) => { + memo[aggrName] = validateAggregation(aggrContainer, childContext(context, aggrName)); + return memo; + }, + {} + ); }; /** @@ -93,15 +96,18 @@ const validateAggregationContainer = ( container: estypes.AggregationContainer, context: ValidationContext ) => { - return Object.entries(container).reduce((memo, [aggName, aggregation]) => { - if (aggregationKeys.includes(aggName)) { - return memo; - } - return { - ...memo, - [aggName]: validateAggregationType(aggName, aggregation, childContext(context, aggName)), - }; - }, {} as estypes.AggregationContainer); + return Object.entries(container).reduce( + (memo, [aggName, aggregation]) => { + if (aggregationKeys.includes(aggName)) { + return memo; + } + return { + ...memo, + [aggName]: validateAggregationType(aggName, aggregation, childContext(context, aggName)), + }; + }, + {} + ); }; const validateAggregationType = ( @@ -143,7 +149,7 @@ const validateAggregationStructure = ( * }, * ``` */ -const attributeFields = ['field']; +const attributeFields = ['field', 'path']; /** * List of fields that have a Record as value * diff --git a/src/core/server/saved_objects/service/lib/aggregations/validation_utils.ts b/src/core/server/saved_objects/service/lib/aggregations/validation_utils.ts index f817497e3759e..0b2cc8e235c9c 100644 --- a/src/core/server/saved_objects/service/lib/aggregations/validation_utils.ts +++ b/src/core/server/saved_objects/service/lib/aggregations/validation_utils.ts @@ -24,15 +24,17 @@ export const isRootLevelAttribute = ( allowedTypes: string[] ): boolean => { const splits = attributePath.split('.'); - if (splits.length !== 2) { + if (splits.length <= 1) { return false; } - const [type, fieldName] = splits; - if (allowedTypes.includes(fieldName)) { + const [type, firstPath, ...otherPaths] = splits; + if (allowedTypes.includes(firstPath)) { return false; } - return allowedTypes.includes(type) && fieldDefined(indexMapping, fieldName); + return ( + allowedTypes.includes(type) && fieldDefined(indexMapping, [firstPath, ...otherPaths].join('.')) + ); }; /** @@ -45,7 +47,8 @@ export const isRootLevelAttribute = ( * ``` */ export const rewriteRootLevelAttribute = (attributePath: string) => { - return attributePath.split('.')[1]; + const [, ...attributes] = attributePath.split('.'); + return attributes.join('.'); }; /** diff --git a/src/fixtures/telemetry_collectors/nested_collector.ts b/src/fixtures/telemetry_collectors/nested_collector.ts index 61642ea94e59d..812b7d7f452dc 100644 --- a/src/fixtures/telemetry_collectors/nested_collector.ts +++ b/src/fixtures/telemetry_collectors/nested_collector.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { CollectorSet, UsageCollector } from '../../plugins/usage_collection/server/collector'; +import { CollectorSet, Collector } from '../../plugins/usage_collection/server/collector'; import { loggerMock } from '../../core/server/logging/logger.mock'; const collectorSet = new CollectorSet({ @@ -19,7 +19,7 @@ interface Usage { } export class NestedInside { - collector?: UsageCollector; + collector?: Collector; createMyCollector() { this.collector = collectorSet.makeUsageCollector({ type: 'my_nested_collector', diff --git a/src/plugins/data/common/search/search_source/search_source.test.ts b/src/plugins/data/common/search/search_source/search_source.test.ts index 8e246b625706e..ef3e020747f7a 100644 --- a/src/plugins/data/common/search/search_source/search_source.test.ts +++ b/src/plugins/data/common/search/search_source/search_source.test.ts @@ -612,6 +612,7 @@ describe('SearchSource', () => { searchSource.setField('fields', ['*']); const request = searchSource.getSearchRequestBody(); + expect(request.hasOwnProperty('docvalue_fields')).toBe(false); expect(request.fields).toEqual([ { field: 'foo-bar' }, { field: 'field1' }, diff --git a/src/plugins/data/kibana.json b/src/plugins/data/kibana.json index 452b081d6387f..4e9e4c318c957 100644 --- a/src/plugins/data/kibana.json +++ b/src/plugins/data/kibana.json @@ -14,7 +14,6 @@ "optionalPlugins": ["usageCollection"], "extraPublicDirs": ["common"], "requiredBundles": [ - "usageCollection", "kibanaUtils", "kibanaReact", "inspector" diff --git a/src/plugins/data/public/autocomplete/collectors/create_usage_collector.ts b/src/plugins/data/public/autocomplete/collectors/create_usage_collector.ts index fc0cea2fdbc52..326fcb1e8153d 100644 --- a/src/plugins/data/public/autocomplete/collectors/create_usage_collector.ts +++ b/src/plugins/data/public/autocomplete/collectors/create_usage_collector.ts @@ -7,8 +7,9 @@ */ import { first } from 'rxjs/operators'; -import { StartServicesAccessor } from '../../../../../core/public'; -import { METRIC_TYPE, UsageCollectionSetup } from '../../../../usage_collection/public'; +import { METRIC_TYPE } from '@kbn/analytics'; +import type { StartServicesAccessor } from 'src/core/public'; +import type { UsageCollectionSetup } from 'src/plugins/usage_collection/public'; import { AUTOCOMPLETE_EVENT_TYPE, AutocompleteUsageCollector } from './types'; export const createUsageCollector = ( diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 868330ce078c7..d4f0ccfe810c6 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -77,7 +77,6 @@ import { PublicUiSettingsParams } from 'src/core/server/types'; import React from 'react'; import * as React_3 from 'react'; import { RecursiveReadonly } from '@kbn/utility-types'; -import { Reporter } from '@kbn/analytics'; import { Request as Request_2 } from '@hapi/hapi'; import { RequestAdapter } from 'src/plugins/inspector/common'; import { RequestStatistics as RequestStatistics_2 } from 'src/plugins/inspector/common'; @@ -102,6 +101,7 @@ import { Type } from '@kbn/config-schema'; import { TypeOf } from '@kbn/config-schema'; import { UiActionsSetup } from 'src/plugins/ui_actions/public'; import { UiActionsStart } from 'src/plugins/ui_actions/public'; +import { UiCounterMetricType } from '@kbn/analytics'; import { Unit } from '@elastic/datemath'; import { UnregisterCallback } from 'history'; import { URL } from 'url'; diff --git a/src/plugins/data/public/search/collectors/create_usage_collector.ts b/src/plugins/data/public/search/collectors/create_usage_collector.ts index 3fe135ea29152..d4f64fcfa962f 100644 --- a/src/plugins/data/public/search/collectors/create_usage_collector.ts +++ b/src/plugins/data/public/search/collectors/create_usage_collector.ts @@ -7,9 +7,10 @@ */ import { first } from 'rxjs/operators'; -import { UiCounterMetricType } from '@kbn/analytics'; -import { StartServicesAccessor } from '../../../../../core/public'; -import { METRIC_TYPE, UsageCollectionSetup } from '../../../../usage_collection/public'; +import { METRIC_TYPE } from '@kbn/analytics'; +import type { UiCounterMetricType } from '@kbn/analytics'; +import type { StartServicesAccessor } from 'src/core/public'; +import type { UsageCollectionSetup } from 'src/plugins/usage_collection/public'; import { SEARCH_EVENT_TYPE, SearchUsageCollector } from './types'; export const createUsageCollector = ( diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index a69df282cd568..e66eaab672e1c 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -55,7 +55,6 @@ import { PublicMethodsOf } from '@kbn/utility-types'; import { RecursiveReadonly } from '@kbn/utility-types'; import { RequestAdapter } from 'src/plugins/inspector/common'; import { RequestHandlerContext } from 'src/core/server'; -import * as Rx from 'rxjs'; import { SavedObject } from 'kibana/server'; import { SavedObject as SavedObject_2 } from 'src/core/server'; import { SavedObjectsClientContract } from 'src/core/server'; diff --git a/src/plugins/discover/public/__mocks__/index_pattern.ts b/src/plugins/discover/public/__mocks__/index_pattern.ts index e74046e9dc1ec..4fbeac80b0972 100644 --- a/src/plugins/discover/public/__mocks__/index_pattern.ts +++ b/src/plugins/discover/public/__mocks__/index_pattern.ts @@ -83,7 +83,7 @@ const indexPattern = ({ indexPattern.flattenHit = indexPatterns.flattenHitWrapper(indexPattern, indexPattern.metaFields); indexPattern.isTimeBased = () => !!indexPattern.timeFieldName; -indexPattern.formatField = (hit: Record, fieldName: string) => { +indexPattern.formatField = (hit: Record, fieldName: string) => { return fieldName === '_source' ? hit._source : indexPattern.flattenHit(hit)[fieldName]; }; diff --git a/src/plugins/discover/public/application/angular/context/api/context.ts b/src/plugins/discover/public/application/angular/context/api/context.ts index 820e37d754ef2..0316178862aa9 100644 --- a/src/plugins/discover/public/application/angular/context/api/context.ts +++ b/src/plugins/discover/public/application/angular/context/api/context.ts @@ -17,8 +17,10 @@ import { getServices } from '../../../../kibana_services'; export type SurrDocType = 'successors' | 'predecessors'; export interface EsHitRecord { + // eslint-disable-next-line @typescript-eslint/no-explicit-any fields: Record; sort: number[]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any _source: Record; _id: string; } diff --git a/src/plugins/discover/public/application/angular/context/api/utils/get_es_query_search_after.ts b/src/plugins/discover/public/application/angular/context/api/utils/get_es_query_search_after.ts index 1f745ab1b728e..fb0e58832a202 100644 --- a/src/plugins/discover/public/application/angular/context/api/utils/get_es_query_search_after.ts +++ b/src/plugins/discover/public/application/angular/context/api/utils/get_es_query_search_after.ts @@ -28,11 +28,11 @@ export function getEsQuerySearchAfter( // already surrounding docs -> first or last record is used const afterTimeRecIdx = type === 'successors' && documents.length ? documents.length - 1 : 0; const afterTimeDoc = documents[afterTimeRecIdx]; - let afterTimeValue = afterTimeDoc.sort[0]; + let afterTimeValue: string | number = afterTimeDoc.sort[0]; if (nanoSeconds) { afterTimeValue = useNewFieldsApi - ? afterTimeDoc.fields[timeFieldName][0] - : afterTimeDoc._source[timeFieldName]; + ? (afterTimeDoc.fields[timeFieldName] as Array)[0] + : (afterTimeDoc._source[timeFieldName] as string | number); } return [afterTimeValue, afterTimeDoc.sort[1]]; } @@ -42,8 +42,8 @@ export function getEsQuerySearchAfter( searchAfter[0] = anchor.sort[0]; if (nanoSeconds) { searchAfter[0] = useNewFieldsApi - ? anchor.fields[timeFieldName][0] - : anchor._source[timeFieldName]; + ? (anchor.fields[timeFieldName] as Array)[0] + : (anchor._source[timeFieldName] as string | number); } searchAfter[1] = anchor.sort[1]; return searchAfter; diff --git a/src/plugins/discover/public/application/angular/context/api/utils/sorting.ts b/src/plugins/discover/public/application/angular/context/api/utils/sorting.ts index 0290675489e54..c6f389ddca46a 100644 --- a/src/plugins/discover/public/application/angular/context/api/utils/sorting.ts +++ b/src/plugins/discover/public/application/angular/context/api/utils/sorting.ts @@ -27,7 +27,6 @@ export function getFirstSortableField(indexPattern: IndexPattern, fieldNames: st const sortableFields = fieldNames.filter( (fieldName) => META_FIELD_NAMES.includes(fieldName) || - // @ts-ignore (indexPattern.fields.getByName(fieldName) || { sortable: false }).sortable ); return sortableFields[0]; diff --git a/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar_directive.ts b/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar_directive.ts index ddcd88a168788..dd8c874391fd4 100644 --- a/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar_directive.ts +++ b/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar_directive.ts @@ -9,6 +9,7 @@ import { getAngularModule } from '../../../../../kibana_services'; import { ActionBar } from './action_bar'; +// eslint-disable-next-line @typescript-eslint/no-explicit-any getAngularModule().directive('contextActionBar', function (reactDirective: any) { return reactDirective(ActionBar); }); diff --git a/src/plugins/discover/public/application/angular/context/query_parameters/actions.test.ts b/src/plugins/discover/public/application/angular/context/query_parameters/actions.test.ts index 774a29721c426..efadf2105074e 100644 --- a/src/plugins/discover/public/application/angular/context/query_parameters/actions.test.ts +++ b/src/plugins/discover/public/application/angular/context/query_parameters/actions.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -// @ts-ignore +// @ts-expect-error import { getQueryParameterActions } from './actions'; import { FilterManager } from '../../../../../../data/public'; import { coreMock } from '../../../../../../../core/public/mocks'; diff --git a/src/plugins/discover/public/application/angular/context_state.test.ts b/src/plugins/discover/public/application/angular/context_state.test.ts index b49a6546a993d..ed4a74c70112b 100644 --- a/src/plugins/discover/public/application/angular/context_state.test.ts +++ b/src/plugins/discover/public/application/angular/context_state.test.ts @@ -12,16 +12,17 @@ import { createBrowserHistory, History } from 'history'; import { FilterManager, Filter } from '../../../../data/public'; import { coreMock } from '../../../../../core/public/mocks'; import { SEARCH_FIELDS_FROM_SOURCE } from '../../../common'; + const setupMock = coreMock.createSetup(); describe('Test Discover Context State', () => { let history: History; - let state: any; + let state: ReturnType; const getCurrentUrl = () => history.createHref(history.location); beforeEach(async () => { history = createBrowserHistory(); history.push('/'); - state = await getState({ + state = getState({ defaultStepSize: '4', timeFieldName: 'time', history, diff --git a/src/plugins/discover/public/application/angular/context_state.ts b/src/plugins/discover/public/application/angular/context_state.ts index 0bae006ec1f6e..8ac111a8fe087 100644 --- a/src/plugins/discover/public/application/angular/context_state.ts +++ b/src/plugins/discover/public/application/angular/context_state.ts @@ -13,8 +13,8 @@ import { createStateContainer, createKbnUrlStateStorage, syncStates, - BaseStateContainer, withNotifyOnErrors, + ReduxLikeStateContainer, } from '../../../../kibana_utils/public'; import { esFilters, FilterManager, Filter, Query } from '../../../../data/public'; import { handleSourceColumnState } from './helpers'; @@ -85,11 +85,11 @@ interface GetStateReturn { /** * Global state, the _g part of the URL */ - globalState: BaseStateContainer; + globalState: ReduxLikeStateContainer; /** * App state, the _a part of the URL */ - appState: BaseStateContainer; + appState: ReduxLikeStateContainer; /** * Start sync between state and URL */ diff --git a/src/plugins/discover/public/application/angular/directives/debounce/debounce.test.ts b/src/plugins/discover/public/application/angular/directives/debounce/debounce.test.ts index a41f6e6259041..0c033fd066eab 100644 --- a/src/plugins/discover/public/application/angular/directives/debounce/debounce.test.ts +++ b/src/plugins/discover/public/application/angular/directives/debounce/debounce.test.ts @@ -12,7 +12,7 @@ import 'angular-mocks'; import 'angular-sanitize'; import 'angular-route'; -// @ts-ignore +// @ts-expect-error import { createDebounceProviderTimeout } from './debounce'; import { coreMock } from '../../../../../../../core/public/mocks'; import { initializeInnerAngularModule } from '../../../../get_inner_angular'; @@ -21,6 +21,7 @@ import { dataPluginMock } from '../../../../../../data/public/mocks'; import { initAngularBootstrap } from '../../../../../../kibana_legacy/public'; describe('debounce service', function () { + // eslint-disable-next-line @typescript-eslint/no-explicit-any let debounce: (fn: () => void, timeout: number, options?: any) => any; let $timeout: ITimeoutService; let spy: SinonSpy; diff --git a/src/plugins/discover/public/application/angular/doc.ts b/src/plugins/discover/public/application/angular/doc.ts index 0745e21378956..27af3a96bbc84 100644 --- a/src/plugins/discover/public/application/angular/doc.ts +++ b/src/plugins/discover/public/application/angular/doc.ts @@ -7,17 +7,17 @@ */ import { getAngularModule, getServices } from '../../kibana_services'; -// @ts-ignore import { getRootBreadcrumbs } from '../helpers/breadcrumbs'; import html from './doc.html'; import { Doc } from '../components/doc/doc'; interface LazyScope extends ng.IScope { - [key: string]: any; + [key: string]: unknown; } const { timefilter } = getServices(); const app = getAngularModule(); +// eslint-disable-next-line @typescript-eslint/no-explicit-any app.directive('discoverDoc', function (reactDirective: any) { return reactDirective( Doc, @@ -31,6 +31,7 @@ app.directive('discoverDoc', function (reactDirective: any) { ); }); +// eslint-disable-next-line @typescript-eslint/no-explicit-any app.config(($routeProvider: any) => { $routeProvider .when('/doc/:indexPattern/:index/:type', { @@ -39,7 +40,7 @@ app.config(($routeProvider: any) => { // the new route, es 7 deprecated types, es 8 removed them .when('/doc/:indexPattern/:index', { // have to be written as function expression, because it's not compiled in dev mode - // eslint-disable-next-line object-shorthand + // eslint-disable-next-line @typescript-eslint/no-explicit-any, object-shorthand controller: function ($scope: LazyScope, $route: any) { timefilter.disableAutoRefreshSelector(); timefilter.disableTimeRangeSelector(); @@ -49,6 +50,7 @@ app.config(($routeProvider: any) => { $scope.indexPatternService = getServices().indexPatterns; }, template: html, + // eslint-disable-next-line @typescript-eslint/no-explicit-any k7Breadcrumbs: ($route: any) => [ ...getRootBreadcrumbs(), { diff --git a/src/plugins/discover/public/application/angular/doc_table/components/pager/index.ts b/src/plugins/discover/public/application/angular/doc_table/components/pager/index.ts index 8b1df567db6cb..180da83beb2af 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/pager/index.ts +++ b/src/plugins/discover/public/application/angular/doc_table/components/pager/index.ts @@ -9,10 +9,12 @@ import { ToolBarPagerText } from './tool_bar_pager_text'; import { ToolBarPagerButtons } from './tool_bar_pager_buttons'; +// eslint-disable-next-line @typescript-eslint/no-explicit-any export function createToolBarPagerTextDirective(reactDirective: any) { return reactDirective(ToolBarPagerText); } +// eslint-disable-next-line @typescript-eslint/no-explicit-any export function createToolBarPagerButtonsDirective(reactDirective: any) { return reactDirective(ToolBarPagerButtons); } diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_header.ts b/src/plugins/discover/public/application/angular/doc_table/components/table_header.ts index dd6017674e5dc..5e3a025d8c7ba 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_header.ts +++ b/src/plugins/discover/public/application/angular/doc_table/components/table_header.ts @@ -11,6 +11,7 @@ import { getServices } from '../../../../kibana_services'; import { SORT_DEFAULT_ORDER_SETTING, DOC_HIDE_TIME_COLUMN_SETTING } from '../../../../../common'; import { UI_SETTINGS } from '../../../../../../data/public'; +// eslint-disable-next-line @typescript-eslint/no-explicit-any export function createTableHeaderDirective(reactDirective: any) { const { uiSettings: config } = getServices(); diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header.tsx b/src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header.tsx index 8519b1e81b0a4..57f7382bd98ca 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header.tsx +++ b/src/plugins/discover/public/application/angular/doc_table/components/table_header/table_header.tsx @@ -8,7 +8,6 @@ import React from 'react'; import { IndexPattern } from '../../../../../kibana_services'; -// @ts-ignore import { TableHeaderColumn } from './table_header_column'; import { SortOrder, getDisplayedColumns } from './helpers'; import { getDefaultSort } from '../../lib/get_default_sort'; diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row.ts b/src/plugins/discover/public/application/angular/doc_table/components/table_row.ts index cf6b507edc070..58ddf1eb7ba25 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_row.ts +++ b/src/plugins/discover/public/application/angular/doc_table/components/table_row.ts @@ -32,6 +32,7 @@ export function noWhiteSpace(html: string): string { const MIN_LINE_LENGTH = 20; interface LazyScope extends ng.IScope { + // eslint-disable-next-line @typescript-eslint/no-explicit-any [key: string]: any; } @@ -94,6 +95,7 @@ export function createTableRowDirective($compile: ng.ICompileService) { createSummaryRow($scope.row); }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any $scope.inlineFilter = function inlineFilter($event: any, type: string) { const column = $($event.currentTarget).data().column; const field = $scope.indexPattern.fields.getByName(column); @@ -119,6 +121,7 @@ export function createTableRowDirective($compile: ng.ICompileService) { }; // create a tr element that lists the value for each *column* + // eslint-disable-next-line @typescript-eslint/no-explicit-any function createSummaryRow(row: any) { const indexPattern = $scope.indexPattern; $scope.flattenedRow = indexPattern.flattenHit(row); @@ -188,7 +191,7 @@ export function createTableRowDirective($compile: ng.ICompileService) { const $cell = $cells.eq(i); if ($cell.data('discover:html') === html) return; - const reuse = find($cells.slice(i + 1), function (cell: any) { + const reuse = find($cells.slice(i + 1), (cell) => { return $.data(cell, 'discover:html') === html; }); @@ -222,6 +225,7 @@ export function createTableRowDirective($compile: ng.ICompileService) { /** * Fill an element with the value of a field */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any function _displayField(row: any, fieldName: string, truncate = false) { const indexPattern = $scope.indexPattern; const text = indexPattern.formatField(row, fieldName); diff --git a/src/plugins/discover/public/application/angular/doc_table/create_doc_table_react.tsx b/src/plugins/discover/public/application/angular/doc_table/create_doc_table_react.tsx index 0202f88e0e902..b8e8bf1fe7d6c 100644 --- a/src/plugins/discover/public/application/angular/doc_table/create_doc_table_react.tsx +++ b/src/plugins/discover/public/application/angular/doc_table/create_doc_table_react.tsx @@ -45,6 +45,7 @@ export type AngularScope = IScope & { renderProps?: DocTableLegacyProps }; export async function injectAngularElement( domNode: Element, template: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any renderProps: any, injector: auto.IInjectorService ) { @@ -64,6 +65,7 @@ export async function injectAngularElement( return newScope; } +// eslint-disable-next-line @typescript-eslint/no-explicit-any function getRenderFn(domNode: Element, props: any) { const directive = { template: ` { + $scope.$watch('hits', (hits: unknown[]) => { if (!hits) return; // Reset infinite scroll limit diff --git a/src/plugins/discover/public/application/angular/doc_table/infinite_scroll.ts b/src/plugins/discover/public/application/angular/doc_table/infinite_scroll.ts index 82fa646513753..f2377b61b5151 100644 --- a/src/plugins/discover/public/application/angular/doc_table/infinite_scroll.ts +++ b/src/plugins/discover/public/application/angular/doc_table/infinite_scroll.ts @@ -9,6 +9,7 @@ import $ from 'jquery'; interface LazyScope extends ng.IScope { + // eslint-disable-next-line @typescript-eslint/no-explicit-any [key: string]: any; } @@ -19,6 +20,7 @@ export function createInfiniteScrollDirective() { more: '=', }, link: ($scope: LazyScope, $element: JQuery) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any let checkTimer: any; /** * depending on which version of Discover is displayed, different elements are scrolling diff --git a/src/plugins/discover/public/application/angular/doc_table/lib/get_default_sort.test.ts b/src/plugins/discover/public/application/angular/doc_table/lib/get_default_sort.test.ts index c73656435fb58..f181d583f0211 100644 --- a/src/plugins/discover/public/application/angular/doc_table/lib/get_default_sort.test.ts +++ b/src/plugins/discover/public/application/angular/doc_table/lib/get_default_sort.test.ts @@ -7,7 +7,7 @@ */ import { getDefaultSort } from './get_default_sort'; -// @ts-ignore +// @ts-expect-error import FixturesStubbedLogstashIndexPatternProvider from '../../../../__fixtures__/stubbed_logstash_index_pattern'; import { IndexPattern } from '../../../../kibana_services'; diff --git a/src/plugins/discover/public/application/angular/doc_table/lib/get_sort.test.ts b/src/plugins/discover/public/application/angular/doc_table/lib/get_sort.test.ts index bd28987b4fdbd..19d629e14da66 100644 --- a/src/plugins/discover/public/application/angular/doc_table/lib/get_sort.test.ts +++ b/src/plugins/discover/public/application/angular/doc_table/lib/get_sort.test.ts @@ -7,7 +7,7 @@ */ import { getSort, getSortArray } from './get_sort'; -// @ts-ignore +// @ts-expect-error import FixturesStubbedLogstashIndexPatternProvider from '../../../../__fixtures__/stubbed_logstash_index_pattern'; import { IndexPattern } from '../../../../kibana_services'; diff --git a/src/plugins/discover/public/application/angular/doc_table/lib/get_sort_for_search_source.test.ts b/src/plugins/discover/public/application/angular/doc_table/lib/get_sort_for_search_source.test.ts index f0a13557af9fd..dc7817d95dd38 100644 --- a/src/plugins/discover/public/application/angular/doc_table/lib/get_sort_for_search_source.test.ts +++ b/src/plugins/discover/public/application/angular/doc_table/lib/get_sort_for_search_source.test.ts @@ -7,7 +7,7 @@ */ import { getSortForSearchSource } from './get_sort_for_search_source'; -// @ts-ignore +// @ts-expect-error import FixturesStubbedLogstashIndexPatternProvider from '../../../../__fixtures__/stubbed_logstash_index_pattern'; import { IndexPattern } from '../../../../kibana_services'; import { SortOrder } from '../components/table_header/helpers'; diff --git a/src/plugins/discover/public/application/angular/doc_table/lib/pager/pager_factory.ts b/src/plugins/discover/public/application/angular/doc_table/lib/pager/pager_factory.ts index 964ac585248b8..7cd36d419969e 100644 --- a/src/plugins/discover/public/application/angular/doc_table/lib/pager/pager_factory.ts +++ b/src/plugins/discover/public/application/angular/doc_table/lib/pager/pager_factory.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -// @ts-ignore +// @ts-expect-error import { Pager } from './pager'; export function createPagerFactory() { diff --git a/src/plugins/discover/public/application/angular/doc_viewer.tsx b/src/plugins/discover/public/application/angular/doc_viewer.tsx index 0b4f3c3cf036a..2b51b68b2fb34 100644 --- a/src/plugins/discover/public/application/angular/doc_viewer.tsx +++ b/src/plugins/discover/public/application/angular/doc_viewer.tsx @@ -9,9 +9,10 @@ import React from 'react'; import { DocViewer } from '../components/doc_viewer/doc_viewer'; +// eslint-disable-next-line @typescript-eslint/no-explicit-any export function createDocViewerDirective(reactDirective: any) { return reactDirective( - (props: any) => { + (props: React.ComponentProps) => { return ; }, [ diff --git a/src/plugins/discover/public/application/angular/helpers/row_formatter.test.ts b/src/plugins/discover/public/application/angular/helpers/row_formatter.test.ts index ca5cdbd808606..80fb8a570f78b 100644 --- a/src/plugins/discover/public/application/angular/helpers/row_formatter.test.ts +++ b/src/plugins/discover/public/application/angular/helpers/row_formatter.test.ts @@ -11,6 +11,7 @@ import { stubbedSavedObjectIndexPattern } from '../../../__mocks__/stubbed_saved import { IndexPattern } from '../../../../../data/common/index_patterns/index_patterns'; import { fieldFormatsMock } from '../../../../../data/common/field_formats/mocks'; import { setServices } from '../../../kibana_services'; +import { DiscoverServices } from '../../../build_services'; describe('Row formatter', () => { const hit = { @@ -59,11 +60,11 @@ describe('Row formatter', () => { beforeEach(() => { // @ts-expect-error indexPattern.formatHit = formatHitMock; - setServices({ + setServices(({ uiSettings: { get: () => 100, }, - }); + } as unknown) as DiscoverServices); }); it('formats document properly', () => { @@ -73,11 +74,11 @@ describe('Row formatter', () => { }); it('limits number of rendered items', () => { - setServices({ + setServices(({ uiSettings: { get: () => 1, }, - }); + } as unknown) as DiscoverServices); expect(formatRow(hit, indexPattern).trim()).toMatchInlineSnapshot( `"
also:
with \\\\"quotes\\\\" or 'single qoutes'
"` ); diff --git a/src/plugins/discover/public/application/angular/helpers/row_formatter.tsx b/src/plugins/discover/public/application/angular/helpers/row_formatter.tsx index c5e64f38a3117..c410273cc7510 100644 --- a/src/plugins/discover/public/application/angular/helpers/row_formatter.tsx +++ b/src/plugins/discover/public/application/angular/helpers/row_formatter.tsx @@ -30,6 +30,7 @@ const TemplateComponent = ({ defPairs }: Props) => { ); }; +// eslint-disable-next-line @typescript-eslint/no-explicit-any export const formatRow = (hit: Record, indexPattern: IndexPattern) => { const highlights = hit?.highlight ?? {}; // Keys are sorted in the hits object @@ -49,7 +50,9 @@ export const formatRow = (hit: Record, indexPattern: IndexPattern) }; export const formatTopLevelObject = ( + // eslint-disable-next-line @typescript-eslint/no-explicit-any row: Record, + // eslint-disable-next-line @typescript-eslint/no-explicit-any fields: Record, indexPattern: IndexPattern ) => { diff --git a/src/plugins/discover/public/application/angular/redirect.ts b/src/plugins/discover/public/application/angular/redirect.ts index a1717baf9226f..5014376ff13af 100644 --- a/src/plugins/discover/public/application/angular/redirect.ts +++ b/src/plugins/discover/public/application/angular/redirect.ts @@ -8,8 +8,10 @@ import { getAngularModule, getServices, getUrlTracker } from '../../kibana_services'; +// eslint-disable-next-line @typescript-eslint/no-explicit-any getAngularModule().config(($routeProvider: any) => { $routeProvider.otherwise({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any resolveRedirectTo: ($rootScope: any) => { const path = window.location.hash.substr(1); getUrlTracker().restorePreviousUrl(); diff --git a/src/plugins/discover/public/application/components/context_app/context_app_legacy_directive.ts b/src/plugins/discover/public/application/components/context_app/context_app_legacy_directive.ts index 5b0ef60b6079a..fc64abfb51025 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app_legacy_directive.ts +++ b/src/plugins/discover/public/application/components/context_app/context_app_legacy_directive.ts @@ -8,6 +8,7 @@ import { ContextAppLegacy } from './context_app_legacy'; +// eslint-disable-next-line @typescript-eslint/no-explicit-any export function createContextAppLegacy(reactDirective: any) { return reactDirective(ContextAppLegacy, [ ['filter', { watchDepth: 'reference' }], diff --git a/src/plugins/discover/public/application/components/context_error_message/context_error_message.test.tsx b/src/plugins/discover/public/application/components/context_error_message/context_error_message.test.tsx index 3edd8f2514e63..cd3571f447cf5 100644 --- a/src/plugins/discover/public/application/components/context_error_message/context_error_message.test.tsx +++ b/src/plugins/discover/public/application/components/context_error_message/context_error_message.test.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { mountWithIntl } from '@kbn/test/jest'; import { ReactWrapper } from 'enzyme'; import { ContextErrorMessage } from './context_error_message'; -// @ts-ignore +// @ts-expect-error import { FAILURE_REASONS, LOADING_STATUS } from '../../angular/context/query'; import { findTestSubject } from '@elastic/eui/lib/test'; diff --git a/src/plugins/discover/public/application/components/context_error_message/context_error_message.tsx b/src/plugins/discover/public/application/components/context_error_message/context_error_message.tsx index 83cb6981d761e..37791e0350ef7 100644 --- a/src/plugins/discover/public/application/components/context_error_message/context_error_message.tsx +++ b/src/plugins/discover/public/application/components/context_error_message/context_error_message.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { EuiCallOut, EuiText } from '@elastic/eui'; import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; -// @ts-ignore +// @ts-expect-error import { FAILURE_REASONS, LOADING_STATUS } from '../../angular/context/query'; export interface ContextErrorMessageProps { diff --git a/src/plugins/discover/public/application/components/create_discover_directive.ts b/src/plugins/discover/public/application/components/create_discover_directive.ts index cc88ef03c5d03..f8c74c07457aa 100644 --- a/src/plugins/discover/public/application/components/create_discover_directive.ts +++ b/src/plugins/discover/public/application/components/create_discover_directive.ts @@ -7,6 +7,7 @@ */ import { Discover } from './discover'; +// eslint-disable-next-line @typescript-eslint/no-explicit-any export function createDiscoverDirective(reactDirective: any) { return reactDirective(Discover, [ ['fetch', { watchDepth: 'reference' }], diff --git a/src/plugins/discover/public/application/components/create_discover_grid_directive.tsx b/src/plugins/discover/public/application/components/create_discover_grid_directive.tsx index d55e46574e1a7..39a9df8926023 100644 --- a/src/plugins/discover/public/application/components/create_discover_grid_directive.tsx +++ b/src/plugins/discover/public/application/components/create_discover_grid_directive.tsx @@ -33,6 +33,7 @@ export function DiscoverGridEmbeddable(props: DiscoverGridProps) { /** * this is just needed for the embeddable */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any export function createDiscoverGridDirective(reactDirective: any) { return reactDirective(DiscoverGridEmbeddable, [ ['columns', { watchDepth: 'collection' }], diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_cell_actions.test.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_cell_actions.test.tsx index 4684c9e49ab31..965d3cb6a30c4 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_cell_actions.test.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_cell_actions.test.tsx @@ -32,6 +32,7 @@ describe('Discover cell actions ', function () { const component = mountWithIntl( } rowIndex={1} columnId={'extension'} @@ -59,6 +60,7 @@ describe('Discover cell actions ', function () { const component = mountWithIntl( } rowIndex={1} columnId={'extension'} diff --git a/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.tsx b/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.tsx index d258c5d36ff75..fc3dd499f92e0 100644 --- a/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.tsx @@ -115,7 +115,7 @@ export const getRenderCellValueFn = ( if (typeof rowFlattened[columnId] === 'object' && isDetails) { return ( } + json={rowFlattened[columnId] as Record} width={defaultMonacoEditorWidth} /> ); @@ -123,7 +123,8 @@ export const getRenderCellValueFn = ( if (field && field.type === '_source') { if (isDetails) { - return ; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return ; } const formatted = indexPattern.formatHit(row); diff --git a/src/plugins/discover/public/application/components/doc/doc.test.tsx b/src/plugins/discover/public/application/components/doc/doc.test.tsx index deaaa1853ae9d..7367315eae7fb 100644 --- a/src/plugins/discover/public/application/components/doc/doc.test.tsx +++ b/src/plugins/discover/public/application/components/doc/doc.test.tsx @@ -18,6 +18,7 @@ import { SEARCH_FIELDS_FROM_SOURCE as mockSearchFieldsFromSource } from '../../. const mockSearchApi = jest.fn(); jest.mock('../../../kibana_services', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any let registry: any[] = []; return { @@ -46,6 +47,7 @@ jest.mock('../../../kibana_services', () => { }, }), getDocViewsRegistry: () => ({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any addDocView(view: any) { registry.push(view); }, @@ -72,12 +74,14 @@ const waitForPromises = async () => * this works but logs ugly error messages until we're using React 16.9 * should be adapted when we upgrade */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any async function mountDoc(update = false, indexPatternGetter: any = null) { const indexPattern = { getComputedFields: () => [], }; const indexPatternService = { get: indexPatternGetter ? indexPatternGetter : jest.fn(() => Promise.resolve(indexPattern)), + // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any; const props = { diff --git a/src/plugins/discover/public/application/components/doc/use_es_doc_search.test.tsx b/src/plugins/discover/public/application/components/doc/use_es_doc_search.test.tsx index b5cdc7db6c6c9..f3a6b274649f5 100644 --- a/src/plugins/discover/public/application/components/doc/use_es_doc_search.test.tsx +++ b/src/plugins/discover/public/application/components/doc/use_es_doc_search.test.tsx @@ -11,6 +11,7 @@ import { buildSearchBody, useEsDocSearch, ElasticRequestState } from './use_es_d import { DocProps } from './doc'; import { Observable } from 'rxjs'; import { SEARCH_FIELDS_FROM_SOURCE as mockSearchFieldsFromSource } from '../../../../common'; +import { IndexPattern } from 'src/plugins/data/common'; const mockSearchResult = new Observable(); @@ -35,60 +36,60 @@ jest.mock('../../../kibana_services', () => ({ describe('Test of helper / hook', () => { test('buildSearchBody given useNewFieldsApi is false', () => { - const indexPattern = { + const indexPattern = ({ getComputedFields: () => ({ storedFields: [], scriptFields: [], docvalueFields: [] }), - } as any; + } as unknown) as IndexPattern; const actual = buildSearchBody('1', indexPattern, false); expect(actual).toMatchInlineSnapshot(` Object { - "_source": true, - "docvalue_fields": Array [], - "fields": undefined, - "query": Object { - "ids": Object { - "values": Array [ - "1", - ], + "body": Object { + "_source": true, + "fields": Array [], + "query": Object { + "ids": Object { + "values": Array [ + "1", + ], + }, }, + "script_fields": Array [], + "stored_fields": Array [], }, - "runtime_mappings": Object {}, - "script_fields": Array [], - "stored_fields": Array [], } `); }); test('buildSearchBody useNewFieldsApi is true', () => { - const indexPattern = { + const indexPattern = ({ getComputedFields: () => ({ storedFields: [], scriptFields: [], docvalueFields: [] }), - } as any; + } as unknown) as IndexPattern; const actual = buildSearchBody('1', indexPattern, true); expect(actual).toMatchInlineSnapshot(` Object { - "_source": false, - "docvalue_fields": Array [], - "fields": Array [ - Object { - "field": "*", - "include_unmapped": "true", - }, - ], - "query": Object { - "ids": Object { - "values": Array [ - "1", - ], + "body": Object { + "fields": Array [ + Object { + "field": "*", + "include_unmapped": "true", + }, + ], + "query": Object { + "ids": Object { + "values": Array [ + "1", + ], + }, }, + "runtime_mappings": Object {}, + "script_fields": Array [], + "stored_fields": Array [], }, - "runtime_mappings": Object {}, - "script_fields": Array [], - "stored_fields": Array [], } `); }); test('buildSearchBody with runtime fields', () => { - const indexPattern = { + const indexPattern = ({ getComputedFields: () => ({ storedFields: [], scriptFields: [], @@ -102,35 +103,35 @@ describe('Test of helper / hook', () => { }, }, }), - } as any; + } as unknown) as IndexPattern; const actual = buildSearchBody('1', indexPattern, true); expect(actual).toMatchInlineSnapshot(` Object { - "_source": false, - "docvalue_fields": Array [], - "fields": Array [ - Object { - "field": "*", - "include_unmapped": "true", - }, - ], - "query": Object { - "ids": Object { - "values": Array [ - "1", - ], + "body": Object { + "fields": Array [ + Object { + "field": "*", + "include_unmapped": "true", + }, + ], + "query": Object { + "ids": Object { + "values": Array [ + "1", + ], + }, }, - }, - "runtime_mappings": Object { - "myRuntimeField": Object { - "script": Object { - "source": "emit(10.0)", + "runtime_mappings": Object { + "myRuntimeField": Object { + "script": Object { + "source": "emit(10.0)", + }, + "type": "double", }, - "type": "double", }, + "script_fields": Array [], + "stored_fields": Array [], }, - "script_fields": Array [], - "stored_fields": Array [], } `); }); @@ -139,21 +140,22 @@ describe('Test of helper / hook', () => { const indexPattern = { getComputedFields: () => [], }; - const indexPatternService = { - get: jest.fn(() => Promise.resolve(indexPattern)), - } as any; - const props = { + const getMock = jest.fn(() => Promise.resolve(indexPattern)); + const indexPatternService = ({ + get: getMock, + } as unknown) as IndexPattern; + const props = ({ id: '1', index: 'index1', indexPatternId: 'xyz', indexPatternService, - } as DocProps; - let hook; + } as unknown) as DocProps; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let hook: any; await act(async () => { hook = renderHook((p: DocProps) => useEsDocSearch(p), { initialProps: props }); }); - // @ts-ignore expect(hook.result.current).toEqual([ElasticRequestState.Loading, null, indexPattern]); - expect(indexPatternService.get).toHaveBeenCalled(); + expect(getMock).toHaveBeenCalled(); }); }); diff --git a/src/plugins/discover/public/application/components/doc/use_es_doc_search.ts b/src/plugins/discover/public/application/components/doc/use_es_doc_search.ts index 0dcf828048a07..6b8e912bbffba 100644 --- a/src/plugins/discover/public/application/components/doc/use_es_doc_search.ts +++ b/src/plugins/discover/public/application/components/doc/use_es_doc_search.ts @@ -7,11 +7,14 @@ */ import { useEffect, useState, useMemo } from 'react'; +import type { estypes } from '@elastic/elasticsearch'; import { IndexPattern, getServices } from '../../../kibana_services'; import { DocProps } from './doc'; import { ElasticSearchHit } from '../../doc_views/doc_views_types'; import { SEARCH_FIELDS_FROM_SOURCE } from '../../../../common'; +type RequestBody = Pick; + export enum ElasticRequestState { Loading, NotFound, @@ -28,22 +31,32 @@ export function buildSearchBody( id: string, indexPattern: IndexPattern, useNewFieldsApi: boolean -): Record { +): RequestBody | undefined { const computedFields = indexPattern.getComputedFields(); - const runtimeFields = indexPattern.getComputedFields().runtimeFields; - return { - query: { - ids: { - values: [id], + const runtimeFields = computedFields.runtimeFields as estypes.RuntimeFields; + const request: RequestBody = { + body: { + query: { + ids: { + values: [id], + }, }, + stored_fields: computedFields.storedFields, + script_fields: computedFields.scriptFields, }, - stored_fields: computedFields.storedFields, - _source: !useNewFieldsApi, - fields: useNewFieldsApi ? [{ field: '*', include_unmapped: 'true' }] : undefined, - script_fields: computedFields.scriptFields, - docvalue_fields: computedFields.docvalueFields, - runtime_mappings: useNewFieldsApi && runtimeFields ? runtimeFields : {}, // needed for index pattern runtime fields in a single doc view }; + if (!request.body) { + return undefined; + } + if (useNewFieldsApi) { + // @ts-expect-error + request.body.fields = [{ field: '*', include_unmapped: 'true' }]; + request.body.runtime_mappings = runtimeFields ? runtimeFields : {}; + } else { + request.body._source = true; + } + request.body.fields = [...(request.body?.fields || []), ...(computedFields.docvalueFields || [])]; + return request; } /** @@ -71,7 +84,7 @@ export function useEsDocSearch({ .search({ params: { index, - body: buildSearchBody(id, indexPatternEntity, useNewFieldsApi), + body: buildSearchBody(id, indexPatternEntity, useNewFieldsApi)?.body, }, }) .toPromise(); diff --git a/src/plugins/discover/public/application/components/doc_viewer/doc_viewer.test.tsx b/src/plugins/discover/public/application/components/doc_viewer/doc_viewer.test.tsx index 6afa7f89371f9..de0353a020a67 100644 --- a/src/plugins/discover/public/application/components/doc_viewer/doc_viewer.test.tsx +++ b/src/plugins/discover/public/application/components/doc_viewer/doc_viewer.test.tsx @@ -14,6 +14,7 @@ import { getDocViewsRegistry } from '../../../kibana_services'; import { DocViewRenderProps } from '../../doc_views/doc_views_types'; jest.mock('../../../kibana_services', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any let registry: any[] = []; return { getServices: () => ({ @@ -22,6 +23,7 @@ jest.mock('../../../kibana_services', () => { }, }), getDocViewsRegistry: () => ({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any addDocView(view: any) { registry.push(view); }, @@ -36,6 +38,7 @@ jest.mock('../../../kibana_services', () => { }); beforeEach(() => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any (getDocViewsRegistry() as any).resetRegistry(); jest.clearAllMocks(); }); @@ -44,6 +47,7 @@ test('Render with 3 different tabs', () => { const registry = getDocViewsRegistry(); registry.addDocView({ order: 10, title: 'Render function', render: jest.fn() }); registry.addDocView({ order: 20, title: 'React component', component: () =>
test
}); + // @ts-expect-error This should be invalid and will throw an error when rendering registry.addDocView({ order: 30, title: 'Invalid doc view' }); const renderProps = { hit: {} } as DocViewRenderProps; diff --git a/src/plugins/discover/public/application/components/doc_viewer/doc_viewer_tab.tsx b/src/plugins/discover/public/application/components/doc_viewer/doc_viewer_tab.tsx index 1ad6500771d48..4ca53d929eeab 100644 --- a/src/plugins/discover/public/application/components/doc_viewer/doc_viewer_tab.tsx +++ b/src/plugins/discover/public/application/components/doc_viewer/doc_viewer_tab.tsx @@ -16,11 +16,11 @@ import { getServices } from '../../../kibana_services'; import { KibanaContextProvider } from '../../../../../kibana_react/public'; interface Props { - component?: React.ComponentType; id: number; - render?: DocViewRenderFn; renderProps: DocViewRenderProps; title: string; + render?: DocViewRenderFn; + component?: React.ComponentType; } interface State { @@ -53,17 +53,11 @@ export class DocViewerTab extends React.Component { } render() { - const { component, render, renderProps, title } = this.props; + const { component: Component, render, renderProps, title } = this.props; const { hasError, error } = this.state; if (hasError && error) { return ; - } else if (!render && !component) { - return ( - - ); } if (render) { @@ -72,14 +66,20 @@ export class DocViewerTab extends React.Component { } // doc view is provided by a react component + if (Component) { + return ( + + + + + + ); + } - const Component = component as any; return ( - - - - - + ); } } diff --git a/src/plugins/discover/public/application/components/json_code_editor/json_code_editor.tsx b/src/plugins/discover/public/application/components/json_code_editor/json_code_editor.tsx index 50a29dde85891..b8427bb6bbdd2 100644 --- a/src/plugins/discover/public/application/components/json_code_editor/json_code_editor.tsx +++ b/src/plugins/discover/public/application/components/json_code_editor/json_code_editor.tsx @@ -22,7 +22,7 @@ const copyToClipboardLabel = i18n.translate('discover.json.copyToClipboardLabel' }); interface JsonCodeEditorProps { - json: Record; + json: Record; width?: string | number; hasLineNumbers?: boolean; } diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx index 54a2de14e2e26..2041a8dfdc637 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { findTestSubject } from '@elastic/eui/lib/test'; -// @ts-ignore +// @ts-expect-error import stubbedLogstashFields from '../../../__fixtures__/logstash_fields'; import { mountWithIntl } from '@kbn/test/jest'; import { DiscoverField } from './discover_field'; @@ -49,7 +49,7 @@ function getComponent({ }) { const indexPattern = getStubIndexPattern( 'logstash-*', - (cfg: any) => cfg, + (cfg: unknown) => cfg, 'time', stubbedLogstashFields(), coreMock.createSetup() diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_details.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field_details.test.tsx index 0113213f70c88..f82154af33d1e 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field_details.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field_details.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { findTestSubject } from '@elastic/eui/lib/test'; -// @ts-ignore +// @ts-expect-error import stubbedLogstashFields from '../../../__fixtures__/logstash_fields'; import { mountWithIntl } from '@kbn/test/jest'; import { DiscoverFieldDetails } from './discover_field_details'; @@ -18,7 +18,7 @@ import { getStubIndexPattern } from '../../../../../data/public/test_utils'; const indexPattern = getStubIndexPattern( 'logstash-*', - (cfg: any) => cfg, + (cfg: unknown) => cfg, 'time', stubbedLogstashFields(), coreMock.createSetup() diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_details_footer.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field_details_footer.test.tsx index 07baeddf034ef..73e906cb62f5f 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field_details_footer.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field_details_footer.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { findTestSubject } from '@elastic/eui/lib/test'; -// @ts-ignore +// @ts-expect-error import stubbedLogstashFields from '../../../__fixtures__/logstash_fields'; import { mountWithIntl } from '@kbn/test/jest'; import { coreMock } from '../../../../../../core/public/mocks'; @@ -18,7 +18,7 @@ import { DiscoverFieldDetailsFooter } from './discover_field_details_footer'; const indexPattern = getStubIndexPattern( 'logstash-*', - (cfg: any) => cfg, + (cfg: unknown) => cfg, 'time', stubbedLogstashFields(), coreMock.createSetup() diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_search.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field_search.test.tsx index ec680b062c9ba..145053de1f21c 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field_search.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field_search.test.tsx @@ -47,7 +47,7 @@ describe('DiscoverFieldSearch', () => { const aggregatableButtonGroup = findButtonGroup(component, 'aggregatable'); act(() => { - // @ts-ignore + // @ts-expect-error (aggregatableButtonGroup.props() as EuiButtonGroupProps).onChange('aggregatable-true', null); }); component.update(); @@ -66,7 +66,7 @@ describe('DiscoverFieldSearch', () => { // change value of aggregatable select const aggregatableButtonGroup = findButtonGroup(component, 'aggregatable'); act(() => { - // @ts-ignore + // @ts-expect-error (aggregatableButtonGroup.props() as EuiButtonGroupProps).onChange('aggregatable-true', null); }); component.update(); @@ -74,14 +74,14 @@ describe('DiscoverFieldSearch', () => { // change value of searchable select const searchableButtonGroup = findButtonGroup(component, 'searchable'); act(() => { - // @ts-ignore + // @ts-expect-error (searchableButtonGroup.props() as EuiButtonGroupProps).onChange('searchable-true', null); }); component.update(); expect(badge.text()).toEqual('2'); // change value of searchable select act(() => { - // @ts-ignore + // @ts-expect-error (searchableButtonGroup.props() as EuiButtonGroupProps).onChange('searchable-any', null); }); component.update(); @@ -114,7 +114,7 @@ describe('DiscoverFieldSearch', () => { const aggregtableButtonGroup = findButtonGroup(component, 'aggregatable'); const missingSwitch = findTestSubject(component, 'missingSwitch'); act(() => { - // @ts-ignore + // @ts-expect-error (aggregtableButtonGroup.props() as EuiButtonGroupProps).onChange('aggregatable-true', null); }); missingSwitch.simulate('click'); diff --git a/src/plugins/discover/public/application/components/sidebar/discover_index_pattern.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_index_pattern.test.tsx index 73de3b14f88f6..f6d577de564ad 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_index_pattern.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_index_pattern.test.tsx @@ -14,7 +14,7 @@ import { ChangeIndexPattern } from './change_indexpattern'; import { SavedObject } from 'kibana/server'; import { DiscoverIndexPattern, DiscoverIndexPatternProps } from './discover_index_pattern'; import { EuiSelectable } from '@elastic/eui'; -import { IndexPattern } from 'src/plugins/data/public'; +import { IndexPattern, IndexPatternAttributes } from 'src/plugins/data/public'; import { configMock } from '../../../__mocks__/config'; import { indexPatternsMock } from '../../../__mocks__/index_patterns'; @@ -28,14 +28,14 @@ const indexPattern1 = { attributes: { title: 'test1 title', }, -} as SavedObject; +} as SavedObject; const indexPattern2 = { id: 'the-index-pattern-id', attributes: { title: 'test2 title', }, -} as SavedObject; +} as SavedObject; const defaultProps = { config: configMock, @@ -58,6 +58,7 @@ function getIndexPatternPickerOptions(instance: ShallowWrapper) { function selectIndexPatternPickerOption(instance: ShallowWrapper, selectedLabel: string) { const options: Array<{ label: string; checked?: 'on' | 'off' }> = getIndexPatternPickerOptions( instance + // eslint-disable-next-line @typescript-eslint/no-explicit-any ).map((option: any) => option.label === selectedLabel ? { ...option, checked: 'on' } @@ -79,6 +80,7 @@ describe('DiscoverIndexPattern', () => { test('should list all index patterns', () => { const instance = shallow(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any expect(getIndexPatternPickerOptions(instance)!.map((option: any) => option.label)).toEqual([ 'test1 title', 'test2 title', diff --git a/src/plugins/discover/public/application/components/sidebar/discover_index_pattern_management.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_index_pattern_management.test.tsx index 5a954270fdf58..6f9ff63d69782 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_index_pattern_management.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_index_pattern_management.test.tsx @@ -9,7 +9,7 @@ import { getStubIndexPattern } from '../../../../../data/public/index_patterns/index_pattern.stub'; import { coreMock } from '../../../../../../core/public/mocks'; import { DiscoverServices } from '../../../build_services'; -// @ts-ignore +// @ts-expect-error import stubbedLogstashFields from '../../../__fixtures__/logstash_fields'; import { mountWithIntl } from '@kbn/test/jest'; import React from 'react'; @@ -56,7 +56,7 @@ const mockServices = ({ describe('Discover IndexPattern Management', () => { const indexPattern = getStubIndexPattern( 'logstash-*', - (cfg: any) => cfg, + (cfg: unknown) => cfg, 'time', stubbedLogstashFields(), coreMock.createSetup() diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx index 01541344be7e1..35d93095a1da3 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx @@ -9,9 +9,9 @@ import { each, cloneDeep } from 'lodash'; import { ReactWrapper } from 'enzyme'; import { findTestSubject } from '@elastic/eui/lib/test'; -// @ts-ignore +// @ts-expect-error import realHits from '../../../__fixtures__/real_hits.js'; -// @ts-ignore +// @ts-expect-error import stubbedLogstashFields from '../../../__fixtures__/logstash_fields'; import { mountWithIntl } from '@kbn/test/jest'; import React from 'react'; @@ -67,7 +67,7 @@ jest.mock('./lib/get_index_pattern_field_list', () => ({ function getCompProps(): DiscoverSidebarProps { const indexPattern = getStubIndexPattern( 'logstash-*', - (cfg: any) => cfg, + (cfg: unknown) => cfg, 'time', stubbedLogstashFields(), coreMock.createSetup() diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.test.tsx index caec61cc501b9..caa0e436f4091 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.test.tsx @@ -9,9 +9,9 @@ import { each, cloneDeep } from 'lodash'; import { ReactWrapper } from 'enzyme'; import { findTestSubject } from '@elastic/eui/lib/test'; -// @ts-ignore +// @ts-expect-error import realHits from '../../../__fixtures__/real_hits.js'; -// @ts-ignore +// @ts-expect-error import stubbedLogstashFields from '../../../__fixtures__/logstash_fields'; import { mountWithIntl } from '@kbn/test/jest'; import React from 'react'; @@ -63,7 +63,7 @@ jest.mock('./lib/get_index_pattern_field_list', () => ({ function getCompProps(): DiscoverSidebarResponsiveProps { const indexPattern = getStubIndexPattern( 'logstash-*', - (cfg: any) => cfg, + (cfg: unknown) => cfg, 'time', stubbedLogstashFields(), coreMock.createSetup() diff --git a/src/plugins/discover/public/application/components/sidebar/lib/field_calculator.test.ts b/src/plugins/discover/public/application/components/sidebar/lib/field_calculator.test.ts index faa31dde1bb80..2cdd99774c2a8 100644 --- a/src/plugins/discover/public/application/components/sidebar/lib/field_calculator.test.ts +++ b/src/plugins/discover/public/application/components/sidebar/lib/field_calculator.test.ts @@ -6,15 +6,17 @@ * Side Public License, v 1. */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + import _ from 'lodash'; -// @ts-ignore +// @ts-expect-error import realHits from '../../../../__fixtures__/real_hits.js'; -// @ts-ignore +// @ts-expect-error import stubbedLogstashFields from '../../../../__fixtures__/logstash_fields'; import { coreMock } from '../../../../../../../core/public/mocks'; import { IndexPattern } from '../../../../../../data/public'; import { getStubIndexPattern } from '../../../../../../data/public/test_utils'; -// @ts-ignore +// @ts-expect-error import { fieldCalculator } from './field_calculator'; let indexPattern: IndexPattern; @@ -23,7 +25,7 @@ describe('fieldCalculator', function () { beforeEach(function () { indexPattern = getStubIndexPattern( 'logstash-*', - (cfg: any) => cfg, + (cfg: unknown) => cfg, 'time', stubbedLogstashFields(), coreMock.createSetup() diff --git a/src/plugins/discover/public/application/components/sidebar/lib/get_details.ts b/src/plugins/discover/public/application/components/sidebar/lib/get_details.ts index 2cea3270e5166..1e35717d249f8 100644 --- a/src/plugins/discover/public/application/components/sidebar/lib/get_details.ts +++ b/src/plugins/discover/public/application/components/sidebar/lib/get_details.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -// @ts-ignore +// @ts-expect-error import { fieldCalculator } from './field_calculator'; import { IndexPattern, IndexPatternField } from '../../../../../../data/public'; import { ElasticSearchHit } from '../../../doc_views/doc_views_types'; diff --git a/src/plugins/discover/public/application/components/sidebar/lib/group_fields.test.ts b/src/plugins/discover/public/application/components/sidebar/lib/group_fields.test.ts index 68099fb0c8e2a..e8eb07784cf9f 100644 --- a/src/plugins/discover/public/application/components/sidebar/lib/group_fields.test.ts +++ b/src/plugins/discover/public/application/components/sidebar/lib/group_fields.test.ts @@ -167,18 +167,11 @@ describe('group_fields', function () { aggregatable: true, readFromDocValues: false, }; - const fieldsToGroup = [category, currency, currencyKeyword]; + const fieldsToGroup = [category, currency, currencyKeyword] as IndexPatternField[]; const fieldFilterState = getDefaultFieldFilter(); - const actual = groupFields( - fieldsToGroup as any, - ['currency'], - 5, - fieldCounts, - fieldFilterState, - true - ); + const actual = groupFields(fieldsToGroup, ['currency'], 5, fieldCounts, fieldFilterState, true); expect(actual.popular).toEqual([category]); expect(actual.selected).toEqual([currency]); diff --git a/src/plugins/discover/public/application/components/table/table.test.tsx b/src/plugins/discover/public/application/components/table/table.test.tsx index 7539f29c1ec9d..e42288d52be8e 100644 --- a/src/plugins/discover/public/application/components/table/table.test.tsx +++ b/src/plugins/discover/public/application/components/table/table.test.tsx @@ -11,6 +11,7 @@ import { mount } from 'enzyme'; import { findTestSubject } from '@elastic/eui/lib/test'; import { DocViewTable } from './table'; import { indexPatterns, IndexPattern } from '../../../../../data/public'; +import { ElasticSearchHit } from '../../doc_views/doc_views_types'; const indexPattern = ({ fields: { @@ -87,7 +88,7 @@ describe('DocViewTable at Discover', () => { scripted: 123, _underscore: 123, }, - } as any; + } as ElasticSearchHit; const props = { hit, @@ -185,7 +186,7 @@ describe('DocViewTable at Discover Context', () => { Integer tincidunt. Cras dapibus. Vivamus elementum semper nisi. Aenean vulputate eleifend tellus. \ Phasellus ullamcorper ipsum rutrum nunc. Nunc nonummy metus. Vestibulum volutpat pretium libero. Cras id dui. Aenean ut', }, - } as any; + } as ElasticSearchHit; const props = { hit, columns: ['extension'], @@ -312,7 +313,7 @@ describe('DocViewTable at Discover Doc with Fields API', () => { }, metaFields: ['_index', '_type', '_score', '_id'], flattenHit: jest.fn((hit) => { - const result = {} as Record; + const result = {} as Record; Object.keys(hit).forEach((key) => { if (key !== 'fields') { result[key] = hit[key]; @@ -325,7 +326,7 @@ describe('DocViewTable at Discover Doc with Fields API', () => { return result; }), formatHit: jest.fn((hit) => { - const result = {} as Record; + const result = {} as Record; Object.keys(hit).forEach((key) => { if (key !== 'fields') { result[key] = hit[key]; @@ -347,7 +348,7 @@ describe('DocViewTable at Discover Doc with Fields API', () => { _index: 'logstash-2014.09.09', _type: 'doc', _id: 'id123', - _score: null, + _score: 1.0, fields: { category: "Women's Clothing", 'category.keyword': "Women's Clothing", @@ -364,7 +365,6 @@ describe('DocViewTable at Discover Doc with Fields API', () => { onAddColumn: jest.fn(), onRemoveColumn: jest.fn(), }; - // @ts-ignore const component = mount(); it('renders multifield rows', () => { const categoryMultifieldRow = findTestSubject( diff --git a/src/plugins/discover/public/application/components/timechart_header/timechart_header.test.tsx b/src/plugins/discover/public/application/components/timechart_header/timechart_header.test.tsx index 74836711373b2..0da1fdd92d2cc 100644 --- a/src/plugins/discover/public/application/components/timechart_header/timechart_header.test.tsx +++ b/src/plugins/discover/public/application/components/timechart_header/timechart_header.test.tsx @@ -79,10 +79,10 @@ describe('timechart header', function () { component = mountWithIntl(); const dropdown = findTestSubject(component, 'discoverIntervalSelect'); expect(dropdown.length).toBe(1); - // @ts-ignore + // @ts-expect-error const values = dropdown.find('option').map((option) => option.prop('value')); expect(values).toEqual(['auto', 'ms', 's']); - // @ts-ignore + // @ts-expect-error const labels = dropdown.find('option').map((option) => option.text()); expect(labels).toEqual(['Auto', 'Millisecond', 'Second']); }); diff --git a/src/plugins/discover/public/application/doc_views/doc_views_helpers.tsx b/src/plugins/discover/public/application/doc_views/doc_views_helpers.tsx index 7acb56ea5535c..a590a9c05eda4 100644 --- a/src/plugins/discover/public/application/doc_views/doc_views_helpers.tsx +++ b/src/plugins/discover/public/application/doc_views/doc_views_helpers.tsx @@ -32,6 +32,7 @@ export async function injectAngularElement( if (typeof Controller === 'function') { // when a controller is defined, expose the value it produces to the view as `$ctrl` // see: https://docs.angularjs.org/api/ng/provider/$compileProvider#component + // eslint-disable-next-line @typescript-eslint/no-explicit-any (newScope as any).$ctrl = $injector.instantiate(Controller, { $scope: newScope, }); diff --git a/src/plugins/discover/public/application/doc_views/doc_views_registry.ts b/src/plugins/discover/public/application/doc_views/doc_views_registry.ts index da36be1b53a78..ad341d07aae5a 100644 --- a/src/plugins/discover/public/application/doc_views/doc_views_registry.ts +++ b/src/plugins/discover/public/application/doc_views/doc_views_registry.ts @@ -25,7 +25,8 @@ export class DocViewsRegistry { const docView = typeof docViewRaw === 'function' ? docViewRaw() : docViewRaw; if (docView.directive) { // convert angular directive to render function for backwards compatibility - docView.render = convertDirectiveToRenderFn(docView.directive, () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (docView.render as any) = convertDirectiveToRenderFn(docView.directive as any, () => { if (!this.angularInjectorGetter) { throw new Error('Angular was not initialized'); } diff --git a/src/plugins/discover/public/application/doc_views/doc_views_types.ts b/src/plugins/discover/public/application/doc_views/doc_views_types.ts index 02ac951f7f57c..58399f31e032f 100644 --- a/src/plugins/discover/public/application/doc_views/doc_views_types.ts +++ b/src/plugins/discover/public/application/doc_views/doc_views_types.ts @@ -12,7 +12,7 @@ import type { estypes } from '@elastic/elasticsearch'; import { IndexPattern } from '../../../../data/public'; export interface AngularDirective { - controller: (...injectedServices: any[]) => void; + controller: (...injectedServices: unknown[]) => void; template: string; } @@ -49,17 +49,34 @@ export type DocViewRenderFn = ( renderProps: DocViewRenderProps ) => () => void; -export interface DocViewInput { - component?: DocViewerComponent; - directive?: AngularDirective; +export interface BaseDocViewInput { order: number; - render?: DocViewRenderFn; shouldShow?: (hit: ElasticSearchHit) => boolean; title: string; } -export interface DocView extends DocViewInput { - shouldShow: (hit: ElasticSearchHit) => boolean; +export interface RenderDocViewInput extends BaseDocViewInput { + render: DocViewRenderFn; + component?: undefined; + directive?: undefined; } +interface ComponentDocViewInput extends BaseDocViewInput { + component: DocViewerComponent; + render?: undefined; + directive?: undefined; +} + +interface DirectiveDocViewInput extends BaseDocViewInput { + component?: undefined; + render?: undefined; + directive: ng.IDirective; +} + +export type DocViewInput = ComponentDocViewInput | RenderDocViewInput | DirectiveDocViewInput; + +export type DocView = DocViewInput & { + shouldShow: NonNullable; +}; + export type DocViewInputFn = () => DocViewInput; diff --git a/src/plugins/discover/public/application/helpers/breadcrumbs.ts b/src/plugins/discover/public/application/helpers/breadcrumbs.ts index 1a190b3882240..8a8d0e7027c65 100644 --- a/src/plugins/discover/public/application/helpers/breadcrumbs.ts +++ b/src/plugins/discover/public/application/helpers/breadcrumbs.ts @@ -21,6 +21,7 @@ export function getRootBreadcrumbs() { ]; } +// eslint-disable-next-line @typescript-eslint/no-explicit-any export function getSavedSearchBreadcrumbs($route: any) { return [ ...getRootBreadcrumbs(), diff --git a/src/plugins/discover/public/application/helpers/calc_field_counts.ts b/src/plugins/discover/public/application/helpers/calc_field_counts.ts index 1f00faaf56c39..edeaa0b9bce52 100644 --- a/src/plugins/discover/public/application/helpers/calc_field_counts.ts +++ b/src/plugins/discover/public/application/helpers/calc_field_counts.ts @@ -14,7 +14,7 @@ import { IndexPattern } from '../../kibana_services'; */ export function calcFieldCounts( counts = {} as Record, - rows: Array>, + rows: Array>, indexPattern: IndexPattern ) { for (const hit of rows) { diff --git a/src/plugins/discover/public/application/helpers/migrate_legacy_query.ts b/src/plugins/discover/public/application/helpers/migrate_legacy_query.ts index fd92dc8259a1f..f87d5328046d8 100644 --- a/src/plugins/discover/public/application/helpers/migrate_legacy_query.ts +++ b/src/plugins/discover/public/application/helpers/migrate_legacy_query.ts @@ -16,7 +16,7 @@ import { Query } from 'src/plugins/data/public'; * @return Object */ -export function migrateLegacyQuery(query: Query | { [key: string]: any } | string): Query { +export function migrateLegacyQuery(query: Query | { [key: string]: unknown } | string): Query { // Lucene was the only option before, so language-less queries are all lucene if (!has(query, 'language')) { return { query, language: 'lucene' }; diff --git a/src/plugins/discover/public/build_services.ts b/src/plugins/discover/public/build_services.ts index cf95d5a85b9f2..19ee06aa51798 100644 --- a/src/plugins/discover/public/build_services.ts +++ b/src/plugins/discover/public/build_services.ts @@ -8,6 +8,7 @@ import { History } from 'history'; +import type { auto } from 'angular'; import { Capabilities, ChromeStart, @@ -57,7 +58,7 @@ export interface DiscoverServices { toastNotifications: ToastsStart; getSavedSearchById: (id: string) => Promise; getSavedSearchUrlById: (id: string) => Promise; - getEmbeddableInjector: any; + getEmbeddableInjector: () => Promise; uiSettings: IUiSettingsClient; trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void; indexPatternFieldEditor: IndexPatternFieldEditorStart; @@ -67,7 +68,7 @@ export async function buildServices( core: CoreStart, plugins: DiscoverStartPlugins, context: PluginInitializerContext, - getEmbeddableInjector: any + getEmbeddableInjector: () => Promise ): Promise { const services = { savedObjectsClient: core.savedObjects.client, diff --git a/src/plugins/discover/public/get_inner_angular.ts b/src/plugins/discover/public/get_inner_angular.ts index d78e53d99f6a2..c36312a87ce9a 100644 --- a/src/plugins/discover/public/get_inner_angular.ts +++ b/src/plugins/discover/public/get_inner_angular.ts @@ -150,7 +150,7 @@ function createLocalStorageModule() { } const createLocalStorageService = function (type: string) { - return function ($window: any) { + return function ($window: ng.IWindowService) { return new Storage($window[type]); }; }; diff --git a/src/plugins/discover/public/kibana_services.ts b/src/plugins/discover/public/kibana_services.ts index e4b0035ed0e03..c2ab4ae34c958 100644 --- a/src/plugins/discover/public/kibana_services.ts +++ b/src/plugins/discover/public/kibana_services.ts @@ -15,21 +15,24 @@ import { createGetterSetter } from '../../kibana_utils/public'; import { search } from '../../data/public'; import { DocViewsRegistry } from './application/doc_views/doc_views_registry'; -let angularModule: any = null; +let angularModule: ng.IModule | null = null; let services: DiscoverServices | null = null; let uiActions: UiActionsStart; /** * set bootstrapped inner angular module */ -export function setAngularModule(module: any) { +export function setAngularModule(module: ng.IModule) { angularModule = module; } /** * get boostrapped inner angular module */ -export function getAngularModule() { +export function getAngularModule(): ng.IModule { + if (!angularModule) { + throw new Error('Discover angular module not yet available'); + } return angularModule; } @@ -40,7 +43,7 @@ export function getServices(): DiscoverServices { return services; } -export function setServices(newServices: any) { +export function setServices(newServices: DiscoverServices) { services = newServices; } diff --git a/src/plugins/discover/public/mocks.ts b/src/plugins/discover/public/mocks.ts index bdad98a457a5c..0f57c5c0fa138 100644 --- a/src/plugins/discover/public/mocks.ts +++ b/src/plugins/discover/public/mocks.ts @@ -22,10 +22,10 @@ const createSetupContract = (): Setup => { const createStartContract = (): Start => { const startContract: Start = { - savedSearchLoader: {} as any, - urlGenerator: { + savedSearchLoader: {} as DiscoverStart['savedSearchLoader'], + urlGenerator: ({ createUrl: jest.fn(), - } as any, + } as unknown) as DiscoverStart['urlGenerator'], }; return startContract; }; diff --git a/src/plugins/discover/public/plugin.tsx b/src/plugins/discover/public/plugin.tsx index 692704c92356e..3f55ab76372bc 100644 --- a/src/plugins/discover/public/plugin.tsx +++ b/src/plugins/discover/public/plugin.tsx @@ -164,7 +164,10 @@ export class DiscoverPlugin public initializeInnerAngular?: () => void; public initializeServices?: () => Promise<{ core: CoreStart; plugins: DiscoverStartPlugins }>; - setup(core: CoreSetup, plugins: DiscoverSetupPlugins) { + setup( + core: CoreSetup, + plugins: DiscoverSetupPlugins + ): DiscoverSetup { const baseUrl = core.http.basePath.prepend('/app/discover'); if (plugins.share) { @@ -190,7 +193,8 @@ export class DiscoverPlugin defaultMessage: 'JSON', }), order: 20, - component: ({ hit }) => , + // eslint-disable-next-line @typescript-eslint/no-explicit-any + component: ({ hit }) => , }); const { diff --git a/src/plugins/discover/public/url_generator.test.ts b/src/plugins/discover/public/url_generator.test.ts index 2d29111fd3757..765e8b36cc1ea 100644 --- a/src/plugins/discover/public/url_generator.test.ts +++ b/src/plugins/discover/public/url_generator.test.ts @@ -31,7 +31,7 @@ const setup = async ({ useHash = false }: SetupParams = {}) => { }; beforeEach(() => { - // @ts-ignore + // @ts-expect-error hashedItemStore.storage = mockStorage; }); diff --git a/src/plugins/discover/server/saved_objects/search.ts b/src/plugins/discover/server/saved_objects/search.ts index b66c06db3e120..070f0253f17e0 100644 --- a/src/plugins/discover/server/saved_objects/search.ts +++ b/src/plugins/discover/server/saved_objects/search.ts @@ -47,5 +47,5 @@ export const searchSavedObjectType: SavedObjectsType = { version: { type: 'integer' }, }, }, - migrations: searchMigrations as any, + migrations: searchMigrations, }; diff --git a/src/plugins/discover/server/saved_objects/search_migrations.ts b/src/plugins/discover/server/saved_objects/search_migrations.ts index feaf91409797a..0c45db5cd779e 100644 --- a/src/plugins/discover/server/saved_objects/search_migrations.ts +++ b/src/plugins/discover/server/saved_objects/search_migrations.ts @@ -6,6 +6,9 @@ * Side Public License, v 1. */ +// TODO: This needs to be removed and properly typed +/* eslint-disable @typescript-eslint/no-explicit-any */ + import { flow, get } from 'lodash'; import { SavedObjectMigrationFn } from 'kibana/server'; import { DEFAULT_QUERY_LANGUAGE } from '../../../data/common'; diff --git a/src/plugins/inspector/public/views/requests/_requests.scss b/src/plugins/inspector/public/views/requests/_requests.scss index 273c9d0ccba2b..ac6414d33684c 100644 --- a/src/plugins/inspector/public/views/requests/_requests.scss +++ b/src/plugins/inspector/public/views/requests/_requests.scss @@ -12,3 +12,7 @@ .insRequestSelector__menuSpinner { margin-left: $euiSizeS; } + +.insRequestCodeViewer .react-monaco-editor-container { + flex-grow: 1; // Ensure the editor takes the full height of its flex container on Safari. +} diff --git a/src/plugins/usage_collection/public/components/index.ts b/src/plugins/usage_collection/public/components/index.ts index 0c2b8f45a1baf..23a28aff05616 100644 --- a/src/plugins/usage_collection/public/components/index.ts +++ b/src/plugins/usage_collection/public/components/index.ts @@ -7,3 +7,4 @@ */ export { TrackApplicationView } from './track_application_view'; +export type { TrackApplicationViewProps } from './track_application_view'; diff --git a/src/plugins/usage_collection/public/components/track_application_view/track_application_view.tsx b/src/plugins/usage_collection/public/components/track_application_view/track_application_view.tsx index 6c14dcae0c512..414ef600e8deb 100644 --- a/src/plugins/usage_collection/public/components/track_application_view/track_application_view.tsx +++ b/src/plugins/usage_collection/public/components/track_application_view/track_application_view.tsx @@ -17,7 +17,7 @@ export const ApplicationUsageContext = createContext = (props) => { diff --git a/src/plugins/usage_collection/public/components/track_application_view/types.ts b/src/plugins/usage_collection/public/components/track_application_view/types.ts index b7783785a53b7..4fd4e1de3c460 100644 --- a/src/plugins/usage_collection/public/components/track_application_view/types.ts +++ b/src/plugins/usage_collection/public/components/track_application_view/types.ts @@ -9,7 +9,7 @@ import { ReactNode } from 'react'; /** - * Props to provide to the {@Link TrackApplicationView} component. + * Props to provide to the {@link TrackApplicationView} component. * @public */ export interface TrackApplicationViewProps { diff --git a/src/plugins/usage_collection/public/index.ts b/src/plugins/usage_collection/public/index.ts index 9b009b1d9e264..d5ac73b7758f5 100644 --- a/src/plugins/usage_collection/public/index.ts +++ b/src/plugins/usage_collection/public/index.ts @@ -6,12 +6,12 @@ * Side Public License, v 1. */ -import { PluginInitializerContext } from '../../../core/public'; +import type { PluginInitializerContext } from 'src/core/public'; import { UsageCollectionPlugin } from './plugin'; -export { METRIC_TYPE } from '@kbn/analytics'; export type { UsageCollectionSetup, UsageCollectionStart } from './plugin'; export { TrackApplicationView } from './components'; +export type { TrackApplicationViewProps } from './components'; export function plugin(initializerContext: PluginInitializerContext) { return new UsageCollectionPlugin(initializerContext); diff --git a/src/plugins/usage_collection/public/plugin.tsx b/src/plugins/usage_collection/public/plugin.tsx index f50919eb8f622..450c8903e1a5e 100644 --- a/src/plugins/usage_collection/public/plugin.tsx +++ b/src/plugins/usage_collection/public/plugin.tsx @@ -7,6 +7,7 @@ */ import { Reporter, ApplicationUsageTracker } from '@kbn/analytics'; +import type { UiCounterMetricType } from '@kbn/analytics'; import type { Subscription } from 'rxjs'; import React from 'react'; import type { @@ -31,15 +32,63 @@ export type IApplicationUsageTracker = Pick< 'trackApplicationViewUsage' | 'flushTrackedView' | 'updateViewClickCounter' >; +/** Public's setup APIs exposed by the UsageCollection Service **/ export interface UsageCollectionSetup { + /** Component helpers to track usage collection in the UI **/ components: { + /** + * The context provider to wrap the application if planning to use + * {@link TrackApplicationView} somewhere inside the app. + * + * @example + * ```typescript jsx + * class MyPlugin implements Plugin { + * ... + * public setup(core: CoreSetup, plugins: { usageCollection?: UsageCollectionSetup }) { + * const ApplicationUsageTrackingProvider = plugins.usageCollection?.components.ApplicationUsageTrackingProvider ?? React.Fragment; + * + * core.application.register({ + * id, + * title, + * ..., + * mount: async (params: AppMountParameters) => { + * ReactDOM.render( + * // Set the tracking context provider at the App level + * + * + * + * , + * element + * ); + * return () => ReactDOM.unmountComponentAtNode(element); + * }, + * }); + * } + * ... + * } + * ``` + */ ApplicationUsageTrackingProvider: React.FC; }; - reportUiCounter: Reporter['reportUiCounter']; + + /** Report whenever a UI event occurs for UI counters to report it **/ + reportUiCounter: ( + appName: string, + type: UiCounterMetricType, + eventNames: string | string[], + count?: number + ) => void; } +/** Public's start APIs exposed by the UsageCollection Service **/ export interface UsageCollectionStart { - reportUiCounter: Reporter['reportUiCounter']; + /** Report whenever a UI event occurs for UI counters to report it **/ + reportUiCounter: ( + appName: string, + type: UiCounterMetricType, + eventNames: string | string[], + count?: number + ) => void; } export function isUnauthenticated(http: HttpSetup) { diff --git a/src/plugins/usage_collection/server/collector/collector.test.ts b/src/plugins/usage_collection/server/collector/collector.test.ts index fa3966f4f2d3c..8c7971e52a4b9 100644 --- a/src/plugins/usage_collection/server/collector/collector.test.ts +++ b/src/plugins/usage_collection/server/collector/collector.test.ts @@ -20,19 +20,6 @@ describe('collector', () => { ); }); - it('should fail if init is not a function', () => { - expect( - () => - new Collector(logger, { - type: 'my_test_collector', - // @ts-expect-error - init: 1, - }) - ).toThrowError( - 'If init property is passed, Collector must be instantiated with a options.init as a function property' - ); - }); - it('should fail if fetch is not defined', () => { expect( () => diff --git a/src/plugins/usage_collection/server/collector/collector.ts b/src/plugins/usage_collection/server/collector/collector.ts index 22c91ac0c038d..21f8229718c87 100644 --- a/src/plugins/usage_collection/server/collector/collector.ts +++ b/src/plugins/usage_collection/server/collector/collector.ts @@ -6,144 +6,18 @@ * Side Public License, v 1. */ +import type { Logger } from 'src/core/server'; import type { - Logger, - ElasticsearchClient, - SavedObjectsClientContract, - KibanaRequest, -} from 'src/core/server'; - -export type AllowedSchemaNumberTypes = - | 'long' - | 'integer' - | 'short' - | 'byte' - | 'double' - | 'float' - | 'date'; -export type AllowedSchemaStringTypes = 'keyword' | 'text' | 'date'; -export type AllowedSchemaBooleanTypes = 'boolean'; - -export type AllowedSchemaTypes = - | AllowedSchemaNumberTypes - | AllowedSchemaStringTypes - | AllowedSchemaBooleanTypes; - -export interface SchemaField { - type: string; -} - -export type PossibleSchemaTypes = U extends string - ? AllowedSchemaStringTypes - : U extends number - ? AllowedSchemaNumberTypes - : U extends boolean - ? AllowedSchemaBooleanTypes - : // allow any schema type from the union if typescript is unable to resolve the exact U type - AllowedSchemaTypes; - -export type RecursiveMakeSchemaFrom = U extends object - ? MakeSchemaFrom - : { type: PossibleSchemaTypes; _meta?: { description: string } }; - -// Using Required to enforce all optional keys in the object -export type MakeSchemaFrom = { - [Key in keyof Required]: Required[Key] extends Array - ? { type: 'array'; items: RecursiveMakeSchemaFrom } - : RecursiveMakeSchemaFrom[Key]>; -}; - -/** - * The context for the `fetch` method: It includes the most commonly used clients in the collectors (ES and SO clients). - * Both are scoped based on the request and the context: - * - When users are requesting a sample of data, it is scoped to their role to avoid exposing data they shouldn't read - * - When building the telemetry data payload to report to the remote cluster, the requests are scoped to the `kibana` internal user - * - * @remark Bear in mind when testing your collector that your user has the same privileges as the Kibana Internal user to ensure the expected data is sent to the remote cluster. - */ -export type CollectorFetchContext = { - /** - * Request-scoped Elasticsearch client - * @remark Bear in mind when testing your collector that your user has the same privileges as the Kibana Internal user to ensure the expected data is sent to the remote cluster (more info: {@link CollectorFetchContext}) - */ - esClient: ElasticsearchClient; - /** - * Request-scoped Saved Objects client - * @remark Bear in mind when testing your collector that your user has the same privileges as the Kibana Internal user to ensure the expected data is sent to the remote cluster (more info: {@link CollectorFetchContext}) - */ - soClient: SavedObjectsClientContract; -} & (WithKibanaRequest extends true - ? { - /** - * The KibanaRequest that can be used to scope the requests: - * It is provided only when your custom clients need to be scoped. If not available, you should use the Internal Client. - * More information about when scoping is needed: {@link CollectorFetchContext} - * @remark You should only use this if you implement your collector to deal with both scenarios: when provided and, especially, when not provided. When telemetry payload is sent to the remote service the `kibanaRequest` will not be provided. - */ - kibanaRequest?: KibanaRequest; - } - : {}); - -export type CollectorFetchMethod< - WithKibanaRequest extends boolean | undefined, - TReturn, - ExtraOptions extends object = {} -> = ( - this: Collector & ExtraOptions, // Specify the context of `this` for this.log and others to become available - context: CollectorFetchContext -) => Promise | TReturn; - -export interface ICollectorOptionsFetchExtendedContext { - /** - * Set to `true` if your `fetch` method requires the `KibanaRequest` object to be added in its context {@link CollectorFetchContextWithRequest}. - * @remark You should fully understand acknowledge that by using the `KibanaRequest` in your collector, you need to ensure it should specially work without it because it won't be provided when building the telemetry payload actually sent to the remote telemetry service. - */ - kibanaRequest?: WithKibanaRequest; -} - -export type CollectorOptionsFetchExtendedContext< - WithKibanaRequest extends boolean -> = ICollectorOptionsFetchExtendedContext & - (WithKibanaRequest extends true // If enforced to true via Types, the config must be expected - ? Required, 'kibanaRequest'>> - : {}); - -export type CollectorOptions< - TFetchReturn = unknown, - WithKibanaRequest extends boolean = boolean, - ExtraOptions extends object = {} -> = { - /** - * Unique string identifier for the collector - */ - type: string; - init?: Function; - /** - * Method to return `true`/`false` or Promise(`true`/`false`) to confirm if the collector is ready for the `fetch` method to be called. - */ - isReady: () => Promise | boolean; - /** - * Schema definition of the output of the `fetch` method. - */ - schema?: MakeSchemaFrom; - /** - * The method that will collect and return the data in the final format. - * @param collectorFetchContext {@link CollectorFetchContext} - */ - fetch: CollectorFetchMethod; -} & ExtraOptions & - (WithKibanaRequest extends true // If enforced to true via Types, the config must be enforced - ? { - extendFetchContext: CollectorOptionsFetchExtendedContext; - } - : { - extendFetchContext?: CollectorOptionsFetchExtendedContext; - }); - -export class Collector { + CollectorFetchMethod, + CollectorOptions, + CollectorOptionsFetchExtendedContext, + ICollector, +} from './types'; + +export class Collector + implements ICollector { public readonly extendFetchContext: CollectorOptionsFetchExtendedContext; public readonly type: CollectorOptions['type']; - public readonly init?: CollectorOptions['init']; public readonly fetch: CollectorFetchMethod; public readonly isReady: CollectorOptions['isReady']; /** @@ -155,7 +29,6 @@ export class Collector { public readonly log: Logger, { type, - init, fetch, isReady, extendFetchContext = {}, @@ -167,11 +40,6 @@ export class Collector { if (type === undefined) { throw new Error('Collector must be instantiated with a options.type string property'); } - if (typeof init !== 'undefined' && typeof init !== 'function') { - throw new Error( - 'If init property is passed, Collector must be instantiated with a options.init as a function property' - ); - } if (typeof fetch !== 'function') { throw new Error('Collector must be instantiated with a options.fetch function property'); } @@ -179,7 +47,6 @@ export class Collector { Object.assign(this, options); // spread in other properties and mutate "this" this.type = type; - this.init = init; this.fetch = fetch; this.isReady = typeof isReady === 'function' ? isReady : () => true; this.extendFetchContext = extendFetchContext; diff --git a/src/plugins/usage_collection/server/collector/collector_set.test.ts b/src/plugins/usage_collection/server/collector/collector_set.test.ts index 5a617e2316dda..aa90d25598518 100644 --- a/src/plugins/usage_collection/server/collector/collector_set.test.ts +++ b/src/plugins/usage_collection/server/collector/collector_set.test.ts @@ -25,10 +25,8 @@ const loggerSpies = { describe('CollectorSet', () => { describe('registers a collector set and runs lifecycle events', () => { - let init: Function; let fetch: Function; beforeEach(() => { - init = noop; fetch = noop; loggerSpies.debug.mockRestore(); loggerSpies.warn.mockRestore(); @@ -42,7 +40,6 @@ describe('CollectorSet', () => { const registerPojo = () => { collectors.registerCollector({ type: 'type_collector_test', - init, // @ts-expect-error we are intentionally sending it wrong. fetch, }); diff --git a/src/plugins/usage_collection/server/collector/collector_set.ts b/src/plugins/usage_collection/server/collector/collector_set.ts index d42eb6644bbbe..d536cf3f2c89b 100644 --- a/src/plugins/usage_collection/server/collector/collector_set.ts +++ b/src/plugins/usage_collection/server/collector/collector_set.ts @@ -13,12 +13,13 @@ import type { SavedObjectsClientContract, KibanaRequest, } from 'src/core/server'; -import { Collector, CollectorOptions } from './collector'; +import { Collector } from './collector'; +import type { ICollector, CollectorOptions } from './types'; import { UsageCollector, UsageCollectorOptions } from './usage_collector'; // Needed for the general array containing all the collectors. We don't really care about their types here // eslint-disable-next-line @typescript-eslint/no-explicit-any -type AnyCollector = Collector; +type AnyCollector = ICollector; interface CollectorSetConfig { logger: Logger; @@ -85,11 +86,6 @@ export class CollectorSet { } this.collectors.set(collector.type, collector); - - if (collector.init) { - this.logger.debug(`Initializing ${collector.type} collector`); - collector.init(); - } }; public getCollectorByType = (type: string) => { diff --git a/src/plugins/usage_collection/server/collector/index.ts b/src/plugins/usage_collection/server/collector/index.ts index 594455f70fdf8..ca240a520ee24 100644 --- a/src/plugins/usage_collection/server/collector/index.ts +++ b/src/plugins/usage_collection/server/collector/index.ts @@ -7,14 +7,17 @@ */ export { CollectorSet } from './collector_set'; -export { Collector } from './collector'; export type { AllowedSchemaTypes, AllowedSchemaNumberTypes, - SchemaField, + AllowedSchemaBooleanTypes, + AllowedSchemaStringTypes, + RecursiveMakeSchemaFrom, MakeSchemaFrom, CollectorOptions, CollectorFetchContext, -} from './collector'; -export { UsageCollector } from './usage_collector'; + CollectorFetchMethod, + CollectorOptionsFetchExtendedContext, + ICollector as Collector, +} from './types'; export type { UsageCollectorOptions } from './usage_collector'; diff --git a/src/plugins/usage_collection/server/collector/types.ts b/src/plugins/usage_collection/server/collector/types.ts new file mode 100644 index 0000000000000..4258d5e4dd2e8 --- /dev/null +++ b/src/plugins/usage_collection/server/collector/types.ts @@ -0,0 +1,197 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { + ElasticsearchClient, + KibanaRequest, + SavedObjectsClientContract, + Logger, +} from 'src/core/server'; + +/** Types matching number values **/ +export type AllowedSchemaNumberTypes = + | 'long' + | 'integer' + | 'short' + | 'byte' + | 'double' + | 'float' + | 'date'; +/** Types matching string values **/ +export type AllowedSchemaStringTypes = 'keyword' | 'text' | 'date'; +/** Types matching boolean values **/ +export type AllowedSchemaBooleanTypes = 'boolean'; + +/** + * Possible type values in the schema + */ +export type AllowedSchemaTypes = + | AllowedSchemaNumberTypes + | AllowedSchemaStringTypes + | AllowedSchemaBooleanTypes; + +/** + * Helper to ensure the declared types match the schema types + */ +export type PossibleSchemaTypes = U extends string + ? AllowedSchemaStringTypes + : U extends number + ? AllowedSchemaNumberTypes + : U extends boolean + ? AllowedSchemaBooleanTypes + : // allow any schema type from the union if typescript is unable to resolve the exact U type + AllowedSchemaTypes; + +/** + * Helper to find out whether to keep recursively looking or if we are on an end value + */ +export type RecursiveMakeSchemaFrom = U extends object + ? MakeSchemaFrom + : { type: PossibleSchemaTypes; _meta?: { description: string } }; + +/** + * The `schema` property in {@link CollectorOptions} must match the output of + * the `fetch` method. This type helps ensure that is correct + */ +export type MakeSchemaFrom = { + // Using Required to enforce all optional keys in the object + [Key in keyof Required]: Required[Key] extends Array + ? { type: 'array'; items: RecursiveMakeSchemaFrom } + : RecursiveMakeSchemaFrom[Key]>; +}; + +/** + * The context for the `fetch` method: It includes the most commonly used clients in the collectors (ES and SO clients). + * Both are scoped based on the request and the context: + * - When users are requesting a sample of data, it is scoped to their role to avoid exposing data they shouldn't read + * - When building the telemetry data payload to report to the remote cluster, the requests are scoped to the `kibana` internal user + * + * @remark Bear in mind when testing your collector that your user has the same privileges as the Kibana Internal user to ensure the expected data is sent to the remote cluster. + */ +export type CollectorFetchContext = { + /** + * Request-scoped Elasticsearch client + * @remark Bear in mind when testing your collector that your user has the same privileges as the Kibana Internal user to ensure the expected data is sent to the remote cluster (more info: {@link CollectorFetchContext}) + */ + esClient: ElasticsearchClient; + /** + * Request-scoped Saved Objects client + * @remark Bear in mind when testing your collector that your user has the same privileges as the Kibana Internal user to ensure the expected data is sent to the remote cluster (more info: {@link CollectorFetchContext}) + */ + soClient: SavedObjectsClientContract; +} & (WithKibanaRequest extends true + ? { + /** + * The KibanaRequest that can be used to scope the requests: + * It is provided only when your custom clients need to be scoped. If not available, you should use the Internal Client. + * More information about when scoping is needed: {@link CollectorFetchContext} + * @remark You should only use this if you implement your collector to deal with both scenarios: when provided and, especially, when not provided. When telemetry payload is sent to the remote service the `kibanaRequest` will not be provided. + */ + kibanaRequest?: KibanaRequest; + } + : {}); + +/** + * The fetch method has the context of the Collector itself + * (this has access to all the properties of the collector like the logger) + * and the the first parameter is {@link CollectorFetchContext}. + */ +export type CollectorFetchMethod< + WithKibanaRequest extends boolean | undefined, + TReturn, + ExtraOptions extends object = {} +> = ( + this: ICollector & ExtraOptions, // Specify the context of `this` for this.log and others to become available + context: CollectorFetchContext +) => Promise | TReturn; + +export interface ICollectorOptionsFetchExtendedContext { + /** + * Set to `true` if your `fetch` method requires the `KibanaRequest` object to be added in its context {@link CollectorFetchContextWithRequest}. + * @remark You should fully acknowledge that by using the `KibanaRequest` in your collector, you need to ensure it should specially work without it because it won't be provided when building the telemetry payload actually sent to the remote telemetry service. + */ + kibanaRequest?: WithKibanaRequest; +} + +/** + * The options to extend the context provided to the `fetch` method. + * @remark Only to be used in very rare scenarios when this is really needed. + */ +export type CollectorOptionsFetchExtendedContext< + WithKibanaRequest extends boolean +> = ICollectorOptionsFetchExtendedContext & + (WithKibanaRequest extends true // If enforced to true via Types, the config must be expected + ? Required, 'kibanaRequest'>> + : {}); + +/** + * Options to instantiate a collector + */ +export type CollectorOptions< + TFetchReturn = unknown, + WithKibanaRequest extends boolean = boolean, + ExtraOptions extends object = {} +> = { + /** + * Unique string identifier for the collector + */ + type: string; + /** + * Method to return `true`/`false` or Promise(`true`/`false`) to confirm if the collector is ready for the `fetch` method to be called. + */ + isReady: () => Promise | boolean; + /** + * Schema definition of the output of the `fetch` method. + */ + schema?: MakeSchemaFrom; + /** + * The method that will collect and return the data in the final format. + * @param collectorFetchContext {@link CollectorFetchContext} + */ + fetch: CollectorFetchMethod; +} & ExtraOptions & + (WithKibanaRequest extends true // If enforced to true via Types, the config must be enforced + ? { + /** {@link CollectorOptionsFetchExtendedContext} **/ + extendFetchContext: CollectorOptionsFetchExtendedContext; + } + : { + /** {@link CollectorOptionsFetchExtendedContext} **/ + extendFetchContext?: CollectorOptionsFetchExtendedContext; + }); + +/** + * Common interface for Usage and Stats Collectors + */ +export interface ICollector { + /** Logger **/ + readonly log: Logger; + /** + * The options to extend the context provided to the `fetch` method: {@link CollectorOptionsFetchExtendedContext}. + * @remark Only to be used in very rare scenarios when this is really needed. + */ + readonly extendFetchContext: CollectorOptionsFetchExtendedContext; + /** The registered type (aka name) of the collector **/ + readonly type: CollectorOptions['type']; + /** + * The actual logic that reports the Usage collection. + * It will be called on every collection request. + * Whatever is returned in this method will be passed through as-is under + * the {@link ICollector.type} key. + * + * @example + * { + * [type]: await fetch(context) + * } + */ + readonly fetch: CollectorFetchMethod; + /** + * Should return `true` when it's safe to call the `fetch` method. + */ + readonly isReady: CollectorOptions['isReady']; +} diff --git a/src/plugins/usage_collection/server/collector/usage_collector.ts b/src/plugins/usage_collection/server/collector/usage_collector.ts index 3af3a7bb65f84..15f7cd9c627fc 100644 --- a/src/plugins/usage_collection/server/collector/usage_collector.ts +++ b/src/plugins/usage_collection/server/collector/usage_collector.ts @@ -6,10 +6,13 @@ * Side Public License, v 1. */ -import { Logger } from 'src/core/server'; -import { Collector, CollectorOptions } from './collector'; +import type { Logger } from 'src/core/server'; +import type { CollectorOptions } from './types'; +import { Collector } from './collector'; -// Enforce the `schema` property for UsageCollectors +/** + * Same as {@link CollectorOptions} but with the `schema` property enforced + */ export type UsageCollectorOptions< TFetchReturn = unknown, WithKibanaRequest extends boolean = false, @@ -17,6 +20,9 @@ export type UsageCollectorOptions< > = CollectorOptions & Required, 'schema'>>; +/** + * @private Only used in fixtures as a type + */ export class UsageCollector extends Collector< TFetchReturn, ExtraOptions diff --git a/src/plugins/usage_collection/server/index.ts b/src/plugins/usage_collection/server/index.ts index b5441a8b7b34d..74fa77be9843c 100644 --- a/src/plugins/usage_collection/server/index.ts +++ b/src/plugins/usage_collection/server/index.ts @@ -9,28 +9,27 @@ import { PluginInitializerContext } from 'src/core/server'; import { UsageCollectionPlugin } from './plugin'; -export { Collector } from './collector'; export type { + Collector, AllowedSchemaTypes, MakeSchemaFrom, - SchemaField, CollectorOptions, UsageCollectorOptions, CollectorFetchContext, + CollectorFetchMethod, + CollectorOptionsFetchExtendedContext, } from './collector'; export type { UsageCountersSavedObject, UsageCountersSavedObjectAttributes, IncrementCounterParams, -} from './usage_counters'; - -export { - USAGE_COUNTERS_SAVED_OBJECT_TYPE, - serializeCounterKey, UsageCounter, + SerializeCounterParams, } from './usage_counters'; +export { USAGE_COUNTERS_SAVED_OBJECT_TYPE, serializeCounterKey } from './usage_counters'; + export type { UsageCollectionSetup } from './plugin'; export { config } from './config'; export const plugin = (initializerContext: PluginInitializerContext) => diff --git a/src/plugins/usage_collection/server/mocks.ts b/src/plugins/usage_collection/server/mocks.ts index b84fa0f0aab70..ab7e53a7ad69b 100644 --- a/src/plugins/usage_collection/server/mocks.ts +++ b/src/plugins/usage_collection/server/mocks.ts @@ -13,7 +13,8 @@ import { savedObjectsClientMock, } from '../../../../src/core/server/mocks'; -import { CollectorOptions, Collector, CollectorSet } from './collector'; +import { CollectorOptions, CollectorSet } from './collector'; +import { Collector } from './collector/collector'; import { UsageCollectionSetup, CollectorFetchContext } from './index'; export type { CollectorOptions }; diff --git a/src/plugins/usage_collection/server/plugin.ts b/src/plugins/usage_collection/server/plugin.ts index 37d7327aed662..1c537ccfbb22b 100644 --- a/src/plugins/usage_collection/server/plugin.ts +++ b/src/plugins/usage_collection/server/plugin.ts @@ -6,54 +6,100 @@ * Side Public License, v 1. */ -import { +import type { PluginInitializerContext, Logger, CoreSetup, CoreStart, ISavedObjectsRepository, Plugin, + ElasticsearchClient, + SavedObjectsClientContract, + KibanaRequest, } from 'src/core/server'; -import { ConfigType } from './config'; +import type { ConfigType } from './config'; import { CollectorSet } from './collector'; +import type { Collector, CollectorOptions, UsageCollectorOptions } from './collector'; import { setupRoutes } from './routes'; import { UsageCountersService } from './usage_counters'; -import type { UsageCountersServiceSetup } from './usage_counters'; +import type { UsageCounter } from './usage_counters'; +/** Server's setup APIs exposed by the UsageCollection Service **/ export interface UsageCollectionSetup { /** * Creates and registers a usage counter to collect daily aggregated plugin counter events */ - createUsageCounter: UsageCountersServiceSetup['createUsageCounter']; + createUsageCounter: (type: string) => UsageCounter; /** * Returns a usage counter by type */ - getUsageCounterByType: UsageCountersServiceSetup['getUsageCounterByType']; + getUsageCounterByType: (type: string) => UsageCounter | undefined; /** * Creates a usage collector to collect plugin telemetry data. - * registerCollector must be called to connect the created collecter with the service. + * registerCollector must be called to connect the created collector with the service. */ - makeUsageCollector: CollectorSet['makeUsageCollector']; + makeUsageCollector: < + TFetchReturn, + WithKibanaRequest extends boolean = false, + ExtraOptions extends object = {} + >( + options: UsageCollectorOptions + ) => Collector; /** * Register a usage collector or a stats collector. * Used to connect the created collector to telemetry. */ - registerCollector: CollectorSet['registerCollector']; + registerCollector: ( + collector: Collector + ) => void; /** * Returns a usage collector by type */ - getCollectorByType: CollectorSet['getCollectorByType']; - /* internal: telemetry use */ - areAllCollectorsReady: CollectorSet['areAllCollectorsReady']; - /* internal: telemetry use */ - bulkFetch: CollectorSet['bulkFetch']; - /* internal: telemetry use */ - toObject: CollectorSet['toObject']; - /* internal: monitoring use */ - toApiFieldNames: CollectorSet['toApiFieldNames']; - /* internal: telemtery and monitoring use */ - makeStatsCollector: CollectorSet['makeStatsCollector']; + getCollectorByType: ( + type: string + ) => Collector | undefined; + /** + * Returns if all the collectors are ready to fetch their reported usage. + * @internal: telemetry use + */ + areAllCollectorsReady: () => Promise; + /** + * Fetches the collection from all the registered collectors + * @internal: telemetry use + */ + bulkFetch: ( + esClient: ElasticsearchClient, + soClient: SavedObjectsClientContract, + kibanaRequest: KibanaRequest | undefined, // intentionally `| undefined` to enforce providing the parameter + collectors?: Map> + ) => Promise>; + /** + * Converts an array of fetched stats results into key/object + * @internal: telemetry use + */ + toObject: , T = unknown>( + statsData?: Array<{ type: string; result: T }> + ) => Result; + /** + * Rename fields to use API conventions + * @internal: monitoring use + */ + toApiFieldNames: ( + apiData: Record | unknown[] + ) => Record | unknown[]; + /** + * Creates a stats collector to collect plugin telemetry data. + * registerCollector must be called to connect the created collector with the service. + * @internal: telemetry and monitoring use + */ + makeStatsCollector: < + TFetchReturn, + WithKibanaRequest extends boolean, + ExtraOptions extends object = {} + >( + options: CollectorOptions + ) => Collector; } export class UsageCollectionPlugin implements Plugin { diff --git a/src/plugins/usage_collection/server/usage_counters/index.ts b/src/plugins/usage_collection/server/usage_counters/index.ts index dc1d1f5b43edf..cc1ee77ef3ea4 100644 --- a/src/plugins/usage_collection/server/usage_counters/index.ts +++ b/src/plugins/usage_collection/server/usage_counters/index.ts @@ -8,8 +8,8 @@ export type { UsageCountersServiceSetup } from './usage_counters_service'; export type { UsageCountersSavedObjectAttributes, UsageCountersSavedObject } from './saved_objects'; -export type { IncrementCounterParams } from './usage_counter'; +export type { IUsageCounter as UsageCounter, IncrementCounterParams } from './usage_counter'; export { UsageCountersService } from './usage_counters_service'; -export { UsageCounter } from './usage_counter'; +export type { SerializeCounterParams } from './saved_objects'; export { USAGE_COUNTERS_SAVED_OBJECT_TYPE, serializeCounterKey } from './saved_objects'; diff --git a/src/plugins/usage_collection/server/usage_counters/saved_objects.ts b/src/plugins/usage_collection/server/usage_counters/saved_objects.ts index 6c585d756e8c1..34de2adac8086 100644 --- a/src/plugins/usage_collection/server/usage_counters/saved_objects.ts +++ b/src/plugins/usage_collection/server/usage_counters/saved_objects.ts @@ -6,24 +6,35 @@ * Side Public License, v 1. */ -import { +import type { SavedObject, SavedObjectsRepository, SavedObjectAttributes, SavedObjectsServiceSetup, } from 'kibana/server'; import moment from 'moment'; -import { CounterMetric } from './usage_counter'; +import type { CounterMetric } from './usage_counter'; +/** + * The attributes stored in the UsageCounters' SavedObjects + */ export interface UsageCountersSavedObjectAttributes extends SavedObjectAttributes { + /** The domain ID registered in the Usage Counter **/ domainId: string; + /** The counter name **/ counterName: string; + /** The counter type **/ counterType: string; + /** Number of times the event has occurred **/ count: number; } +/** + * The structure of the SavedObjects of type "usage-counters" + */ export type UsageCountersSavedObject = SavedObject; +/** The Saved Objects type for Usage Counters **/ export const USAGE_COUNTERS_SAVED_OBJECT_TYPE = 'usage-counters'; export const registerUsageCountersSavedObjectType = ( @@ -42,13 +53,26 @@ export const registerUsageCountersSavedObjectType = ( }); }; +/** + * Parameters to the `serializeCounterKey` method + * @internal used in kibana_usage_collectors + */ export interface SerializeCounterParams { + /** The domain ID registered in the UsageCounter **/ domainId: string; + /** The counter name **/ counterName: string; + /** The counter type **/ counterType: string; + /** The date to which serialize the key **/ date: moment.MomentInput; } +/** + * Generates a key based on the UsageCounter details + * @internal used in kibana_usage_collectors + * @param opts {@link SerializeCounterParams} + */ export const serializeCounterKey = ({ domainId, counterName, diff --git a/src/plugins/usage_collection/server/usage_counters/usage_counter.ts b/src/plugins/usage_collection/server/usage_counters/usage_counter.ts index af00ad04149b7..b8057fdda8eb6 100644 --- a/src/plugins/usage_collection/server/usage_counters/usage_counter.ts +++ b/src/plugins/usage_collection/server/usage_counters/usage_counter.ts @@ -20,13 +20,32 @@ export interface UsageCounterDeps { counter$: Rx.Subject; } +/** + * Details about the counter to be incremented + */ export interface IncrementCounterParams { + /** The name of the counter **/ counterName: string; + /** The counter type ("count" by default) **/ counterType?: string; + /** Increment the counter by this number (1 if not specified) **/ incrementBy?: number; } -export class UsageCounter { +/** + * Usage Counter allows to keep track of any events that occur. + * By calling {@link IUsageCounter.incrementCounter} devs can notify this + * API whenever the event happens. + */ +export interface IUsageCounter { + /** + * Notifies the counter about a new event happening so it can increase the count internally. + * @param params {@link IncrementCounterParams} + */ + incrementCounter: (params: IncrementCounterParams) => void; +} + +export class UsageCounter implements IUsageCounter { private domainId: string; private counter$: Rx.Subject; diff --git a/test/functional/fixtures/es_archiver/saved_objects_management/edit_saved_object/mappings.json b/test/functional/fixtures/es_archiver/saved_objects_management/edit_saved_object/mappings.json index 96e6b7c0a19f1..6a416126d7f26 100644 --- a/test/functional/fixtures/es_archiver/saved_objects_management/edit_saved_object/mappings.json +++ b/test/functional/fixtures/es_archiver/saved_objects_management/edit_saved_object/mappings.json @@ -162,8 +162,8 @@ "map": { "properties": { "bounds": { - "type": "geo_shape", - "tree": "quadtree" + "dynamic": false, + "properties": {} }, "description": { "type": "text" @@ -456,4 +456,4 @@ } } } -} \ No newline at end of file +} diff --git a/test/functional/fixtures/es_archiver/saved_objects_management/export_transform/mappings.json b/test/functional/fixtures/es_archiver/saved_objects_management/export_transform/mappings.json index d85125efd672a..43b851e817fa8 100644 --- a/test/functional/fixtures/es_archiver/saved_objects_management/export_transform/mappings.json +++ b/test/functional/fixtures/es_archiver/saved_objects_management/export_transform/mappings.json @@ -188,8 +188,8 @@ "map": { "properties": { "bounds": { - "type": "geo_shape", - "tree": "quadtree" + "dynamic": false, + "properties": {} }, "description": { "type": "text" diff --git a/test/functional/fixtures/es_archiver/saved_objects_management/hidden_saved_objects/mappings.json b/test/functional/fixtures/es_archiver/saved_objects_management/hidden_saved_objects/mappings.json index 00d349a27795d..1de768d290d35 100644 --- a/test/functional/fixtures/es_archiver/saved_objects_management/hidden_saved_objects/mappings.json +++ b/test/functional/fixtures/es_archiver/saved_objects_management/hidden_saved_objects/mappings.json @@ -188,8 +188,8 @@ "map": { "properties": { "bounds": { - "type": "geo_shape", - "tree": "quadtree" + "dynamic": false, + "properties": {} }, "description": { "type": "text" diff --git a/test/functional/fixtures/es_archiver/saved_objects_management/hidden_types/mappings.json b/test/functional/fixtures/es_archiver/saved_objects_management/hidden_types/mappings.json index a862731c13f7a..a9abae009cb59 100644 --- a/test/functional/fixtures/es_archiver/saved_objects_management/hidden_types/mappings.json +++ b/test/functional/fixtures/es_archiver/saved_objects_management/hidden_types/mappings.json @@ -193,8 +193,8 @@ "map": { "properties": { "bounds": { - "type": "geo_shape", - "tree": "quadtree" + "dynamic": false, + "properties": {} }, "description": { "type": "text" diff --git a/test/functional/fixtures/es_archiver/saved_objects_management/show_relationships/mappings.json b/test/functional/fixtures/es_archiver/saved_objects_management/show_relationships/mappings.json index d53e6c96e883e..a5478a5805d50 100644 --- a/test/functional/fixtures/es_archiver/saved_objects_management/show_relationships/mappings.json +++ b/test/functional/fixtures/es_archiver/saved_objects_management/show_relationships/mappings.json @@ -176,8 +176,8 @@ "map": { "properties": { "bounds": { - "type": "geo_shape", - "tree": "quadtree" + "dynamic": false, + "properties": {} }, "description": { "type": "text" diff --git a/vars/workers.groovy b/vars/workers.groovy index acd3fa51a3fe7..e0c5ddb358d09 100644 --- a/vars/workers.groovy +++ b/vars/workers.groovy @@ -79,10 +79,12 @@ def base(Map params, Closure closure) { dir("kibana") { checkoutInfo = getCheckoutInfo() - // use `checkoutInfo` as a flag to indicate that we've already reported the pending commit status - if (buildState.get('shouldSetCommitStatus') && !buildState.has('checkoutInfo')) { + if (!buildState.has('checkoutInfo')) { buildState.set('checkoutInfo', checkoutInfo) - githubCommitStatus.onStart() + + if (buildState.get('shouldSetCommitStatus')) { + githubCommitStatus.onStart() + } } } diff --git a/x-pack/plugins/actions/server/create_execute_function.test.ts b/x-pack/plugins/actions/server/create_execute_function.test.ts index d4100537fa6b8..4cacba6dc880a 100644 --- a/x-pack/plugins/actions/server/create_execute_function.test.ts +++ b/x-pack/plugins/actions/server/create_execute_function.test.ts @@ -177,6 +177,35 @@ describe('execute()', () => { ); }); + test('throws when isMissingSecrets is true for connector', async () => { + const executeFn = createExecutionEnqueuerFunction({ + taskManager: mockTaskManager, + isESOCanEncrypt: true, + actionTypeRegistry: actionTypeRegistryMock.create(), + preconfiguredActions: [], + }); + savedObjectsClient.get.mockResolvedValueOnce({ + id: '123', + type: 'action', + attributes: { + name: 'mock-action', + isMissingSecrets: true, + actionTypeId: 'mock-action', + }, + references: [], + }); + await expect( + executeFn(savedObjectsClient, { + id: '123', + params: { baz: false }, + spaceId: 'default', + apiKey: null, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Unable to execute action because no secrets are defined for the \\"mock-action\\" connector."` + ); + }); + test('should ensure action type is enabled', async () => { const mockedActionTypeRegistry = actionTypeRegistryMock.create(); const executeFn = createExecutionEnqueuerFunction({ diff --git a/x-pack/plugins/actions/server/create_execute_function.ts b/x-pack/plugins/actions/server/create_execute_function.ts index 025b4d3107798..6421396e66bb2 100644 --- a/x-pack/plugins/actions/server/create_execute_function.ts +++ b/x-pack/plugins/actions/server/create_execute_function.ts @@ -46,12 +46,18 @@ export function createExecutionEnqueuerFunction({ ); } - const actionTypeId = await getActionTypeId( + const { actionTypeId, name, isMissingSecrets } = await getAction( unsecuredSavedObjectsClient, preconfiguredActions, id ); + if (isMissingSecrets) { + throw new Error( + `Unable to execute action because no secrets are defined for the "${name}" connector.` + ); + } + if (!actionTypeRegistry.isActionExecutable(id, actionTypeId, { notifyUsage: true })) { actionTypeRegistry.ensureActionTypeEnabled(actionTypeId); } @@ -91,18 +97,16 @@ function executionSourceAsSavedObjectReferences(executionSource: ActionExecutorO : {}; } -async function getActionTypeId( +async function getAction( unsecuredSavedObjectsClient: SavedObjectsClientContract, preconfiguredActions: PreConfiguredAction[], actionId: string -): Promise { +): Promise { const pcAction = preconfiguredActions.find((action) => action.id === actionId); if (pcAction) { - return pcAction.actionTypeId; + return pcAction; } - const { - attributes: { actionTypeId }, - } = await unsecuredSavedObjectsClient.get('action', actionId); - return actionTypeId; + const { attributes } = await unsecuredSavedObjectsClient.get('action', actionId); + return attributes; } diff --git a/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts b/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts index 1db990edef2a9..7bc965f0aa5c8 100644 --- a/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts +++ b/x-pack/plugins/alerting/server/alerts_client/alerts_client.ts @@ -269,7 +269,7 @@ export class AlertsClient { throw Boom.badRequest(`Error creating rule: could not create API key - ${error.message}`); } - this.validateActions(alertType, data.actions); + await this.validateActions(alertType, data.actions); const createTime = Date.now(); const { references, actions } = await this.denormalizeActions(data.actions); @@ -728,7 +728,7 @@ export class AlertsClient { data.params, alertType.validate?.params ); - this.validateActions(alertType, data.actions); + await this.validateActions(alertType, data.actions); const { actions, references } = await this.denormalizeActions(data.actions); const username = await this.getUserName(); @@ -1434,10 +1434,36 @@ export class AlertsClient { }; } - private validateActions( + private async validateActions( alertType: UntypedNormalizedAlertType, actions: NormalizedAlertAction[] - ): void { + ): Promise { + if (actions.length === 0) { + return; + } + + // check for actions using connectors with missing secrets + const actionsClient = await this.getActionsClient(); + const actionIds = [...new Set(actions.map((action) => action.id))]; + const actionResults = (await actionsClient.getBulk(actionIds)) || []; + const actionsUsingConnectorsWithMissingSecrets = actionResults.filter( + (result) => result.isMissingSecrets + ); + + if (actionsUsingConnectorsWithMissingSecrets.length) { + throw Boom.badRequest( + i18n.translate('xpack.alerting.alertsClient.validateActions.misconfiguredConnector', { + defaultMessage: 'Invalid connectors: {groups}', + values: { + groups: actionsUsingConnectorsWithMissingSecrets + .map((connector) => connector.name) + .join(', '), + }, + }) + ); + } + + // check for actions with invalid action groups const { actionGroups: alertTypeActionGroups } = alertType; const usedAlertActionGroups = actions.map((action) => action.group); const availableAlertTypeActionGroups = new Set(map(alertTypeActionGroups, 'id')); diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/create.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/create.test.ts index 6f493ced47371..21974cff5eb2f 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/create.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/create.test.ts @@ -93,9 +93,30 @@ function getMockData( describe('create()', () => { let alertsClient: AlertsClient; + let actionsClient: jest.Mocked; - beforeEach(() => { + beforeEach(async () => { alertsClient = new AlertsClient(alertsClientParams); + actionsClient = (await alertsClientParams.getActionsClient()) as jest.Mocked; + actionsClient.getBulk.mockReset(); + actionsClient.getBulk.mockResolvedValue([ + { + id: '1', + actionTypeId: 'test', + config: { + from: 'me@me.com', + hasAuth: false, + host: 'hello', + port: 22, + secure: null, + service: null, + }, + isMissingSecrets: false, + name: 'email connector', + isPreconfigured: false, + }, + ]); + alertsClientParams.getActionsClient.mockResolvedValue(actionsClient); }); describe('authorization', () => { @@ -104,19 +125,6 @@ describe('create()', () => { bar: boolean; }> ): Promise { - unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actions: [], - actionTypeId: 'test', - }, - references: [], - }, - ], - }); unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', @@ -460,7 +468,6 @@ describe('create()', () => { "scheduledTaskId": "task-123", } `); - const actionsClient = (await alertsClientParams.getActionsClient()) as jest.Mocked; expect(actionsClient.isActionTypeEnabled).toHaveBeenCalledWith('test', { notifyUsage: true }); }); @@ -557,6 +564,39 @@ describe('create()', () => { }, ], }); + actionsClient.getBulk.mockReset(); + actionsClient.getBulk.mockResolvedValue([ + { + id: '1', + actionTypeId: 'test', + config: { + from: 'me@me.com', + hasAuth: false, + host: 'hello', + port: 22, + secure: null, + service: null, + }, + isMissingSecrets: false, + name: 'email connector', + isPreconfigured: false, + }, + { + id: '2', + actionTypeId: 'test', + config: { + from: 'me@me.com', + hasAuth: false, + host: 'hello', + port: 22, + secure: null, + service: null, + }, + isMissingSecrets: false, + name: 'email connector', + isPreconfigured: false, + }, + ]); unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', @@ -683,19 +723,6 @@ describe('create()', () => { test('creates a disabled alert', async () => { const data = getMockData({ enabled: false }); - unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actions: [], - actionTypeId: 'test', - }, - references: [], - }, - ], - }); unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', @@ -761,19 +788,6 @@ describe('create()', () => { test('should trim alert name when creating API key', async () => { const data = getMockData({ name: ' my alert name ' }); - unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actions: [], - actionTypeId: 'test', - }, - references: [], - }, - ], - }); unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', @@ -1275,10 +1289,8 @@ describe('create()', () => { test('throws error if loading actions fails', async () => { const data = getMockData(); // Reset from default behaviour - const actionsClient = (await alertsClientParams.getActionsClient()) as jest.Mocked; actionsClient.getBulk.mockReset(); actionsClient.getBulk.mockRejectedValueOnce(new Error('Test Error')); - alertsClientParams.getActionsClient.mockResolvedValue(actionsClient); await expect(alertsClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( `"Test Error"` ); @@ -1292,19 +1304,6 @@ describe('create()', () => { apiKeysEnabled: true, result: { id: '123', name: '123', api_key: 'abc' }, }); - unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actions: [], - actionTypeId: 'test', - }, - references: [], - }, - ], - }); unsecuredSavedObjectsClient.create.mockRejectedValueOnce(new Error('Test failure')); const createdAt = new Date().toISOString(); unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ @@ -1329,19 +1328,6 @@ describe('create()', () => { test('attempts to remove saved object if scheduling failed', async () => { const data = getMockData(); - unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actions: [], - actionTypeId: 'test', - }, - references: [], - }, - ], - }); unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', @@ -1386,19 +1372,6 @@ describe('create()', () => { test('returns task manager error if cleanup fails, logs to console', async () => { const data = getMockData(); - unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actions: [], - actionTypeId: 'test', - }, - references: [], - }, - ], - }); unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', @@ -1455,19 +1428,6 @@ describe('create()', () => { apiKeysEnabled: true, result: { id: '123', name: '123', api_key: 'abc' }, }); - unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actions: [], - actionTypeId: 'test', - }, - references: [], - }, - ], - }); unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', @@ -1579,19 +1539,6 @@ describe('create()', () => { test(`doesn't create API key for disabled alerts`, async () => { const data = getMockData({ enabled: false }); - unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actions: [], - actionTypeId: 'test', - }, - references: [], - }, - ], - }); unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', @@ -1722,4 +1669,32 @@ describe('create()', () => { `"Fail"` ); }); + + test('throws error when adding action using connector with missing secrets', async () => { + const data = getMockData(); + // Reset from default behaviour + actionsClient.getBulk.mockReset(); + actionsClient.getBulk.mockResolvedValueOnce([ + { + id: '1', + actionTypeId: 'test', + config: { + from: 'me@me.com', + hasAuth: false, + host: 'hello', + port: 22, + secure: null, + service: null, + }, + isMissingSecrets: true, + name: 'email connector', + isPreconfigured: false, + }, + ]); + await expect(alertsClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( + `"Invalid connectors: email connector"` + ); + expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); + expect(taskManager.schedule).not.toHaveBeenCalled(); + }); }); diff --git a/x-pack/plugins/alerting/server/alerts_client/tests/update.test.ts b/x-pack/plugins/alerting/server/alerts_client/tests/update.test.ts index cdbfbbac9f9a1..c5e30e29efb44 100644 --- a/x-pack/plugins/alerting/server/alerts_client/tests/update.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/tests/update.test.ts @@ -60,6 +60,7 @@ setGlobalDate(); describe('update()', () => { let alertsClient: AlertsClient; + let actionsClient: jest.Mocked; const existingAlert = { id: '1', type: 'alert', @@ -96,8 +97,28 @@ describe('update()', () => { }, }; - beforeEach(() => { + beforeEach(async () => { alertsClient = new AlertsClient(alertsClientParams); + actionsClient = (await alertsClientParams.getActionsClient()) as jest.Mocked; + actionsClient.getBulk.mockReset(); + actionsClient.getBulk.mockResolvedValue([ + { + id: '1', + actionTypeId: 'test', + config: { + from: 'me@me.com', + hasAuth: false, + host: 'hello', + port: 22, + secure: null, + service: null, + }, + isMissingSecrets: false, + name: 'email connector', + isPreconfigured: false, + }, + ]); + alertsClientParams.getActionsClient.mockResolvedValue(actionsClient); unsecuredSavedObjectsClient.get.mockResolvedValue(existingAlert); encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingDecryptedAlert); alertTypeRegistry.get.mockReturnValue({ @@ -113,6 +134,39 @@ describe('update()', () => { }); test('updates given parameters', async () => { + actionsClient.getBulk.mockReset(); + actionsClient.getBulk.mockResolvedValue([ + { + id: '1', + actionTypeId: 'test', + config: { + from: 'me@me.com', + hasAuth: false, + host: 'hello', + port: 22, + secure: null, + service: null, + }, + isMissingSecrets: false, + name: 'email connector', + isPreconfigured: false, + }, + { + id: '2', + actionTypeId: 'test2', + config: { + from: 'me@me.com', + hasAuth: false, + host: 'hello', + port: 22, + secure: null, + service: null, + }, + isMissingSecrets: false, + name: 'email connector', + isPreconfigured: false, + }, + ]); unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', @@ -342,25 +396,11 @@ describe('update()', () => { "version": "123", } `); - const actionsClient = (await alertsClientParams.getActionsClient()) as jest.Mocked; expect(actionsClient.isActionTypeEnabled).toHaveBeenCalledWith('test', { notifyUsage: true }); expect(actionsClient.isActionTypeEnabled).toHaveBeenCalledWith('test2', { notifyUsage: true }); }); it('calls the createApiKey function', async () => { - unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actions: [], - actionTypeId: 'test', - }, - references: [], - }, - ], - }); alertsClientParams.createAPIKey.mockResolvedValueOnce({ apiKeysEnabled: true, result: { id: '123', name: '123', api_key: 'abc' }, @@ -530,19 +570,6 @@ describe('update()', () => { enabled: false, }, }); - unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actions: [], - actionTypeId: 'test', - }, - references: [], - }, - ], - }); unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', @@ -784,19 +811,6 @@ describe('update()', () => { }); it('should trim alert name in the API key name', async () => { - unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actions: [], - actionTypeId: 'test', - }, - references: [], - }, - ], - }); unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', @@ -842,19 +856,6 @@ describe('update()', () => { }); it('swallows error when invalidate API key throws', async () => { - unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actions: [], - actionTypeId: 'test', - }, - references: [], - }, - ], - }); unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', @@ -914,28 +915,39 @@ describe('update()', () => { it('swallows error when getDecryptedAsInternalUser throws', async () => { encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValue(new Error('Fail')); - unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actions: [], - actionTypeId: 'test', - }, - references: [], + actionsClient.getBulk.mockReset(); + actionsClient.getBulk.mockResolvedValue([ + { + id: '1', + actionTypeId: 'test', + config: { + from: 'me@me.com', + hasAuth: false, + host: 'hello', + port: 22, + secure: null, + service: null, }, - { - id: '2', - type: 'action', - attributes: { - actions: [], - actionTypeId: 'test2', - }, - references: [], + isMissingSecrets: false, + name: 'email connector', + isPreconfigured: false, + }, + { + id: '2', + actionTypeId: 'test2', + config: { + from: 'me@me.com', + hasAuth: false, + host: 'hello', + port: 22, + secure: null, + service: null, }, - ], - }); + isMissingSecrets: false, + name: 'email connector', + isPreconfigured: false, + }, + ]); unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ id: '1', type: 'alert', @@ -1040,19 +1052,6 @@ describe('update()', () => { apiKeysEnabled: true, result: { id: '234', name: '234', api_key: 'abc' }, }); - unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actions: [], - actionTypeId: 'test', - }, - references: [], - }, - ], - }); unsecuredSavedObjectsClient.create.mockRejectedValue(new Error('Fail')); await expect( alertsClient.update({ @@ -1101,19 +1100,6 @@ describe('update()', () => { async executor() {}, producer: 'alerts', }); - unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({ - saved_objects: [ - { - id: '1', - type: 'action', - attributes: { - actions: [], - actionTypeId: 'test', - }, - references: [], - }, - ], - }); encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce({ id: alertId, type: 'alert', @@ -1313,6 +1299,84 @@ describe('update()', () => { }); }); + test('throws error when updating action using connector with missing secrets', async () => { + // Reset from default behaviour + actionsClient.getBulk.mockReset(); + actionsClient.getBulk.mockResolvedValueOnce([ + { + id: '1', + actionTypeId: 'test', + config: { + from: 'me@me.com', + hasAuth: false, + host: 'hello', + port: 22, + secure: null, + service: null, + }, + isMissingSecrets: false, + name: 'email connector', + isPreconfigured: false, + }, + { + id: '2', + actionTypeId: 'tes2', + config: { + from: 'me@me.com', + hasAuth: false, + host: 'hello', + port: 22, + secure: null, + service: null, + }, + isMissingSecrets: true, + name: 'another connector', + isPreconfigured: false, + }, + ]); + + await expect( + alertsClient.update({ + id: '1', + data: { + schedule: { interval: '10s' }, + name: 'abc', + tags: ['foo'], + params: { + bar: true, + }, + throttle: null, + notifyWhen: 'onActiveAlert', + actions: [ + { + group: 'default', + id: '1', + params: { + foo: true, + }, + }, + { + group: 'default', + id: '1', + params: { + foo: true, + }, + }, + { + group: 'default', + id: '2', + params: { + foo: true, + }, + }, + ], + }, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"Invalid connectors: another connector"`); + expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); + expect(taskManager.schedule).not.toHaveBeenCalled(); + }); + describe('authorization', () => { beforeEach(() => { unsecuredSavedObjectsClient.create.mockResolvedValueOnce({ diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/deep_links.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/deep_links.spec.ts new file mode 100644 index 0000000000000..106c380b43207 --- /dev/null +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/deep_links.spec.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +describe('APM depp links', () => { + before(() => { + cy.loginAsReadOnlyUser(); + }); + it('navigates to apm links on search elastic', () => { + cy.visit('/'); + cy.get('[data-test-subj="nav-search-input"]').type('APM'); + cy.contains('APM'); + cy.contains('APM / Services'); + cy.contains('APM / Traces'); + cy.contains('APM / Service Map'); + + // navigates to home page + cy.contains('APM').click(); + cy.url().should('include', '/apm/services'); + + cy.get('[data-test-subj="nav-search-input"]').type('APM'); + // navigates to services page + cy.contains('APM / Services').click(); + cy.url().should('include', '/apm/services'); + + cy.get('[data-test-subj="nav-search-input"]').type('APM'); + // navigates to traces page + cy.contains('APM / Traces').click(); + cy.url().should('include', '/apm/traces'); + + cy.get('[data-test-subj="nav-search-input"]').type('APM'); + // navigates to service maps + cy.contains('APM / Service Map').click(); + cy.url().should('include', '/apm/service-map'); + }); +}); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/home.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/home.spec.ts index 812fa8253a698..4d65424a7b9bd 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/home.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/home.spec.ts @@ -4,26 +4,45 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ + import url from 'url'; import archives_metadata from '../../fixtures/es_archiver/archives_metadata'; import { esArchiverLoad, esArchiverUnload } from '../../tasks/es_archiver'; + const { start, end } = archives_metadata['apm_8.0.0']; +const servicesPath = '/app/apm/services'; +const baseUrl = url.format({ + pathname: servicesPath, + query: { rangeFrom: start, rangeTo: end }, +}); + describe('Home page', () => { before(() => { esArchiverLoad('apm_8.0.0'); - cy.loginAsReadOnlyUser(); }); after(() => { esArchiverUnload('apm_8.0.0'); }); + beforeEach(() => { + cy.loginAsReadOnlyUser(); + }); it('Redirects to service page with rangeFrom and rangeTo added to the URL', () => { - const baseUrl = url.format({ - pathname: '/app/apm', - query: { rangeFrom: start, rangeTo: end }, - }); + cy.visit('/app/apm'); - cy.visit(baseUrl); + cy.url().should( + 'include', + 'app/apm/services?rangeFrom=now-15m&rangeTo=now' + ); cy.get('.euiTabs .euiTab-isSelected').contains('Services'); }); + + it('includes services with only metric documents', () => { + cy.visit( + `${baseUrl}&kuery=not%2520(processor.event%2520%253A%2522transaction%2522%2520)` + ); + cy.contains('opbeans-python'); + cy.contains('opbeans-java'); + cy.contains('opbeans-node'); + }); }); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/header_filters.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/header_filters.spec.ts new file mode 100644 index 0000000000000..d253a290f4a51 --- /dev/null +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/header_filters.spec.ts @@ -0,0 +1,145 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import url from 'url'; +import archives_metadata from '../../../fixtures/es_archiver/archives_metadata'; +import { esArchiverLoad, esArchiverUnload } from '../../../tasks/es_archiver'; + +const { start, end } = archives_metadata['apm_8.0.0']; + +const serviceOverviewPath = '/app/apm/services/kibana/overview'; +const baseUrl = url.format({ + pathname: serviceOverviewPath, + query: { rangeFrom: start, rangeTo: end }, +}); + +const apisToIntercept = [ + { + endpoint: '/api/apm/services/kibana/transactions/charts/latency', + as: 'latencyChartRequest', + }, + { + endpoint: '/api/apm/services/kibana/throughput', + as: 'throughputChartRequest', + }, + { + endpoint: '/api/apm/services/kibana/transactions/charts/error_rate', + as: 'errorRateChartRequest', + }, + { + endpoint: + '/api/apm/services/kibana/transactions/groups/detailed_statistics', + as: 'transactionGroupsDetailedRequest', + }, + { + endpoint: + '/api/apm/services/kibana/service_overview_instances/detailed_statistics', + as: 'instancesDetailedRequest', + }, + { + endpoint: + '/api/apm/services/kibana/service_overview_instances/main_statistics', + as: 'instancesMainStatisticsRequest', + }, + { + endpoint: '/api/apm/services/kibana/error_groups/main_statistics', + as: 'errorGroupsMainStatisticsRequest', + }, + { + endpoint: '/api/apm/services/kibana/transaction/charts/breakdown', + as: 'transactonBreakdownRequest', + }, + { + endpoint: '/api/apm/services/kibana/transactions/groups/main_statistics', + as: 'transactionsGroupsMainStatisticsRequest', + }, +]; + +describe('Service overview - header filters', () => { + before(() => { + esArchiverLoad('apm_8.0.0'); + }); + after(() => { + esArchiverUnload('apm_8.0.0'); + }); + beforeEach(() => { + cy.loginAsReadOnlyUser(); + }); + describe('Filtering by transaction type', () => { + it('changes url when selecting different value', () => { + cy.visit(baseUrl); + cy.contains('Kibana'); + cy.url().should('not.include', 'transactionType'); + cy.get('[data-test-subj="headerFilterTransactionType"]').should( + 'have.value', + 'request' + ); + cy.get('[data-test-subj="headerFilterTransactionType"]').select( + 'taskManager' + ); + cy.url().should('include', 'transactionType=taskManager'); + cy.get('[data-test-subj="headerFilterTransactionType"]').should( + 'have.value', + 'taskManager' + ); + }); + + it('calls APIs with correct transaction type', () => { + apisToIntercept.map(({ endpoint, as }) => { + cy.intercept('GET', endpoint).as(as); + }); + cy.visit(baseUrl); + cy.contains('Kibana'); + cy.get('[data-test-subj="headerFilterTransactionType"]').should( + 'have.value', + 'request' + ); + + cy.expectAPIsToHaveBeenCalledWith({ + apisIntercepted: apisToIntercept.map(({ as }) => `@${as}`), + value: 'transactionType=request', + }); + + cy.get('[data-test-subj="headerFilterTransactionType"]').select( + 'taskManager' + ); + cy.url().should('include', 'transactionType=taskManager'); + cy.get('[data-test-subj="headerFilterTransactionType"]').should( + 'have.value', + 'taskManager' + ); + cy.expectAPIsToHaveBeenCalledWith({ + apisIntercepted: apisToIntercept.map(({ as }) => `@${as}`), + value: 'transactionType=taskManager', + }); + }); + }); + + describe('Filtering by kuerybar', () => { + it('filters by transaction.name', () => { + cy.visit( + url.format({ + pathname: '/app/apm/services/opbeans-java/overview', + query: { rangeFrom: start, rangeTo: end }, + }) + ); + cy.contains('opbeans-java'); + cy.get('[data-test-subj="headerFilterKuerybar"]').type('transaction.n'); + cy.contains('transaction.name'); + cy.get('[data-test-subj="suggestionContainer"]') + .find('li') + .first() + .click(); + cy.get('[data-test-subj="headerFilterKuerybar"]').type(':'); + cy.get('[data-test-subj="suggestionContainer"]') + .find('li') + .first() + .click(); + cy.get('[data-test-subj="suggestionContainer"]').realPress('{enter}'); + cy.url().should('include', '&kuery=transaction.name'); + }); + }); +}); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/instances_table.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/instances_table.spec.ts new file mode 100644 index 0000000000000..2d76dfe977ef7 --- /dev/null +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/instances_table.spec.ts @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import url from 'url'; +import archives_metadata from '../../../fixtures/es_archiver/archives_metadata'; +import { esArchiverLoad, esArchiverUnload } from '../../../tasks/es_archiver'; + +const { start, end } = archives_metadata['apm_8.0.0']; + +const serviceOverviewPath = '/app/apm/services/opbeans-java/overview'; +const baseUrl = url.format({ + pathname: serviceOverviewPath, + query: { rangeFrom: start, rangeTo: end }, +}); + +const apisToIntercept = [ + { + endpoint: + '/api/apm/services/opbeans-java/service_overview_instances/main_statistics', + as: 'instancesMainRequest', + }, + { + endpoint: + '/api/apm/services/opbeans-java/service_overview_instances/detailed_statistics', + as: 'instancesDetailsRequest', + }, + { + endpoint: + '/api/apm/services/opbeans-java/service_overview_instances/details/02950c4c5fbb0fda1cc98c47bf4024b473a8a17629db6530d95dcee68bd54c6c', + as: 'instanceDetailsRequest', + }, + { + endpoint: + '/api/apm/services/opbeans-java/service_overview_instances/details/02950c4c5fbb0fda1cc98c47bf4024b473a8a17629db6530d95dcee68bd54c6c', + as: 'instanceDetailsRequest', + }, +]; + +describe('Instances table', () => { + beforeEach(() => { + cy.loginAsReadOnlyUser(); + }); + describe('when data is not loaded', () => { + it('shows empty message', () => { + cy.visit(baseUrl); + cy.contains('opbeans-java'); + cy.get('[data-test-subj="serviceInstancesTableContainer"]').contains( + 'No items found' + ); + }); + }); + + describe('when data is loaded', () => { + before(() => { + esArchiverLoad('apm_8.0.0'); + }); + after(() => { + esArchiverUnload('apm_8.0.0'); + }); + const serviceNodeName = + '02950c4c5fbb0fda1cc98c47bf4024b473a8a17629db6530d95dcee68bd54c6c'; + it('has data in the table', () => { + cy.visit(baseUrl); + cy.contains('opbeans-java'); + cy.contains(serviceNodeName); + }); + it('shows instance details', () => { + apisToIntercept.map(({ endpoint, as }) => { + cy.intercept('GET', endpoint).as(as); + }); + + cy.visit(baseUrl); + cy.contains('opbeans-java'); + + cy.wait('@instancesMainRequest'); + cy.contains(serviceNodeName); + + cy.wait('@instancesDetailsRequest'); + cy.get( + `[data-test-subj="instanceDetailsButton_${serviceNodeName}"]` + ).realClick(); + cy.get('[data-test-subj="loadingSpinner"]').should('be.visible'); + cy.wait('@instanceDetailsRequest').then(() => { + cy.contains('Service'); + }); + }); + it('shows actions available', () => { + apisToIntercept.map(({ endpoint, as }) => { + cy.intercept('GET', endpoint).as(as); + }); + + cy.visit(baseUrl); + cy.contains('opbeans-java'); + + cy.wait('@instancesMainRequest'); + cy.contains(serviceNodeName); + + cy.wait('@instancesDetailsRequest'); + cy.get( + `[data-test-subj="instanceActionsButton_${serviceNodeName}"]` + ).realClick(); + cy.contains('Pod logs'); + cy.contains('Pod metrics'); + cy.contains('Container logs'); + cy.contains('Container metrics'); + cy.contains('Filter overview by instance'); + cy.contains('Metrics'); + }); + }); +}); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/support/commands.ts b/x-pack/plugins/apm/ftr_e2e/cypress/support/commands.ts index 37e498619bdd4..31eab9507ef5e 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/support/commands.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/support/commands.ts @@ -5,6 +5,7 @@ * 2.0. */ import 'cypress-real-events/support'; +import { Interception } from 'cypress/types/net-stubbing'; Cypress.Commands.add('loginAsReadOnlyUser', () => { cy.loginAs({ username: 'apm_read_user', password: 'changeme' }); @@ -39,3 +40,24 @@ Cypress.Commands.add('changeTimeRange', (value: string) => { cy.get('[data-test-subj="superDatePickerToggleQuickMenuButton"]').click(); cy.contains(value).click(); }); + +Cypress.Commands.add( + 'expectAPIsToHaveBeenCalledWith', + ({ + apisIntercepted, + value, + }: { + apisIntercepted: string[]; + value: string; + }) => { + cy.wait(apisIntercepted).then((interceptions) => { + if (Array.isArray(interceptions)) { + interceptions.map((interception) => { + expect(interception.request.url).include(value); + }); + } else { + expect((interceptions as Interception).request.url).include(value); + } + }); + } +); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/support/types.d.ts b/x-pack/plugins/apm/ftr_e2e/cypress/support/types.d.ts index fff38c9c6d18b..b47e664e0a0f8 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/support/types.d.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/support/types.d.ts @@ -11,5 +11,9 @@ declare namespace Cypress { loginAsSuperUser(): void; loginAs(params: { username: string; password: string }): void; changeTimeRange(value: string): void; + expectAPIsToHaveBeenCalledWith(params: { + apisIntercepted: string[]; + value: string; + }): void; } } diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/get_columns.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/get_columns.tsx index 4da5ba5a4ae64..46747e18c44af 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/get_columns.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/get_columns.tsx @@ -234,6 +234,7 @@ export function getColumns({ anchorPosition="leftCenter" button={ toggleRowActionMenu(instanceItem.serviceNodeName) @@ -257,6 +258,7 @@ export function getColumns({ render: (instanceItem: MainStatsServiceInstanceItem) => { return ( toggleRowDetails(instanceItem.serviceNodeName)} aria-label={ itemIdToExpandedRowMap[instanceItem.serviceNodeName] diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/index.tsx index 909c879b433b0..98443fdf0d0ea 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/index.tsx @@ -138,12 +138,13 @@ export function ServiceOverviewInstancesTable({ - + (this.parentNode = node)}>{suggestions} + (this.parentNode = node)} + > + {suggestions} + ); } } diff --git a/x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/index.js b/x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/index.js index 21524a877d4cc..f441497209c3a 100644 --- a/x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/index.js +++ b/x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/index.js @@ -172,6 +172,7 @@ export class Typeahead extends Component { >
; export type CommentPatchAttributes = rt.TypeOf; export type CommentRequestUserType = rt.TypeOf; export type CommentRequestAlertType = rt.TypeOf; +export type GetCaseIdsByAlertIdAggs = rt.TypeOf; diff --git a/x-pack/plugins/cases/common/api/runtime_types.ts b/x-pack/plugins/cases/common/api/runtime_types.ts index 8001eb80cec73..f608b7effe969 100644 --- a/x-pack/plugins/cases/common/api/runtime_types.ts +++ b/x-pack/plugins/cases/common/api/runtime_types.ts @@ -13,6 +13,10 @@ import { isObject } from 'lodash/fp'; type ErrorFactory = (message: string) => Error; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils/src/format_errors/index.ts + * Bug fix for the TODO is in the format_errors package + */ export const formatErrors = (errors: rt.Errors): string[] => { const err = errors.map((error) => { if (error.message != null) { diff --git a/x-pack/plugins/cases/common/constants.ts b/x-pack/plugins/cases/common/constants.ts index f9fae2466a59b..966305524c059 100644 --- a/x-pack/plugins/cases/common/constants.ts +++ b/x-pack/plugins/cases/common/constants.ts @@ -31,6 +31,8 @@ export const CASE_STATUS_URL = `${CASES_URL}/status`; export const CASE_TAGS_URL = `${CASES_URL}/tags`; export const CASE_USER_ACTIONS_URL = `${CASE_DETAILS_URL}/user_actions`; +export const CASE_ALERTS_URL = `${CASES_URL}/alerts/{alert_id}`; + /** * Action routes */ diff --git a/x-pack/plugins/cases/server/routes/api/cases/alerts/get_cases.ts b/x-pack/plugins/cases/server/routes/api/cases/alerts/get_cases.ts new file mode 100644 index 0000000000000..1fc41874fe9d5 --- /dev/null +++ b/x-pack/plugins/cases/server/routes/api/cases/alerts/get_cases.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; +import Boom from '@hapi/boom'; + +import { RouteDeps } from '../../types'; +import { wrapError } from '../../utils'; +import { CASE_ALERTS_URL } from '../../../../../common/constants'; + +export function initGetCaseIdsByAlertIdApi({ caseService, router, logger }: RouteDeps) { + router.get( + { + path: CASE_ALERTS_URL, + validate: { + params: schema.object({ + alert_id: schema.string(), + }), + }, + }, + async (context, request, response) => { + try { + const alertId = request.params.alert_id; + if (alertId == null || alertId === '') { + throw Boom.badRequest('The `alertId` is not valid'); + } + const client = context.core.savedObjects.client; + const caseIds = await caseService.getCaseIdsByAlertId({ + client, + alertId, + }); + + return response.ok({ + body: caseIds, + }); + } catch (error) { + logger.error( + `Failed to retrieve case ids for this alert id: ${request.params.alert_id}: ${error}` + ); + return response.customError(wrapError(error)); + } + } + ); +} diff --git a/x-pack/plugins/cases/server/routes/api/index.ts b/x-pack/plugins/cases/server/routes/api/index.ts index c5b7aa85dc33e..a1635254dd09b 100644 --- a/x-pack/plugins/cases/server/routes/api/index.ts +++ b/x-pack/plugins/cases/server/routes/api/index.ts @@ -38,6 +38,7 @@ import { initPatchSubCasesApi } from './cases/sub_case/patch_sub_cases'; import { initFindSubCasesApi } from './cases/sub_case/find_sub_cases'; import { initDeleteSubCasesApi } from './cases/sub_case/delete_sub_cases'; import { ENABLE_CASE_CONNECTOR } from '../../../common/constants'; +import { initGetCaseIdsByAlertIdApi } from './cases/alerts/get_cases'; /** * Default page number when interacting with the saved objects API. @@ -86,4 +87,6 @@ export function initCaseApi(deps: RouteDeps) { initGetCasesStatusApi(deps); // Tags initGetTagsApi(deps); + // Alerts + initGetCaseIdsByAlertIdApi(deps); } diff --git a/x-pack/plugins/cases/server/services/index.ts b/x-pack/plugins/cases/server/services/index.ts index a27a8860e96b5..11b8cef6ab5a5 100644 --- a/x-pack/plugins/cases/server/services/index.ts +++ b/x-pack/plugins/cases/server/services/index.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { AggregationContainer } from '@elastic/elasticsearch/api/types'; import { KibanaRequest, Logger, @@ -17,6 +18,7 @@ import { SavedObjectsBulkResponse, SavedObjectsFindResult, } from 'kibana/server'; +import { nodeBuilder } from '../../../../../src/plugins/data/common'; import { AuthenticatedUser, SecurityPluginSetup } from '../../../security/server'; import { @@ -34,6 +36,7 @@ import { CaseResponse, caseTypeField, CasesFindRequest, + GetCaseIdsByAlertIdAggs, } from '../../common'; import { combineFilters, defaultSortField, groupTotalAlertsByID } from '../common'; import { defaultPage, defaultPerPage } from '../routes/api'; @@ -113,6 +116,10 @@ interface GetCommentArgs extends ClientArgs { commentId: string; } +interface GetCaseIdsByAlertIdArgs extends ClientArgs { + alertId: string; +} + interface PostCaseArgs extends ClientArgs { attributes: ESCaseAttributes; } @@ -220,6 +227,7 @@ export interface CaseServiceSetup { getSubCases(args: GetSubCasesArgs): Promise>; getCases(args: GetCasesArgs): Promise>; getComment(args: GetCommentArgs): Promise>; + getCaseIdsByAlertId(args: GetCaseIdsByAlertIdArgs): Promise; getTags(args: ClientArgs): Promise; getReporters(args: ClientArgs): Promise; getUser(args: GetUserArgs): Promise; @@ -899,6 +907,56 @@ export class CaseService implements CaseServiceSetup { } } + private buildCaseIdsAggs = (size: number = 100): Record => ({ + references: { + nested: { + path: `${CASE_COMMENT_SAVED_OBJECT}.references`, + }, + aggregations: { + caseIds: { + terms: { + field: `${CASE_COMMENT_SAVED_OBJECT}.references.id`, + size, + }, + }, + }, + }, + }); + + public async getCaseIdsByAlertId({ + client, + alertId, + }: GetCaseIdsByAlertIdArgs): Promise { + try { + this.log.debug(`Attempting to GET all cases for alert id ${alertId}`); + + let response = await client.find({ + type: CASE_COMMENT_SAVED_OBJECT, + fields: [], + page: 1, + perPage: 1, + sortField: defaultSortField, + aggs: this.buildCaseIdsAggs(), + filter: nodeBuilder.is(`${CASE_COMMENT_SAVED_OBJECT}.attributes.alertId`, alertId), + }); + if (response.total > 100) { + response = await client.find({ + type: CASE_COMMENT_SAVED_OBJECT, + fields: [], + page: 1, + perPage: 1, + sortField: defaultSortField, + aggs: this.buildCaseIdsAggs(response.total), + filter: nodeBuilder.is(`${CASE_COMMENT_SAVED_OBJECT}.attributes.alertId`, alertId), + }); + } + return response.aggregations?.references.caseIds.buckets.map((b) => b.key) ?? []; + } catch (error) { + this.log.error(`Error on GET all cases for alert id ${alertId}: ${error}`); + throw error; + } + } + /** * Default behavior is to retrieve all comments that adhere to a given filter (if one is included). * to override this pass in the either the page or perPage options. diff --git a/x-pack/plugins/cases/server/services/mocks.ts b/x-pack/plugins/cases/server/services/mocks.ts index 51eb0bbb1a7e4..d67a297508b14 100644 --- a/x-pack/plugins/cases/server/services/mocks.ts +++ b/x-pack/plugins/cases/server/services/mocks.ts @@ -31,6 +31,7 @@ export const createCaseServiceMock = (): CaseServiceMock => ({ getAllSubCaseComments: jest.fn(), getCase: jest.fn(), getCases: jest.fn(), + getCaseIdsByAlertId: jest.fn(), getComment: jest.fn(), getMostRecentSubCase: jest.fn(), getSubCase: jest.fn(), diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.test.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.test.tsx index 801db2e16ab3b..bd46724a77934 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.test.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.test.tsx @@ -5,6 +5,7 @@ * 2.0. */ +import { Subject } from 'rxjs'; import { FlyoutCreateDrilldownAction, OpenFlyoutAddDrilldownParams, @@ -22,6 +23,9 @@ const actionParams: OpenFlyoutAddDrilldownParams = { start: () => ({ core: { overlays, + application: { + currentAppId$: new Subject(), + }, } as any, plugins: { uiActionsEnhanced, diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx index 4c0db8f317e51..1d7cab8db14bc 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx @@ -7,6 +7,8 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; +import { skip, take, takeUntil } from 'rxjs/operators'; +import { Subject } from 'rxjs'; import { Action } from '../../../../../../../../src/plugins/ui_actions/public'; import { toMountPoint } from '../../../../../../../../src/plugins/kibana_react/public'; import { @@ -82,7 +84,11 @@ export class FlyoutCreateDrilldownAction implements Action { } const templates = createDrilldownTemplatesFromSiblings(embeddable); - + const closed$ = new Subject(); + const close = () => { + closed$.next(true); + handle.close(); + }; const handle = core.overlays.openFlyout( toMountPoint( { triggers={[...ensureNestedTriggers(embeddable.supportedTriggers()), CONTEXT_MENU_TRIGGER]} placeContext={{ embeddable }} templates={templates} - onClose={() => handle.close()} + onClose={close} /> ), { @@ -100,5 +106,10 @@ export class FlyoutCreateDrilldownAction implements Action { 'data-test-subj': 'createDrilldownFlyout', } ); + + // Close flyout on application change. + core.application.currentAppId$.pipe(takeUntil(closed$), skip(1), take(1)).subscribe(() => { + close(); + }); } } diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.test.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.test.tsx index 30ba1b9842c96..e4c2d5b657630 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.test.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.test.tsx @@ -5,6 +5,7 @@ * 2.0. */ +import { Subject } from 'rxjs'; import { FlyoutEditDrilldownAction, FlyoutEditDrilldownParams } from './flyout_edit_drilldown'; import { coreMock } from '../../../../../../../../src/core/public/mocks'; import { ViewMode } from '../../../../../../../../src/plugins/embeddable/public'; @@ -32,6 +33,9 @@ const actionParams: FlyoutEditDrilldownParams = { start: () => ({ core: { overlays, + application: { + currentAppId$: new Subject(), + }, } as any, plugins: { uiActionsEnhanced: uiActions, diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx index 44eb63bbc504b..3949b12ace328 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx @@ -6,6 +6,8 @@ */ import React from 'react'; +import { skip, take, takeUntil } from 'rxjs/operators'; +import { Subject } from 'rxjs'; import { Action } from '../../../../../../../../src/plugins/ui_actions/public'; import { reactToUiComponent, @@ -67,7 +69,11 @@ export class FlyoutEditDrilldownAction implements Action { } const templates = createDrilldownTemplatesFromSiblings(embeddable); - + const closed$ = new Subject(); + const close = () => { + closed$.next(true); + handle.close(); + }; const handle = core.overlays.openFlyout( toMountPoint( { triggers={[...ensureNestedTriggers(embeddable.supportedTriggers()), CONTEXT_MENU_TRIGGER]} placeContext={{ embeddable }} templates={templates} - onClose={() => handle.close()} + onClose={close} /> ), { @@ -84,5 +90,10 @@ export class FlyoutEditDrilldownAction implements Action { 'data-test-subj': 'editDrilldownFlyout', } ); + + // Close flyout on application change. + core.application.currentAppId$.pipe(takeUntil(closed$), skip(1), take(1)).subscribe(() => { + close(); + }); } } diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/main.test.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/main.test.tsx index dcc39e9fb385a..13dfe12ccb5e4 100644 --- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/main.test.tsx +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/main.test.tsx @@ -60,7 +60,7 @@ describe('Background Search Session Management Main', () => { ELASTIC_WEBSITE_URL: `boo/`, DOC_LINK_VERSION: `#foo`, links: { - elasticsearch: { asyncSearch: `mock-url` } as any, + search: { sessions: `mock-url` } as any, } as any, }; diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.test.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.test.tsx index 123cc3dcb97cc..4d97092209c67 100644 --- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.test.tsx +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.test.tsx @@ -129,10 +129,10 @@ describe('Background Search Session Management Table', () => { expect(table.find('tbody td').map((node) => node.text())).toMatchInlineSnapshot(` Array [ "App", - "Namevery background search", - "StatusIn progress", + "Namevery background search ", + "StatusExpired", "Created2 Dec, 2020, 00:19:32", - "Expiration7 Dec, 2020, 00:19:32", + "Expiration--", "", "", ] diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.test.ts b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.test.ts index f1f38fecfe012..3857b08ad0a3a 100644 --- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.test.ts +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.test.ts @@ -87,6 +87,35 @@ describe('Search Sessions Management API', () => { `); }); + test('completed session with expired time is showed as expired', async () => { + sessionsClient.find = jest.fn().mockImplementation(async () => { + return { + saved_objects: [ + { + id: 'hello-pizza-123', + attributes: { + name: 'Veggie', + appId: 'pizza', + status: 'complete', + expires: moment().subtract(3, 'days'), + initialState: {}, + restoreState: {}, + }, + }, + ], + } as SavedObjectsFindResponse; + }); + + const api = new SearchSessionsMgmtAPI(sessionsClient, mockConfig, { + urls: mockUrls, + notifications: mockCoreStart.notifications, + application: mockCoreStart.application, + }); + + const res = await api.fetchTableData(); + expect(res[0].status).toBe(SearchSessionStatus.EXPIRED); + }); + test('handle error from sessionsClient response', async () => { sessionsClient.find = jest.fn().mockRejectedValue(new Error('implementation is so bad')); diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.ts b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.ts index 943fc7d26d36d..3710dfa16e76b 100644 --- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.ts +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.ts @@ -17,12 +17,16 @@ import { } from '../../../../../../../src/plugins/data/public'; import { SearchSessionStatus } from '../../../../../../../src/plugins/data/common'; import { ACTION } from '../components/actions'; -import { PersistedSearchSessionSavedObjectAttributes, UISession } from '../types'; +import { + PersistedSearchSessionSavedObjectAttributes, + UISearchSessionState, + UISession, +} from '../types'; import { SessionsConfigSchema } from '..'; type UrlGeneratorsStart = SharePluginStart['urlGenerators']; -function getActions(status: SearchSessionStatus) { +function getActions(status: UISearchSessionState) { const actions: ACTION[] = []; actions.push(ACTION.INSPECT); actions.push(ACTION.RENAME); @@ -30,9 +34,33 @@ function getActions(status: SearchSessionStatus) { actions.push(ACTION.EXTEND); actions.push(ACTION.DELETE); } + + if (status === SearchSessionStatus.EXPIRED) { + actions.push(ACTION.DELETE); + } + return actions; } +/** + * Status we display on mgtm UI might be different from the one inside the saved object + * @param status + */ +function getUIStatus(session: PersistedSearchSessionSavedObjectAttributes): UISearchSessionState { + const isSessionExpired = () => { + const curTime = moment(); + return curTime.diff(moment(session.expires), 'ms') > 0; + }; + + switch (session.status) { + case SearchSessionStatus.COMPLETE: + case SearchSessionStatus.IN_PROGRESS: + return isSessionExpired() ? SearchSessionStatus.EXPIRED : session.status; + } + + return session.status; +} + async function getUrlFromState( urls: UrlGeneratorsStart, urlGeneratorId: string, @@ -59,12 +87,12 @@ const mapToUISession = (urls: UrlGeneratorsStart, config: SessionsConfigSchema) appId, created, expires, - status, urlGeneratorId, initialState, restoreState, } = savedObject.attributes; + const status = getUIStatus(savedObject.attributes); const actions = getActions(status); // TODO: initialState should be saved without the searchSessionID diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/documentation.ts b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/documentation.ts index 38db89e88a6e1..545c7f7ec26b2 100644 --- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/documentation.ts +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/documentation.ts @@ -12,8 +12,7 @@ export class AsyncSearchIntroDocumentation { constructor(docs: DocLinksStart) { const { links } = docs; - // TODO: There should be Kibana documentation link about Search Sessions in Kibana - this.docUrl = links.elasticsearch.asyncSearch; + this.docUrl = links.search.sessions; } public getElasticsearchDocLink() { diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/types.ts b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/types.ts index 369eac450e07b..d0d5ee9fb17dd 100644 --- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/types.ts +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/types.ts @@ -25,13 +25,15 @@ export type PersistedSearchSessionSavedObjectAttributes = SearchSessionSavedObje > >; +export type UISearchSessionState = SearchSessionStatus; + export interface UISession { id: string; name: string; appId: string; created: string; expires: string | null; - status: SearchSessionStatus; + status: UISearchSessionState; actions?: ACTION[]; reloadUrl: string; restoreUrl: string; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/constants.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/constants.ts new file mode 100644 index 0000000000000..903d1768f3cc1 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/constants.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const LICENSE_CALLOUT_BODY = i18n.translate('xpack.enterpriseSearch.licenseCalloutBody', { + defaultMessage: + 'Enterprise authentication via SAML, document-level permission and authorization support, custom search experiences and more are available with a valid Platinum license.', +}); + +export const LICENSE_CALLOUT_BUTTON = i18n.translate( + 'xpack.enterpriseSearch.licenseCalloutButton', + { + defaultMessage: 'Manage your license', + } +); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/index.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/index.ts new file mode 100644 index 0000000000000..80a9355ee118e --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { LicenseCallout } from './license_callout'; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/license_callout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/license_callout.test.tsx new file mode 100644 index 0000000000000..26892f7cec7ae --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/license_callout.test.tsx @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { setMockValues } from '../../../__mocks__'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiPanel, EuiText } from '@elastic/eui'; + +import { EuiButtonTo } from '../../../shared/react_router_helpers'; + +import { LicenseCallout } from './'; + +describe('LicenseCallout', () => { + it('renders when non-platinum or on trial', () => { + setMockValues({ + hasPlatinumLicense: false, + isTrial: true, + }); + const wrapper = shallow(); + + expect(wrapper.find(EuiPanel)).toHaveLength(1); + expect(wrapper.find(EuiText)).toHaveLength(2); + expect(wrapper.find(EuiButtonTo).prop('to')).toEqual( + '/app/management/stack/license_management' + ); + }); + + it('does not render for platinum', () => { + setMockValues({ + hasPlatinumLicense: true, + isTrial: false, + }); + const wrapper = shallow(); + + expect(wrapper.isEmptyRender()).toBe(true); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/license_callout.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/license_callout.tsx new file mode 100644 index 0000000000000..4a4de17450f1b --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/license_callout/license_callout.tsx @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useValues } from 'kea'; + +import { EuiPanel, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; + +import { LicensingLogic } from '../../../shared/licensing'; +import { EuiButtonTo } from '../../../shared/react_router_helpers'; + +import { PRODUCT_SELECTOR_CALLOUT_HEADING } from '../../constants'; + +import { LICENSE_CALLOUT_BODY, LICENSE_CALLOUT_BUTTON } from './constants'; + +export const LicenseCallout: React.FC = () => { + const { hasPlatinumLicense, isTrial } = useValues(LicensingLogic); + + if (hasPlatinumLicense && !isTrial) return null; + + return ( + + + + +

{PRODUCT_SELECTOR_CALLOUT_HEADING}

+
+ {LICENSE_CALLOUT_BODY} +
+ + + + {LICENSE_CALLOUT_BUTTON} + + +
+
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.test.tsx index 9ee34634e3797..9c0f7c2bac94b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.test.tsx @@ -13,8 +13,10 @@ import { shallow } from 'enzyme'; import { EuiPage } from '@elastic/eui'; +import { LicenseCallout } from '../license_callout'; import { ProductCard } from '../product_card'; import { SetupGuideCta } from '../setup_guide'; +import { TrialCallout } from '../trial_callout'; import { ProductSelector } from './'; @@ -26,6 +28,15 @@ describe('ProductSelector', () => { expect(wrapper.find(EuiPage).hasClass('enterpriseSearchOverview')).toBe(true); expect(wrapper.find(ProductCard)).toHaveLength(2); expect(wrapper.find(SetupGuideCta)).toHaveLength(1); + expect(wrapper.find(LicenseCallout)).toHaveLength(0); + }); + + it('renders the license and trial callouts', () => { + setMockValues({ config: { host: 'localhost' } }); + const wrapper = shallow(); + + expect(wrapper.find(TrialCallout)).toHaveLength(1); + expect(wrapper.find(LicenseCallout)).toHaveLength(1); }); describe('access checks when host is set', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.tsx index f2476a5770c25..a7eb2424e797a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.tsx @@ -29,8 +29,10 @@ import { SendEnterpriseSearchTelemetry as SendTelemetry } from '../../../shared/ import AppSearchImage from '../../assets/app_search.png'; import WorkplaceSearchImage from '../../assets/workplace_search.png'; +import { LicenseCallout } from '../license_callout'; import { ProductCard } from '../product_card'; import { SetupGuideCta } from '../setup_guide'; +import { TrialCallout } from '../trial_callout'; interface ProductSelectorProps { access: { @@ -53,6 +55,7 @@ export const ProductSelector: React.FC = ({ access }) => { + @@ -88,8 +91,8 @@ export const ProductSelector: React.FC = ({ access }) => { )} - - {!config.host && } + + {config.host ? : } diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide_cta.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide_cta.tsx index 17260cc15793a..277af62b882c6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide_cta.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide_cta.tsx @@ -11,6 +11,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { EuiPanelTo } from '../../../shared/react_router_helpers'; +import { PRODUCT_SELECTOR_CALLOUT_HEADING } from '../../constants'; import CtaImage from './assets/getting_started.png'; import './setup_guide_cta.scss'; @@ -20,11 +21,7 @@ export const SetupGuideCta: React.FC = () => ( -

- {i18n.translate('xpack.enterpriseSearch.overview.setupCta.title', { - defaultMessage: 'Enterprise-grade functionality for teams big and small', - })} -

+

{PRODUCT_SELECTOR_CALLOUT_HEADING}

{i18n.translate('xpack.enterpriseSearch.overview.setupCta.description', { diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/trial_callout/index.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/trial_callout/index.ts new file mode 100644 index 0000000000000..fdcfa70ffa948 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/trial_callout/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { TrialCallout } from './trial_callout'; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/trial_callout/trial_callout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/trial_callout/trial_callout.test.tsx new file mode 100644 index 0000000000000..2ed2f9e43e9c4 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/trial_callout/trial_callout.test.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { setMockValues } from '../../../__mocks__'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiCallOut } from '@elastic/eui'; + +import { TrialCallout } from './'; + +describe('TrialCallout', () => { + it('renders when non-platinum or on trial', () => { + setMockValues({ isTrial: true }); + const wrapper = shallow(); + + expect(wrapper.find(EuiCallOut)).toHaveLength(1); + }); + + it('does not render when not on trial', () => { + setMockValues({ isTrial: false }); + const wrapper = shallow(); + + expect(wrapper.find(EuiCallOut)).toHaveLength(0); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/trial_callout/trial_callout.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/trial_callout/trial_callout.tsx new file mode 100644 index 0000000000000..1140d42b9189f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/trial_callout/trial_callout.tsx @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useValues } from 'kea'; +import moment from 'moment'; + +import { EuiCallOut, EuiLink, EuiSpacer } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { LicensingLogic } from '../../../shared/licensing'; + +export const TrialCallout: React.FC = () => { + const { license, isTrial } = useValues(LicensingLogic); + + if (!isTrial) return null; + + const title = ( + <> + {' '} + + + + + + + ); + + return ( + <> + + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/constants.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/constants.ts new file mode 100644 index 0000000000000..c5997222fef6e --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/constants.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const PRODUCT_SELECTOR_CALLOUT_HEADING = i18n.translate( + 'xpack.enterpriseSearch.productSelectorCalloutTitle', + { + defaultMessage: 'Enterprise-grade functionality for teams big and small', + } +); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.test.ts index b7569111c1ade..4ea74e1c0d4f2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.test.ts @@ -158,5 +158,34 @@ describe('LicensingLogic', () => { expect(LicensingLogic.values.hasGoldLicense).toEqual(false); }); }); + + describe('isTrial', () => { + it('is true for active trial license', () => { + updateLicense({ status: 'active', type: 'trial' }); + expect(LicensingLogic.values.isTrial).toEqual(true); + }); + + it('is false if the trial license is expired', () => { + updateLicense({ status: 'expired', type: 'trial' }); + expect(LicensingLogic.values.isTrial).toEqual(false); + }); + + it('is false for all non-trial licenses', () => { + updateLicense({ status: 'active', type: 'basic' }); + expect(LicensingLogic.values.isTrial).toEqual(false); + + updateLicense({ status: 'active', type: 'standard' }); + expect(LicensingLogic.values.isTrial).toEqual(false); + + updateLicense({ status: 'active', type: 'gold' }); + expect(LicensingLogic.values.isTrial).toEqual(false); + + updateLicense({ status: 'active', type: 'platinum' }); + expect(LicensingLogic.values.isTrial).toEqual(false); + + updateLicense({ status: 'active', type: 'enterprise' }); + expect(LicensingLogic.values.isTrial).toEqual(false); + }); + }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.ts index 627401cdc6147..7d0222f476214 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.ts @@ -15,6 +15,7 @@ interface LicensingValues { licenseSubscription: Subscription | null; hasPlatinumLicense: boolean; hasGoldLicense: boolean; + isTrial: boolean; } interface LicensingActions { setLicense(license: ILicense): ILicense; @@ -56,6 +57,10 @@ export const LicensingLogic = kea [selectors.license], + (license) => license?.isActive && license?.type === 'trial', + ], }, events: ({ props, actions, values }) => ({ afterMount: () => { diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/enrollment_token_list_page/components/new_enrollment_key_flyout.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/enrollment_token_list_page/components/new_enrollment_key_flyout.tsx index a21e59c5c220f..7fae295d0d5b4 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/enrollment_token_list_page/components/new_enrollment_key_flyout.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/enrollment_token_list_page/components/new_enrollment_key_flyout.tsx @@ -116,10 +116,12 @@ export const NewEnrollmentTokenFlyout: React.FunctionComponent = ({ required={true} defaultValue={policyIdDefaultValue} {...form.policyIdInput.props} - options={agentPolicies.map((agentPolicy) => ({ - value: agentPolicy.id, - text: agentPolicy.name, - }))} + options={agentPolicies + .filter((agentPolicy) => !agentPolicy.is_managed) + .map((agentPolicy) => ({ + value: agentPolicy.id, + text: agentPolicy.name, + }))} /> diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/enrollment_token_list_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/enrollment_token_list_page/index.tsx index 5d867a1c4c93c..6d141b0c9ebf1 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/enrollment_token_list_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/enrollment_token_list_page/index.tsx @@ -31,7 +31,7 @@ import { useStartServices, sendDeleteOneEnrollmentAPIKey, } from '../../../hooks'; -import type { EnrollmentAPIKey } from '../../../types'; +import type { EnrollmentAPIKey, GetAgentPoliciesResponseItem } from '../../../types'; import { SearchBar } from '../../../components/search_bar'; import { NewEnrollmentTokenFlyout } from './components/new_enrollment_key_flyout'; @@ -171,9 +171,21 @@ export const EnrollmentTokenListPage: React.FunctionComponent<{}> = () => { }); const agentPolicies = agentPoliciesRequest.data ? agentPoliciesRequest.data.items : []; + const agentPoliciesById = agentPolicies.reduce( + (acc: { [key: string]: GetAgentPoliciesResponseItem }, policy) => { + acc[policy.id] = policy; + return acc; + }, + {} + ); const total = enrollmentAPIKeysRequest?.data?.total ?? 0; - const items = enrollmentAPIKeysRequest?.data?.list ?? []; + const rowItems = + enrollmentAPIKeysRequest?.data?.list.filter((enrollmentKey) => { + if (!agentPolicies.length || !enrollmentKey.policy_id) return false; + const agentPolicy = agentPoliciesById[enrollmentKey.policy_id]; + return !agentPolicy?.is_managed; + }) || []; const columns = [ { @@ -203,7 +215,7 @@ export const EnrollmentTokenListPage: React.FunctionComponent<{}> = () => { defaultMessage: 'Agent policy', }), render: (policyId: string) => { - const agentPolicy = agentPolicies.find((c) => c.id === policyId); + const agentPolicy = agentPoliciesById[policyId]; const value = agentPolicy ? agentPolicy.name : policyId; return ( @@ -314,7 +326,7 @@ export const EnrollmentTokenListPage: React.FunctionComponent<{}> = () => { /> ) } - items={total ? items : []} + items={total ? rowItems : []} itemId="id" columns={columns} pagination={{ diff --git a/x-pack/plugins/fleet/server/services/hosts_utils.test.ts b/x-pack/plugins/fleet/server/services/hosts_utils.test.ts new file mode 100644 index 0000000000000..785fbbc3f5bdd --- /dev/null +++ b/x-pack/plugins/fleet/server/services/hosts_utils.test.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { normalizeHostsForAgents } from './hosts_utils'; + +describe('normalizeHostsForAgents', () => { + const scenarios = [ + { sourceUrl: 'http://test.fr', expectedUrl: 'http://test.fr:80' }, + { sourceUrl: 'http://test.fr/test/toto', expectedUrl: 'http://test.fr:80/test/toto' }, + { sourceUrl: 'https://test.fr', expectedUrl: 'https://test.fr:443' }, + { sourceUrl: 'https://test.fr/test/toto', expectedUrl: 'https://test.fr:443/test/toto' }, + { sourceUrl: 'https://test.fr:9243', expectedUrl: 'https://test.fr:9243' }, + { sourceUrl: 'https://test.fr:9243/test/toto', expectedUrl: 'https://test.fr:9243/test/toto' }, + ]; + + for (const scenario of scenarios) { + it(`should transform ${scenario.sourceUrl} correctly`, () => { + const url = normalizeHostsForAgents(scenario.sourceUrl); + + expect(url).toEqual(scenario.expectedUrl); + }); + } +}); diff --git a/x-pack/plugins/fleet/server/services/hosts_utils.ts b/x-pack/plugins/fleet/server/services/hosts_utils.ts new file mode 100644 index 0000000000000..2db77bc12879b --- /dev/null +++ b/x-pack/plugins/fleet/server/services/hosts_utils.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +function getPortForURL(url: URL) { + if (url.port !== '') { + return url.port; + } + + if (url.protocol === 'http:') { + return '80'; + } + + if (url.protocol === 'https:') { + return '443'; + } +} + +export function normalizeHostsForAgents(host: string) { + // Elastic Agent is not using default port for http|https for Fleet server and ES https://github.com/elastic/beats/issues/25420 + const hostURL = new URL(host); + + // We are building the URL manualy as url format will not include the port if the port is 80 or 443 + return `${hostURL.protocol}//${hostURL.hostname}:${getPortForURL(hostURL)}${ + hostURL.pathname === '/' ? '' : hostURL.pathname + }`; +} diff --git a/x-pack/plugins/fleet/server/services/output.ts b/x-pack/plugins/fleet/server/services/output.ts index 6f043be25b67c..b3857ba5c0ef3 100644 --- a/x-pack/plugins/fleet/server/services/output.ts +++ b/x-pack/plugins/fleet/server/services/output.ts @@ -12,6 +12,7 @@ import { DEFAULT_OUTPUT, OUTPUT_SAVED_OBJECT_TYPE } from '../constants'; import { decodeCloudId } from '../../common'; import { appContextService } from './app_context'; +import { normalizeHostsForAgents } from './hosts_utils'; const SAVED_OBJECT_TYPE = OUTPUT_SAVED_OBJECT_TYPE; @@ -49,14 +50,6 @@ class OutputService { }; } - public async updateOutput( - soClient: SavedObjectsClientContract, - id: string, - data: Partial - ) { - await soClient.update(SAVED_OBJECT_TYPE, id, data); - } - public async getDefaultOutputId(soClient: SavedObjectsClientContract) { const outputs = await this.getDefaultOutput(soClient); @@ -72,9 +65,15 @@ class OutputService { output: NewOutput, options?: { id?: string } ): Promise { + const data = { ...output }; + + if (data.hosts) { + data.hosts = data.hosts.map(normalizeHostsForAgents); + } + const newSo = await soClient.create( SAVED_OBJECT_TYPE, - output as Output, + data as Output, options ); @@ -98,7 +97,13 @@ class OutputService { } public async update(soClient: SavedObjectsClientContract, id: string, data: Partial) { - const outputSO = await soClient.update(SAVED_OBJECT_TYPE, id, data); + const updateData = { ...data }; + + if (updateData.hosts) { + updateData.hosts = updateData.hosts.map(normalizeHostsForAgents); + } + + const outputSO = await soClient.update(SAVED_OBJECT_TYPE, id, updateData); if (outputSO.error) { throw new Error(outputSO.error.message); diff --git a/x-pack/plugins/fleet/server/services/settings.test.ts b/x-pack/plugins/fleet/server/services/settings.test.ts index bec0f737c0bc8..75712c471f20c 100644 --- a/x-pack/plugins/fleet/server/services/settings.test.ts +++ b/x-pack/plugins/fleet/server/services/settings.test.ts @@ -8,7 +8,7 @@ import { savedObjectsClientMock } from 'src/core/server/mocks'; import { appContextService } from './app_context'; -import { getCloudFleetServersHosts, normalizeFleetServerHost, settingsSetup } from './settings'; +import { getCloudFleetServersHosts, settingsSetup } from './settings'; jest.mock('./app_context'); @@ -205,22 +205,3 @@ describe('settingsSetup', () => { expect(soClientMock.update).not.toBeCalled(); }); }); - -describe('normalizeFleetServerHost', () => { - const scenarios = [ - { sourceUrl: 'http://test.fr', expectedUrl: 'http://test.fr:80' }, - { sourceUrl: 'http://test.fr/test/toto', expectedUrl: 'http://test.fr:80/test/toto' }, - { sourceUrl: 'https://test.fr', expectedUrl: 'https://test.fr:443' }, - { sourceUrl: 'https://test.fr/test/toto', expectedUrl: 'https://test.fr:443/test/toto' }, - { sourceUrl: 'https://test.fr:9243', expectedUrl: 'https://test.fr:9243' }, - { sourceUrl: 'https://test.fr:9243/test/toto', expectedUrl: 'https://test.fr:9243/test/toto' }, - ]; - - for (const scenario of scenarios) { - it(`should transform ${scenario.sourceUrl} correctly`, () => { - const url = normalizeFleetServerHost(scenario.sourceUrl); - - expect(url).toEqual(scenario.expectedUrl); - }); - } -}); diff --git a/x-pack/plugins/fleet/server/services/settings.ts b/x-pack/plugins/fleet/server/services/settings.ts index e493234d1cff8..226fbb29467c2 100644 --- a/x-pack/plugins/fleet/server/services/settings.ts +++ b/x-pack/plugins/fleet/server/services/settings.ts @@ -12,6 +12,7 @@ import { decodeCloudId, GLOBAL_SETTINGS_SAVED_OBJECT_TYPE } from '../../common'; import type { SettingsSOAttributes, Settings, BaseSettings } from '../../common'; import { appContextService } from './app_context'; +import { normalizeHostsForAgents } from './hosts_utils'; export async function getSettings(soClient: SavedObjectsClientContract): Promise { const res = await soClient.find({ @@ -51,37 +52,13 @@ export async function settingsSetup(soClient: SavedObjectsClientContract) { } } -function getPortForURL(url: URL) { - if (url.port !== '') { - return url.port; - } - - if (url.protocol === 'http:') { - return '80'; - } - - if (url.protocol === 'https:') { - return '443'; - } -} - -export function normalizeFleetServerHost(host: string) { - // Fleet server is not using default port for http|https https://github.com/elastic/beats/issues/25420 - const fleetServerURL = new URL(host); - - // We are building the URL manualy as url format will not include the port if the port is 80 or 443 - return `${fleetServerURL.protocol}//${fleetServerURL.hostname}:${getPortForURL(fleetServerURL)}${ - fleetServerURL.pathname === '/' ? '' : fleetServerURL.pathname - }`; -} - export async function saveSettings( soClient: SavedObjectsClientContract, newData: Partial> ): Promise & Pick> { const data = { ...newData }; if (data.fleet_server_hosts) { - data.fleet_server_hosts = data.fleet_server_hosts.map(normalizeFleetServerHost); + data.fleet_server_hosts = data.fleet_server_hosts.map(normalizeHostsForAgents); } try { diff --git a/x-pack/plugins/fleet/server/types/models/package_policy.ts b/x-pack/plugins/fleet/server/types/models/package_policy.ts index 1f39b3135cb3f..5a8fd70a9b84e 100644 --- a/x-pack/plugins/fleet/server/types/models/package_policy.ts +++ b/x-pack/plugins/fleet/server/types/models/package_policy.ts @@ -24,6 +24,7 @@ const ConfigRecordSchema = schema.recordOf( schema.object({ type: schema.maybe(schema.string()), value: schema.maybe(schema.any()), + frozen: schema.maybe(schema.boolean()), }) ); diff --git a/x-pack/plugins/infra/common/inventory_models/host/metrics/tsvb/host_system_overview.ts b/x-pack/plugins/infra/common/inventory_models/host/metrics/tsvb/host_system_overview.ts index 15febee856b17..eef480e12aab0 100644 --- a/x-pack/plugins/infra/common/inventory_models/host/metrics/tsvb/host_system_overview.ts +++ b/x-pack/plugins/infra/common/inventory_models/host/metrics/tsvb/host_system_overview.ts @@ -24,42 +24,10 @@ export const hostSystemOverview: TSVBMetricModelCreator = ( split_mode: 'everything', metrics: [ { - field: 'system.cpu.user.pct', - id: 'avg-cpu-user', + field: 'system.cpu.total.norm.pct', + id: 'avg-cpu-total', type: 'avg', }, - { - field: 'system.cpu.cores', - id: 'max-cpu-cores', - type: 'max', - }, - { - field: 'system.cpu.system.pct', - id: 'avg-cpu-system', - type: 'avg', - }, - { - id: 'calc-user-system-cores', - script: '(params.users + params.system) / params.cores', - type: 'calculation', - variables: [ - { - field: 'avg-cpu-user', - id: 'var-users', - name: 'users', - }, - { - field: 'avg-cpu-system', - id: 'var-system', - name: 'system', - }, - { - field: 'max-cpu-cores', - id: 'var-cores', - name: 'cores', - }, - ], - }, ], }, { diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/group_of_nodes.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/group_of_nodes.tsx index 751f2e1913ee3..2a3371fe81459 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/group_of_nodes.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/group_of_nodes.tsx @@ -43,18 +43,20 @@ export const GroupOfNodes: React.FC = ({ - {group.nodes.map((node) => ( - - ))} + {group.width + ? group.nodes.map((node) => ( + + )) + : null} ); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node.tsx index d6934c6846b79..4484e1281978c 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node.tsx @@ -106,26 +106,32 @@ export class Node extends React.PureComponent { - - + + {this.state.isOverlayOpen && ( + + )} + + {isAlertFlyoutVisible && ( + + )} ); } diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node_context_menu.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node_context_menu.tsx index 95b78e222a72b..6e33c270d8e7a 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node_context_menu.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node_context_menu.tsx @@ -201,17 +201,19 @@ export const NodeContextMenu: React.FC = withTheme
- + {flyoutVisible && ( + + )} ); } diff --git a/x-pack/plugins/infra/server/index.ts b/x-pack/plugins/infra/server/index.ts index b283b690d6b07..a25bba48d673e 100644 --- a/x-pack/plugins/infra/server/index.ts +++ b/x-pack/plugins/infra/server/index.ts @@ -9,6 +9,7 @@ import { PluginInitializerContext } from 'src/core/server'; import { config, InfraConfig, InfraServerPlugin, InfraPluginSetup } from './plugin'; export { config, InfraConfig, InfraPluginSetup }; +export { InfraRequestHandlerContext } from './types'; export function plugin(context: PluginInitializerContext) { return new InfraServerPlugin(context); diff --git a/x-pack/plugins/infra/server/routes/overview/lib/create_top_nodes_query.ts b/x-pack/plugins/infra/server/routes/overview/lib/create_top_nodes_query.ts index a9245a8c8ce75..7533f2801607c 100644 --- a/x-pack/plugins/infra/server/routes/overview/lib/create_top_nodes_query.ts +++ b/x-pack/plugins/infra/server/routes/overview/lib/create_top_nodes_query.ts @@ -60,7 +60,7 @@ export const createTopNodesQuery = ( }, cpu: { avg: { - field: 'system.cpu.total.pct', + field: 'system.cpu.total.norm.pct', }, }, iowait: { diff --git a/x-pack/plugins/lens/kibana.json b/x-pack/plugins/lens/kibana.json index bfcc20cc88b81..a5c19911f60b9 100644 --- a/x-pack/plugins/lens/kibana.json +++ b/x-pack/plugins/lens/kibana.json @@ -35,7 +35,6 @@ "savedObjects", "kibanaUtils", "kibanaReact", - "embeddable", - "usageCollection" + "embeddable" ] } diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx index 214ce6d11cff2..3c4412813bb83 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx @@ -25,7 +25,7 @@ import { DefaultInspectorAdapters, RenderMode } from 'src/plugins/expressions'; import { map, distinctUntilChanged, skip } from 'rxjs/operators'; import isEqual from 'fast-deep-equal'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/public'; -import { METRIC_TYPE } from '../../../../../../src/plugins/usage_collection/public'; +import { METRIC_TYPE } from '@kbn/analytics'; import { ExpressionRendererEvent, ReactExpressionRendererType, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx index 2c503a7bd6967..b74e97df4a895 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx @@ -27,7 +27,6 @@ import { getOperationDisplay, insertOrReplaceColumn, replaceColumn, - deleteColumn, updateColumnParam, resetIncomplete, FieldBasedIndexPatternColumn, @@ -422,15 +421,6 @@ export function DimensionEditor(props: DimensionEditorProps) { : (selectedColumn as FieldBasedIndexPatternColumn)?.sourceField } incompleteOperation={incompleteOperation} - onDeleteColumn={() => { - setStateWrapper( - deleteColumn({ - layer: state.layers[layerId], - columnId, - indexPattern: currentIndexPattern, - }) - ); - }} onChoose={(choice) => { setStateWrapper( insertOrReplaceColumn({ diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx index 5e79fde0fa8fa..f80b12aecabde 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx @@ -1795,7 +1795,7 @@ describe('IndexPatternDimensionEditorPanel', () => { ); }); - it('should clear the dimension when removing the selection in field combobox', () => { + it('should keep the latest valid dimension when removing the selection in field combobox', () => { wrapper = mount(); act(() => { @@ -1805,20 +1805,7 @@ describe('IndexPatternDimensionEditorPanel', () => { .prop('onChange')!([]); }); - expect(setState).toHaveBeenCalledWith( - { - ...state, - layers: { - first: { - indexPatternId: '1', - columns: {}, - columnOrder: [], - incompleteColumns: {}, - }, - }, - }, - { shouldRemoveDimension: false, shouldReplaceDimension: false } - ); + expect(setState).not.toHaveBeenCalled(); }); it('allows custom format', () => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx index ffd4ac2498133..b80d90ba78b1d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx @@ -39,7 +39,7 @@ export interface FieldSelectProps extends EuiComboBoxProps void; - onDeleteColumn: () => void; + onDeleteColumn?: () => void; existingFields: IndexPatternPrivateState['existingFields']; fieldIsInvalid: boolean; markAllFieldsCompatible?: boolean; @@ -195,7 +195,7 @@ export function FieldSelect({ singleSelection={{ asPlainText: true }} onChange={(choices) => { if (choices.length === 0) { - onDeleteColumn(); + onDeleteColumn?.(); return; } diff --git a/x-pack/plugins/lists/common/format_errors.ts b/x-pack/plugins/lists/common/format_errors.ts index 7b33612b4e887..16925699b0fcf 100644 --- a/x-pack/plugins/lists/common/format_errors.ts +++ b/x-pack/plugins/lists/common/format_errors.ts @@ -8,6 +8,9 @@ import * as t from 'io-ts'; import { isObject } from 'lodash/fp'; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils/src/format_errors/index.ts + */ export const formatErrors = (errors: t.Errors): string[] => { const err = errors.map((error) => { if (error.message != null) { diff --git a/x-pack/plugins/lists/common/schemas/common/schemas.ts b/x-pack/plugins/lists/common/schemas/common/schemas.ts index 7e43e7dd5f4ab..f223d56eb15cb 100644 --- a/x-pack/plugins/lists/common/schemas/common/schemas.ts +++ b/x-pack/plugins/lists/common/schemas/common/schemas.ts @@ -12,14 +12,44 @@ import * as t from 'io-ts'; import { DefaultNamespace } from '../types/default_namespace'; import { DefaultArray, DefaultStringArray, NonEmptyString } from '../../shared_imports'; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const name = t.string; + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type Name = t.TypeOf; + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const nameOrUndefined = t.union([name, t.undefined]); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type NameOrUndefined = t.TypeOf; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const description = t.string; + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type Description = t.TypeOf; + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const descriptionOrUndefined = t.union([description, t.undefined]); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type DescriptionOrUndefined = t.TypeOf; export const list_id = NonEmptyString; @@ -28,15 +58,47 @@ export const list_idOrUndefined = t.union([list_id, t.undefined]); export type ListIdOrUndefined = t.TypeOf; export const item = t.string; + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const created_at = t.string; // TODO: Make this into an ISO Date string check + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const updated_at = t.string; // TODO: Make this into an ISO Date string check + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const updated_by = t.string; + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const created_by = t.string; + export const file = t.object; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const id = NonEmptyString; + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type Id = t.TypeOf; + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const idOrUndefined = t.union([id, t.undefined]); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type IdOrUndefined = t.TypeOf; export const binary = t.string; @@ -93,6 +155,9 @@ export const valueOrUndefined = t.union([value, t.undefined]); export const tie_breaker_id = t.string; // TODO: Use UUID for this instead of a string for validation export const _index = t.string; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const type = t.keyof({ binary: null, boolean: null, @@ -122,9 +187,24 @@ export const type = t.keyof({ export const typeOrUndefined = t.union([type, t.undefined]); export type Type = t.TypeOf; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const meta = t.object; + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type Meta = t.TypeOf; + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const metaOrUndefined = t.union([meta, t.undefined]); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type MetaOrUndefined = t.TypeOf; export const esDataTypeRange = t.exact(t.type({ gte: t.string, lte: t.string })); @@ -207,28 +287,77 @@ export const esDataTypeUnion = t.union([ export type EsDataTypeUnion = t.TypeOf; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const tags = DefaultStringArray; + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type Tags = t.TypeOf; + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const tagsOrUndefined = t.union([tags, t.undefined]); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type TagsOrUndefined = t.TypeOf; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const exceptionListType = t.keyof({ detection: null, endpoint: null, endpoint_events: null, }); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const exceptionListTypeOrUndefined = t.union([exceptionListType, t.undefined]); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type ExceptionListType = t.TypeOf; + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type ExceptionListTypeOrUndefined = t.TypeOf; + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export enum ExceptionListTypeEnum { DETECTION = 'detection', ENDPOINT = 'endpoint', ENDPOINT_EVENTS = 'endpoint_events', } +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const exceptionListItemType = t.keyof({ simple: null }); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const exceptionListItemTypeOrUndefined = t.union([exceptionListItemType, t.undefined]); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type ExceptionListItemType = t.TypeOf; + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type ExceptionListItemTypeOrUndefined = t.TypeOf; export const list_type = t.keyof({ item: null, list: null }); @@ -275,14 +404,32 @@ export type CursorOrUndefined = t.TypeOf; export const namespace_type = DefaultNamespace; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const operatorIncluded = t.keyof({ included: null }); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const operator = t.keyof({ excluded: null, included: null }); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type Operator = t.TypeOf; + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export enum OperatorEnum { INCLUDED = 'included', EXCLUDED = 'excluded', } +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export enum OperatorTypeEnum { NESTED = 'nested', MATCH = 'match', @@ -320,15 +467,36 @@ export type Immutable = t.TypeOf; export const immutableOrUndefined = t.union([immutable, t.undefined]); export type ImmutableOrUndefined = t.TypeOf; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const osType = t.keyof({ linux: null, macos: null, windows: null, }); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type OsType = t.TypeOf; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const osTypeArray = DefaultArray(osType); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type OsTypeArray = t.TypeOf; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const osTypeArrayOrUndefined = t.union([osTypeArray, t.undefined]); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type OsTypeArrayOrUndefined = t.OutputOf; diff --git a/x-pack/plugins/lists/common/schemas/types/comment.ts b/x-pack/plugins/lists/common/schemas/types/comment.ts index eeb6688799cf8..016ef1b75edf8 100644 --- a/x-pack/plugins/lists/common/schemas/types/comment.ts +++ b/x-pack/plugins/lists/common/schemas/types/comment.ts @@ -10,6 +10,9 @@ import * as t from 'io-ts'; import { NonEmptyString } from '../../shared_imports'; import { created_at, created_by, id, updated_at, updated_by } from '../common/schemas'; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const comment = t.intersection([ t.exact( t.type({ @@ -27,8 +30,27 @@ export const comment = t.intersection([ ), ]); +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const commentsArray = t.array(comment); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type CommentsArray = t.TypeOf; + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type Comment = t.TypeOf; + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const commentsArrayOrUndefined = t.union([commentsArray, t.undefined]); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type CommentsArrayOrUndefined = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/types/create_comment.ts b/x-pack/plugins/lists/common/schemas/types/create_comment.ts index 57c2b0598d9a1..070e860299f3d 100644 --- a/x-pack/plugins/lists/common/schemas/types/create_comment.ts +++ b/x-pack/plugins/lists/common/schemas/types/create_comment.ts @@ -9,15 +9,41 @@ import * as t from 'io-ts'; import { NonEmptyString } from '../../shared_imports'; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const createComment = t.exact( t.type({ comment: NonEmptyString, }) ); +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type CreateComment = t.TypeOf; + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const createCommentsArray = t.array(createComment); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type CreateCommentsArray = t.TypeOf; + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type CreateComments = t.TypeOf; + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const createCommentsArrayOrUndefined = t.union([createCommentsArray, t.undefined]); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type CreateCommentsArrayOrUndefined = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/types/default_comments_array.ts b/x-pack/plugins/lists/common/schemas/types/default_comments_array.ts index ea00a6c07f63b..b190bfb649a9f 100644 --- a/x-pack/plugins/lists/common/schemas/types/default_comments_array.ts +++ b/x-pack/plugins/lists/common/schemas/types/default_comments_array.ts @@ -13,6 +13,7 @@ import { CommentsArray, comment } from './comment'; /** * Types the DefaultCommentsArray as: * - If null or undefined, then a default array of type entry will be set + * @deprecated Use packages/kbn-securitysolution-io-ts-utils */ export const DefaultCommentsArray = new t.Type( 'DefaultCommentsArray', diff --git a/x-pack/plugins/lists/common/schemas/types/default_create_comments_array.ts b/x-pack/plugins/lists/common/schemas/types/default_create_comments_array.ts index ed03182346a9b..92121aaf05a6b 100644 --- a/x-pack/plugins/lists/common/schemas/types/default_create_comments_array.ts +++ b/x-pack/plugins/lists/common/schemas/types/default_create_comments_array.ts @@ -13,6 +13,7 @@ import { CreateCommentsArray, createComment } from './create_comment'; /** * Types the DefaultCreateComments as: * - If null or undefined, then a default array of type entry will be set + * @deprecated Use packages/kbn-securitysolution-io-ts-utils */ export const DefaultCreateCommentsArray = new t.Type< CreateCommentsArray, diff --git a/x-pack/plugins/lists/common/schemas/types/default_namespace.ts b/x-pack/plugins/lists/common/schemas/types/default_namespace.ts index 667ddbb82253d..c5ae93b0a11a5 100644 --- a/x-pack/plugins/lists/common/schemas/types/default_namespace.ts +++ b/x-pack/plugins/lists/common/schemas/types/default_namespace.ts @@ -14,6 +14,7 @@ export type NamespaceType = t.TypeOf; /** * Types the DefaultNamespace as: * - If null or undefined, then a default string/enumeration of "single" will be used. + * @deprecated Use packages/kbn-securitysolution-io-ts-utils */ export const DefaultNamespace = new t.Type( 'DefaultNamespace', diff --git a/x-pack/plugins/lists/common/schemas/types/default_namespace_array.ts b/x-pack/plugins/lists/common/schemas/types/default_namespace_array.ts index a87c31b7d9abf..c27e4eade4b38 100644 --- a/x-pack/plugins/lists/common/schemas/types/default_namespace_array.ts +++ b/x-pack/plugins/lists/common/schemas/types/default_namespace_array.ts @@ -17,6 +17,7 @@ export type NamespaceTypeArray = t.TypeOf; * Types the DefaultNamespaceArray as: * - If null or undefined, then a default string array of "single" will be used. * - If it contains a string, then it is split along the commas and puts them into an array and validates it + * @deprecated Use packages/kbn-securitysolution-io-ts-utils */ export const DefaultNamespaceArray = new t.Type< NamespaceTypeArray, @@ -40,5 +41,12 @@ export const DefaultNamespaceArray = new t.Type< String ); +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type DefaultNamespaceArrayType = t.OutputOf; + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type DefaultNamespaceArrayTypeDecoded = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/types/default_string_boolean_false.ts b/x-pack/plugins/lists/common/schemas/types/default_string_boolean_false.ts index 02b18b079e3dd..d409648b5435b 100644 --- a/x-pack/plugins/lists/common/schemas/types/default_string_boolean_false.ts +++ b/x-pack/plugins/lists/common/schemas/types/default_string_boolean_false.ts @@ -12,6 +12,7 @@ import { Either } from 'fp-ts/lib/Either'; * Types the DefaultStringBooleanFalse as: * - If a string this will convert the string to a boolean * - If null or undefined, then a default false will be set + * @deprecated Use packages/kbn-securitysolution-io-ts-utils */ export const DefaultStringBooleanFalse = new t.Type( 'DefaultStringBooleanFalse', @@ -30,4 +31,7 @@ export const DefaultStringBooleanFalse = new t.Type [] * - Example input converted to output: null -> [] * - Example input converted to output: "a,b,c" -> ["a", "b", "c"] + * @deprecated Use packages/kbn-securitysolution-io-ts-utils */ export const EmptyStringArray = new t.Type( 'EmptyStringArray', @@ -40,5 +41,12 @@ export const EmptyStringArray = new t.Type; + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type EmptyStringArrayDecoded = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/types/endpoint/entries.ts b/x-pack/plugins/lists/common/schemas/types/endpoint/entries.ts index 393744cf1fbbf..4622a8a7d39b7 100644 --- a/x-pack/plugins/lists/common/schemas/types/endpoint/entries.ts +++ b/x-pack/plugins/lists/common/schemas/types/endpoint/entries.ts @@ -12,15 +12,22 @@ import { endpointEntryMatchAny } from './entry_match_any'; import { endpointEntryMatch } from './entry_match'; import { endpointEntryNested } from './entry_nested'; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const endpointEntriesArray = t.array( t.union([endpointEntryMatch, endpointEntryMatchAny, endpointEntryNested]) ); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type EndpointEntriesArray = t.TypeOf; /** * Types the nonEmptyEndpointEntriesArray as: * - An array of entries of length 1 or greater - * + * @deprecated Use packages/kbn-securitysolution-io-ts-utils */ export const nonEmptyEndpointEntriesArray = new t.Type< EndpointEntriesArray, @@ -39,5 +46,12 @@ export const nonEmptyEndpointEntriesArray = new t.Type< t.identity ); +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type NonEmptyEndpointEntriesArray = t.OutputOf; + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type NonEmptyEndpointEntriesArrayDecoded = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/types/endpoint/entry_match.ts b/x-pack/plugins/lists/common/schemas/types/endpoint/entry_match.ts index fc085a45a519a..e4c24aa5e3560 100644 --- a/x-pack/plugins/lists/common/schemas/types/endpoint/entry_match.ts +++ b/x-pack/plugins/lists/common/schemas/types/endpoint/entry_match.ts @@ -10,6 +10,9 @@ import * as t from 'io-ts'; import { NonEmptyString } from '../../../shared_imports'; import { operatorIncluded } from '../../common/schemas'; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const endpointEntryMatch = t.exact( t.type({ field: NonEmptyString, @@ -18,4 +21,8 @@ export const endpointEntryMatch = t.exact( value: NonEmptyString, }) ); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type EndpointEntryMatch = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/types/endpoint/entry_match_any.ts b/x-pack/plugins/lists/common/schemas/types/endpoint/entry_match_any.ts index 13559d6699e6e..ffafd0f786547 100644 --- a/x-pack/plugins/lists/common/schemas/types/endpoint/entry_match_any.ts +++ b/x-pack/plugins/lists/common/schemas/types/endpoint/entry_match_any.ts @@ -11,6 +11,9 @@ import { NonEmptyString } from '../../../shared_imports'; import { operatorIncluded } from '../../common/schemas'; import { nonEmptyOrNullableStringArray } from '../non_empty_or_nullable_string_array'; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const endpointEntryMatchAny = t.exact( t.type({ field: NonEmptyString, @@ -19,4 +22,8 @@ export const endpointEntryMatchAny = t.exact( value: nonEmptyOrNullableStringArray, }) ); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type EndpointEntryMatchAny = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/types/endpoint/entry_match_wildcard.ts b/x-pack/plugins/lists/common/schemas/types/endpoint/entry_match_wildcard.ts index dfcaa963666de..ca4894991664c 100644 --- a/x-pack/plugins/lists/common/schemas/types/endpoint/entry_match_wildcard.ts +++ b/x-pack/plugins/lists/common/schemas/types/endpoint/entry_match_wildcard.ts @@ -10,6 +10,9 @@ import * as t from 'io-ts'; import { NonEmptyString } from '../../../shared_imports'; import { operatorIncluded } from '../../common/schemas'; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const endpointEntryMatchWildcard = t.exact( t.type({ field: NonEmptyString, @@ -18,4 +21,8 @@ export const endpointEntryMatchWildcard = t.exact( value: NonEmptyString, }) ); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type EndpointEntryMatchWildcard = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/types/endpoint/entry_nested.ts b/x-pack/plugins/lists/common/schemas/types/endpoint/entry_nested.ts index 23bf0097ecef8..4304b7cd06c37 100644 --- a/x-pack/plugins/lists/common/schemas/types/endpoint/entry_nested.ts +++ b/x-pack/plugins/lists/common/schemas/types/endpoint/entry_nested.ts @@ -11,6 +11,9 @@ import { NonEmptyString } from '../../../shared_imports'; import { nonEmptyEndpointNestedEntriesArray } from './non_empty_nested_entries_array'; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const endpointEntryNested = t.exact( t.type({ entries: nonEmptyEndpointNestedEntriesArray, @@ -18,4 +21,8 @@ export const endpointEntryNested = t.exact( type: t.keyof({ nested: null }), }) ); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type EndpointEntryNested = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/types/endpoint/non_empty_nested_entries_array.ts b/x-pack/plugins/lists/common/schemas/types/endpoint/non_empty_nested_entries_array.ts index 06b41791a3669..e6bd3d61f7d78 100644 --- a/x-pack/plugins/lists/common/schemas/types/endpoint/non_empty_nested_entries_array.ts +++ b/x-pack/plugins/lists/common/schemas/types/endpoint/non_empty_nested_entries_array.ts @@ -11,15 +11,22 @@ import { Either } from 'fp-ts/lib/Either'; import { endpointEntryMatchAny } from './entry_match_any'; import { endpointEntryMatch } from './entry_match'; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const endpointNestedEntriesArray = t.array( t.union([endpointEntryMatch, endpointEntryMatchAny]) ); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type EndpointNestedEntriesArray = t.TypeOf; /** * Types the nonEmptyNestedEntriesArray as: * - An array of entries of length 1 or greater - * + * @deprecated Use packages/kbn-securitysolution-io-ts-utils */ export const nonEmptyEndpointNestedEntriesArray = new t.Type< EndpointNestedEntriesArray, @@ -38,9 +45,16 @@ export const nonEmptyEndpointNestedEntriesArray = new t.Type< t.identity ); +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type NonEmptyEndpointNestedEntriesArray = t.OutputOf< typeof nonEmptyEndpointNestedEntriesArray >; + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type NonEmptyEndpointNestedEntriesArrayDecoded = t.TypeOf< typeof nonEmptyEndpointNestedEntriesArray >; diff --git a/x-pack/plugins/lists/common/schemas/types/entries.ts b/x-pack/plugins/lists/common/schemas/types/entries.ts index 26cfed568cea8..043348070031b 100644 --- a/x-pack/plugins/lists/common/schemas/types/entries.ts +++ b/x-pack/plugins/lists/common/schemas/types/entries.ts @@ -16,6 +16,10 @@ import { entriesMatchWildcard } from './entry_match_wildcard'; // NOTE: Type nested is not included here to denote it's non-recursive nature. // So a nested entry is really just a collection of `Entry` types. + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const entry = t.union([ entriesMatch, entriesMatchAny, @@ -23,8 +27,15 @@ export const entry = t.union([ entriesExists, entriesMatchWildcard, ]); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type Entry = t.TypeOf; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const entriesArray = t.array( t.union([ entriesMatch, @@ -35,7 +46,18 @@ export const entriesArray = t.array( entriesMatchWildcard, ]) ); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type EntriesArray = t.TypeOf; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const entriesArrayOrUndefined = t.union([entriesArray, t.undefined]); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type EntriesArrayOrUndefined = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/types/entry_exists.ts b/x-pack/plugins/lists/common/schemas/types/entry_exists.ts index da456603e7851..32fb8888c88dc 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_exists.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_exists.ts @@ -10,6 +10,9 @@ import * as t from 'io-ts'; import { NonEmptyString } from '../../shared_imports'; import { operator } from '../common/schemas'; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const entriesExists = t.exact( t.type({ field: NonEmptyString, @@ -17,4 +20,8 @@ export const entriesExists = t.exact( type: t.keyof({ exists: null }), }) ); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type EntryExists = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/types/entry_list.ts b/x-pack/plugins/lists/common/schemas/types/entry_list.ts index 23cf25336836f..9b2b345244805 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_list.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_list.ts @@ -10,6 +10,9 @@ import * as t from 'io-ts'; import { NonEmptyString } from '../../shared_imports'; import { operator, type } from '../common/schemas'; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const entriesList = t.exact( t.type({ field: NonEmptyString, @@ -18,4 +21,8 @@ export const entriesList = t.exact( type: t.keyof({ list: null }), }) ); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type EntryList = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/types/entry_match.ts b/x-pack/plugins/lists/common/schemas/types/entry_match.ts index b572e550ba2cb..e3529f2d043be 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_match.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_match.ts @@ -10,6 +10,9 @@ import * as t from 'io-ts'; import { NonEmptyString } from '../../shared_imports'; import { operator } from '../common/schemas'; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const entriesMatch = t.exact( t.type({ field: NonEmptyString, @@ -18,4 +21,8 @@ export const entriesMatch = t.exact( value: NonEmptyString, }) ); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type EntryMatch = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/types/entry_match_any.ts b/x-pack/plugins/lists/common/schemas/types/entry_match_any.ts index 8f370898cc266..d4846c7949ebb 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_match_any.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_match_any.ts @@ -12,6 +12,9 @@ import { operator } from '../common/schemas'; import { nonEmptyOrNullableStringArray } from './non_empty_or_nullable_string_array'; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const entriesMatchAny = t.exact( t.type({ field: NonEmptyString, @@ -20,4 +23,8 @@ export const entriesMatchAny = t.exact( value: nonEmptyOrNullableStringArray, }) ); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type EntryMatchAny = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/types/entry_match_wildcard.ts b/x-pack/plugins/lists/common/schemas/types/entry_match_wildcard.ts index 14522256df354..1c8cb4dcdd2d8 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_match_wildcard.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_match_wildcard.ts @@ -10,6 +10,9 @@ import * as t from 'io-ts'; import { NonEmptyString } from '../../shared_imports'; import { operator } from '../common/schemas'; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const entriesMatchWildcard = t.exact( t.type({ field: NonEmptyString, @@ -18,4 +21,8 @@ export const entriesMatchWildcard = t.exact( value: NonEmptyString, }) ); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type EntryMatchWildcard = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/types/entry_nested.ts b/x-pack/plugins/lists/common/schemas/types/entry_nested.ts index 4ef3c9be61182..e0027dab7e071 100644 --- a/x-pack/plugins/lists/common/schemas/types/entry_nested.ts +++ b/x-pack/plugins/lists/common/schemas/types/entry_nested.ts @@ -11,6 +11,9 @@ import { NonEmptyString } from '../../shared_imports'; import { nonEmptyNestedEntriesArray } from './non_empty_nested_entries_array'; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const entriesNested = t.exact( t.type({ entries: nonEmptyNestedEntriesArray, @@ -18,4 +21,8 @@ export const entriesNested = t.exact( type: t.keyof({ nested: null }), }) ); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type EntryNested = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/types/non_empty_entries_array.ts b/x-pack/plugins/lists/common/schemas/types/non_empty_entries_array.ts index 8a9c5ed39e8a1..89d47c5742b08 100644 --- a/x-pack/plugins/lists/common/schemas/types/non_empty_entries_array.ts +++ b/x-pack/plugins/lists/common/schemas/types/non_empty_entries_array.ts @@ -14,7 +14,7 @@ import { entriesList } from './entry_list'; /** * Types the nonEmptyEntriesArray as: * - An array of entries of length 1 or greater - * + * @deprecated Use packages/kbn-securitysolution-io-ts-utils */ export const nonEmptyEntriesArray = new t.Type( 'NonEmptyEntriesArray', @@ -37,5 +37,12 @@ export const nonEmptyEntriesArray = new t.Type; + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type NonEmptyEntriesArrayDecoded = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/types/non_empty_nested_entries_array.ts b/x-pack/plugins/lists/common/schemas/types/non_empty_nested_entries_array.ts index 722f5dd600eec..475183695a559 100644 --- a/x-pack/plugins/lists/common/schemas/types/non_empty_nested_entries_array.ts +++ b/x-pack/plugins/lists/common/schemas/types/non_empty_nested_entries_array.ts @@ -19,7 +19,7 @@ export type NestedEntriesArray = t.TypeOf; /** * Types the nonEmptyNestedEntriesArray as: * - An array of entries of length 1 or greater - * + * @deprecated Use packages/kbn-securitysolution-io-ts-utils */ export const nonEmptyNestedEntriesArray = new t.Type< NestedEntriesArray, @@ -38,5 +38,12 @@ export const nonEmptyNestedEntriesArray = new t.Type< t.identity ); +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type NonEmptyNestedEntriesArray = t.OutputOf; + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type NonEmptyNestedEntriesArrayDecoded = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/types/non_empty_or_nullable_string_array.ts b/x-pack/plugins/lists/common/schemas/types/non_empty_or_nullable_string_array.ts index 42fbce7e45b61..ae5a24a250f3a 100644 --- a/x-pack/plugins/lists/common/schemas/types/non_empty_or_nullable_string_array.ts +++ b/x-pack/plugins/lists/common/schemas/types/non_empty_or_nullable_string_array.ts @@ -12,7 +12,7 @@ import { Either } from 'fp-ts/lib/Either'; * Types the nonEmptyOrNullableStringArray as: * - An array of non empty strings of length 1 or greater * - This differs from NonEmptyStringArray in that both input and output are type array - * + * @deprecated Use packages/kbn-securitysolution-io-ts-utils */ export const nonEmptyOrNullableStringArray = new t.Type( 'NonEmptyOrNullableStringArray', @@ -31,5 +31,12 @@ export const nonEmptyOrNullableStringArray = new t.Type; + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type NonEmptyOrNullableStringArrayDecoded = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/types/non_empty_string_array.ts b/x-pack/plugins/lists/common/schemas/types/non_empty_string_array.ts index 8fcd2c645be6c..0afb318a6b33a 100644 --- a/x-pack/plugins/lists/common/schemas/types/non_empty_string_array.ts +++ b/x-pack/plugins/lists/common/schemas/types/non_empty_string_array.ts @@ -13,6 +13,7 @@ import { Either } from 'fp-ts/lib/Either'; * - A string that is not empty (which will be turned into an array of size 1) * - A comma separated string that can turn into an array by splitting on it * - Example input converted to output: "a,b,c" -> ["a", "b", "c"] + * @deprecated Use packages/kbn-securitysolution-io-ts-utils */ export const NonEmptyStringArray = new t.Type( 'NonEmptyStringArray', @@ -36,5 +37,12 @@ export const NonEmptyStringArray = new t.Type( String ); +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type NonEmptyStringArray = t.OutputOf; + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type NonEmptyStringArrayDecoded = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/types/string_to_positive_number.ts b/x-pack/plugins/lists/common/schemas/types/string_to_positive_number.ts index c988a05d3954d..21acc88118039 100644 --- a/x-pack/plugins/lists/common/schemas/types/string_to_positive_number.ts +++ b/x-pack/plugins/lists/common/schemas/types/string_to_positive_number.ts @@ -15,6 +15,7 @@ export type StringToPositiveNumberC = t.Type; * - If a string this converts the string into a number * - Ensures it is a number (and not NaN) * - Ensures it is positive number + * @deprecated Use packages/kbn-securitysolution-io-ts-utils */ export const StringToPositiveNumber: StringToPositiveNumberC = new t.Type( 'StringToPositiveNumber', diff --git a/x-pack/plugins/lists/common/schemas/types/update_comment.ts b/x-pack/plugins/lists/common/schemas/types/update_comment.ts index a167b50ba6304..88d9d38688391 100644 --- a/x-pack/plugins/lists/common/schemas/types/update_comment.ts +++ b/x-pack/plugins/lists/common/schemas/types/update_comment.ts @@ -10,6 +10,9 @@ import * as t from 'io-ts'; import { NonEmptyString } from '../../shared_imports'; import { id } from '../common/schemas'; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const updateComment = t.intersection([ t.exact( t.type({ @@ -23,8 +26,27 @@ export const updateComment = t.intersection([ ), ]); +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type UpdateComment = t.TypeOf; + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const updateCommentsArray = t.array(updateComment); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type UpdateCommentsArray = t.TypeOf; + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const updateCommentsArrayOrUndefined = t.union([updateCommentsArray, t.undefined]); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type UpdateCommentsArrayOrUndefined = t.TypeOf; diff --git a/x-pack/plugins/lists/common/test_utils.ts b/x-pack/plugins/lists/common/test_utils.ts index 84bf0d25057a2..dcf6a2747c3de 100644 --- a/x-pack/plugins/lists/common/test_utils.ts +++ b/x-pack/plugins/lists/common/test_utils.ts @@ -16,10 +16,16 @@ interface Message { schema: T | {}; } +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils/src/test_utils/index.ts + */ const onLeft = (errors: t.Errors): Message => { return { errors, schema: {} }; }; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils/src/test_utils/index.ts + */ const onRight = (schema: T): Message => { return { errors: [], @@ -27,12 +33,16 @@ const onRight = (schema: T): Message => { }; }; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils/src/test_utils/index.ts + */ export const foldLeftRight = fold(onLeft, onRight); /** * Convenience utility to keep the error message handling within tests to be * very concise. * @param validation The validation to get the errors from + * @deprecated Use packages/kbn-securitysolution-io-ts-utils/src/test_utils/index.ts */ export const getPaths =
(validation: t.Validation): string[] => { return pipe( @@ -46,6 +56,7 @@ export const getPaths = (validation: t.Validation): string[] => { /** * Convenience utility to remove text appended to links by EUI + * @deprecated Use packages/kbn-securitysolution-io-ts-utils/src/test_utils/index.ts */ export const removeExternalLinkText = (str: string): string => str.replace(/\(opens in a new tab or window\)/g, ''); diff --git a/x-pack/plugins/lists/common/types.ts b/x-pack/plugins/lists/common/types.ts index 130e3ef1bae1a..a3cbb870faa5b 100644 --- a/x-pack/plugins/lists/common/types.ts +++ b/x-pack/plugins/lists/common/types.ts @@ -18,7 +18,7 @@ export type SavedObjectType = 'exception-list' | 'exception-list-agnostic'; * * will yield a type of: * type A = { a: undefined; b: number; } - * + * @deprecated This has no replacement. We should stop using/relying on this and just remove it. */ export type RequiredKeepUndefined = { [K in keyof T]-?: [T[K]] } extends infer U ? U extends Record diff --git a/x-pack/plugins/maps/public/embeddable/index.ts b/x-pack/plugins/maps/public/embeddable/index.ts index 97ab0f905530f..b73f9fb9de42a 100644 --- a/x-pack/plugins/maps/public/embeddable/index.ts +++ b/x-pack/plugins/maps/public/embeddable/index.ts @@ -6,4 +6,5 @@ */ export * from './map_embeddable'; +export * from './types'; export * from './map_embeddable_factory'; diff --git a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx index d11e1d59b28f7..643199dbf3933 100644 --- a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx +++ b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx @@ -78,7 +78,6 @@ import { MapEmbeddableInput, MapEmbeddableOutput, } from './types'; -export { MapEmbeddableInput, MapEmbeddableOutput }; function getIsRestore(searchSessionId?: string) { if (!searchSessionId) { diff --git a/x-pack/plugins/maps/public/index.ts b/x-pack/plugins/maps/public/index.ts index dc9cb2d594fe3..29cc20c706296 100644 --- a/x-pack/plugins/maps/public/index.ts +++ b/x-pack/plugins/maps/public/index.ts @@ -23,4 +23,6 @@ export type { RenderTooltipContentParams } from './classes/tooltips/tooltip_prop export { MapsStartApi } from './api'; -export type { MapEmbeddable, MapEmbeddableInput } from './embeddable'; +export type { MapEmbeddable, MapEmbeddableInput, MapEmbeddableOutput } from './embeddable'; + +export type { EMSTermJoinConfig, SampleValuesConfig } from './ems_autosuggest'; diff --git a/x-pack/plugins/ml/public/application/components/ml_embedded_map/ml_embedded_map.tsx b/x-pack/plugins/ml/public/application/components/ml_embedded_map/ml_embedded_map.tsx index b652312c12d14..0ca5389f1d423 100644 --- a/x-pack/plugins/ml/public/application/components/ml_embedded_map/ml_embedded_map.tsx +++ b/x-pack/plugins/ml/public/application/components/ml_embedded_map/ml_embedded_map.tsx @@ -10,13 +10,14 @@ import React, { useEffect, useRef, useState } from 'react'; import { htmlIdGenerator } from '@elastic/eui'; import { LayerDescriptor } from '../../../../../maps/common/descriptor_types'; import { INITIAL_LOCATION } from '../../../../../maps/common/constants'; -import { +import type { MapEmbeddable, MapEmbeddableInput, MapEmbeddableOutput, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../maps/public/embeddable'; -import { MAP_SAVED_OBJECT_TYPE, RenderTooltipContentParams } from '../../../../../maps/public'; + RenderTooltipContentParams, +} from '../../../../../maps/public'; +import { MAP_SAVED_OBJECT_TYPE } from '../../../../../maps/public'; + import { EmbeddableFactory, ErrorEmbeddable, diff --git a/x-pack/plugins/monitoring/server/config.test.ts b/x-pack/plugins/monitoring/server/config.test.ts index 8ea37d04c146c..45b3e07200680 100644 --- a/x-pack/plugins/monitoring/server/config.test.ts +++ b/x-pack/plugins/monitoring/server/config.test.ts @@ -32,6 +32,9 @@ describe('config schema', () => { "interval": "10s", }, "cluster_alerts": Object { + "allowedSpaces": Array [ + "default", + ], "email_notifications": Object { "email_address": "", "enabled": true, diff --git a/x-pack/plugins/monitoring/server/config.ts b/x-pack/plugins/monitoring/server/config.ts index 860c564ce3249..8c411fb5c28a8 100644 --- a/x-pack/plugins/monitoring/server/config.ts +++ b/x-pack/plugins/monitoring/server/config.ts @@ -57,6 +57,7 @@ export const configSchema = schema.object({ }), }), cluster_alerts: schema.object({ + allowedSpaces: schema.arrayOf(schema.string(), { defaultValue: ['default'] }), enabled: schema.boolean({ defaultValue: true }), email_notifications: schema.object({ enabled: schema.boolean({ defaultValue: true }), diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts index 01ab392e0563c..e48f424a3d8ee 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts @@ -8,7 +8,7 @@ // @ts-ignore import { handleError } from '../../../../lib/errors'; import { AlertsFactory } from '../../../../alerts'; -import { RouteDependencies } from '../../../../types'; +import { LegacyServer, RouteDependencies } from '../../../../types'; import { ALERT_ACTION_TYPE_LOG } from '../../../../../common/constants'; import { ActionResult } from '../../../../../../actions/common'; import { AlertingSecurity } from '../../../../lib/elasticsearch/verify_alerting_security'; @@ -17,7 +17,7 @@ import { AlertTypeParams, SanitizedAlert } from '../../../../../../alerting/comm const DEFAULT_SERVER_LOG_NAME = 'Monitoring: Write to Kibana log'; -export function enableAlertsRoute(_server: unknown, npRoute: RouteDependencies) { +export function enableAlertsRoute(server: LegacyServer, npRoute: RouteDependencies) { npRoute.router.post( { path: '/api/monitoring/v1/alerts/enable', @@ -25,6 +25,17 @@ export function enableAlertsRoute(_server: unknown, npRoute: RouteDependencies) }, async (context, request, response) => { try { + // Check to ensure the space is listed in monitoring.cluster_alerts.allowedSpaces + const config = server.config(); + const allowedSpaces = + config.get('monitoring.cluster_alerts.allowedSpaces') || ([] as string[]); + if (!allowedSpaces.includes(context.infra.spaceId)) { + server.log.info( + `Skipping alert creation for "${context.infra.spaceId}" space; add space ID to 'monitoring.cluster_alerts.allowedSpaces' in your kibana.yml` + ); + return response.ok({ body: undefined }); + } + const alerts = AlertsFactory.getAll(); if (alerts.length) { const { @@ -33,6 +44,9 @@ export function enableAlertsRoute(_server: unknown, npRoute: RouteDependencies) } = await AlertingSecurity.getSecurityHealth(context, npRoute.encryptedSavedObjects); if (!isSufficientlySecure || !hasPermanentEncryptionKey) { + server.log.info( + `Skipping alert creation for "${context.infra.spaceId}" space; Stack monitoring alerts require Transport Layer Security between Kibana and Elasticsearch, and an encryption key in your kibana.yml file.` + ); return response.ok({ body: { isSufficientlySecure, @@ -89,6 +103,10 @@ export function enableAlertsRoute(_server: unknown, npRoute: RouteDependencies) ); } + server.log.info( + `Created ${createdAlerts.length} alerts for "${context.infra.spaceId}" space` + ); + return response.ok({ body: { createdAlerts, disabledWatcherClusterAlerts } }); } catch (err) { throw handleError(err); diff --git a/x-pack/plugins/monitoring/server/types.ts b/x-pack/plugins/monitoring/server/types.ts index 799214a2931ed..3dcf6862b7232 100644 --- a/x-pack/plugins/monitoring/server/types.ts +++ b/x-pack/plugins/monitoring/server/types.ts @@ -25,7 +25,7 @@ import { PluginStartContract as AlertingPluginStartContract, PluginSetupContract as AlertingPluginSetupContract, } from '../../alerting/server'; -import { InfraPluginSetup } from '../../infra/server'; +import { InfraPluginSetup, InfraRequestHandlerContext } from '../../infra/server'; import { LicensingPluginStart } from '../../licensing/server'; import { PluginSetupContract as FeaturesPluginSetupContract } from '../../features/server'; import { EncryptedSavedObjectsPluginSetup } from '../../encrypted_saved_objects/server'; @@ -57,6 +57,7 @@ export interface PluginsSetup { export interface RequestHandlerContextMonitoringPlugin extends RequestHandlerContext { actions?: ActionsApiRequestHandlerContext; alerting?: AlertingApiRequestHandlerContext; + infra: InfraRequestHandlerContext; } export interface PluginsStart { diff --git a/x-pack/plugins/osquery/common/exact_check.ts b/x-pack/plugins/osquery/common/exact_check.ts index b10328a4db233..5334989ea085b 100644 --- a/x-pack/plugins/osquery/common/exact_check.ts +++ b/x-pack/plugins/osquery/common/exact_check.ts @@ -25,6 +25,7 @@ import { isObject, get } from 'lodash/fp'; * @param original The original to check if it has additional keys * @param decoded The decoded either which has either an existing error or the * decoded object which could have additional keys stripped from it. + * @deprecated Use packages/kbn-securitysolution-io-ts-utils/src/exact_check/index.ts */ export const exactCheck = ( original: unknown, diff --git a/x-pack/plugins/osquery/common/format_errors.ts b/x-pack/plugins/osquery/common/format_errors.ts index 7b33612b4e887..16925699b0fcf 100644 --- a/x-pack/plugins/osquery/common/format_errors.ts +++ b/x-pack/plugins/osquery/common/format_errors.ts @@ -8,6 +8,9 @@ import * as t from 'io-ts'; import { isObject } from 'lodash/fp'; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils/src/format_errors/index.ts + */ export const formatErrors = (errors: t.Errors): string[] => { const err = errors.map((error) => { if (error.message != null) { diff --git a/x-pack/plugins/osquery/common/test_utils.ts b/x-pack/plugins/osquery/common/test_utils.ts index 84bf0d25057a2..dcf6a2747c3de 100644 --- a/x-pack/plugins/osquery/common/test_utils.ts +++ b/x-pack/plugins/osquery/common/test_utils.ts @@ -16,10 +16,16 @@ interface Message { schema: T | {}; } +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils/src/test_utils/index.ts + */ const onLeft = (errors: t.Errors): Message => { return { errors, schema: {} }; }; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils/src/test_utils/index.ts + */ const onRight = (schema: T): Message => { return { errors: [], @@ -27,12 +33,16 @@ const onRight = (schema: T): Message => { }; }; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils/src/test_utils/index.ts + */ export const foldLeftRight = fold(onLeft, onRight); /** * Convenience utility to keep the error message handling within tests to be * very concise. * @param validation The validation to get the errors from + * @deprecated Use packages/kbn-securitysolution-io-ts-utils/src/test_utils/index.ts */ export const getPaths = (validation: t.Validation): string[] => { return pipe( @@ -46,6 +56,7 @@ export const getPaths = (validation: t.Validation): string[] => { /** * Convenience utility to remove text appended to links by EUI + * @deprecated Use packages/kbn-securitysolution-io-ts-utils/src/test_utils/index.ts */ export const removeExternalLinkText = (str: string): string => str.replace(/\(opens in a new tab or window\)/g, ''); diff --git a/x-pack/plugins/remote_clusters/common/lib/cluster_serialization.ts b/x-pack/plugins/remote_clusters/common/lib/cluster_serialization.ts index bf0fc11e882cc..37c06c83f87e1 100644 --- a/x-pack/plugins/remote_clusters/common/lib/cluster_serialization.ts +++ b/x-pack/plugins/remote_clusters/common/lib/cluster_serialization.ts @@ -8,13 +8,17 @@ import { PROXY_MODE } from '../constants'; // Values returned from ES GET /_remote/info +/** + * TODO: This interface needs to be updated with values from {@link RemoteInfo} provided + * by the @elastic/elasticsearch client + */ export interface ClusterInfoEs { seeds?: string[]; mode?: 'proxy' | 'sniff'; connected?: boolean; num_nodes_connected?: number; - max_connections_per_cluster?: number; - initial_connect_timeout?: string; + max_connections_per_cluster?: string | number; + initial_connect_timeout: string | number; skip_unavailable?: boolean; transport?: { ping_schedule?: string; @@ -39,8 +43,8 @@ export interface Cluster { transportPingSchedule?: string; transportCompress?: boolean; connectedNodesCount?: number; - maxConnectionsPerCluster?: number; - initialConnectTimeout?: string; + maxConnectionsPerCluster?: string | number; + initialConnectTimeout?: string | number; connectedSocketsCount?: number; hasDeprecatedProxySetting?: boolean; } diff --git a/x-pack/plugins/remote_clusters/server/lib/does_cluster_exist.ts b/x-pack/plugins/remote_clusters/server/lib/does_cluster_exist.ts index 23cbebe8ac98e..f76854bead41d 100644 --- a/x-pack/plugins/remote_clusters/server/lib/does_cluster_exist.ts +++ b/x-pack/plugins/remote_clusters/server/lib/does_cluster_exist.ts @@ -5,9 +5,14 @@ * 2.0. */ -export async function doesClusterExist(callAsCurrentUser: any, clusterName: string): Promise { +import { IScopedClusterClient } from 'src/core/server'; + +export async function doesClusterExist( + clusterClient: IScopedClusterClient, + clusterName: string +): Promise { try { - const clusterInfoByName = await callAsCurrentUser('cluster.remoteInfo'); + const { body: clusterInfoByName } = await clusterClient.asCurrentUser.cluster.remoteInfo(); return Boolean(clusterInfoByName && clusterInfoByName[clusterName]); } catch (err) { throw new Error('Unable to check if cluster already exists.'); diff --git a/x-pack/plugins/remote_clusters/server/plugin.ts b/x-pack/plugins/remote_clusters/server/plugin.ts index 2b8d9afe979e8..01cb0fa892316 100644 --- a/x-pack/plugins/remote_clusters/server/plugin.ts +++ b/x-pack/plugins/remote_clusters/server/plugin.ts @@ -19,6 +19,8 @@ import { registerDeleteRoute, } from './routes/api'; +import { handleEsError } from './shared_imports'; + export interface RemoteClustersPluginSetup { isUiEnabled: boolean; } @@ -44,6 +46,9 @@ export class RemoteClustersServerPlugin config: { isCloudEnabled: Boolean(cloud?.isCloudEnabled), }, + lib: { + handleEsError, + }, }; features.registerElasticsearchFeature({ diff --git a/x-pack/plugins/remote_clusters/server/routes/api/add_route.test.ts b/x-pack/plugins/remote_clusters/server/routes/api/add_route.test.ts index 9348fd1eb20df..b648554842a5c 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/add_route.test.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/add_route.test.ts @@ -5,10 +5,9 @@ * 2.0. */ +import { RequestHandler } from 'src/core/server'; + import { kibanaResponseFactory } from '../../../../../../src/core/server'; -import { register } from './add_route'; -import { API_BASE_PATH } from '../../../common/constants'; -import { LicenseStatus } from '../../types'; import { licensingMock } from '../../../../../plugins/licensing/server/mocks'; @@ -19,6 +18,16 @@ import { coreMock, } from '../../../../../../src/core/server/mocks'; +import { API_BASE_PATH } from '../../../common/constants'; + +import { handleEsError } from '../../shared_imports'; + +import { register } from './add_route'; + +import { ScopedClusterClientMock } from './types'; + +const { createApiResponse } = elasticsearchServiceMock; + // Re-implement the mock that was imported directly from `x-pack/mocks` function createCoreRequestHandlerContextMock() { return { @@ -30,264 +39,235 @@ function createCoreRequestHandlerContextMock() { const xpackMocks = { createRequestHandlerContext: createCoreRequestHandlerContextMock, }; -interface TestOptions { - licenseCheckResult?: LicenseStatus; - apiResponses?: Array<() => Promise>; - asserts: { statusCode: number; result?: Record; apiArguments?: unknown[][] }; - payload?: Record; -} describe('ADD remote clusters', () => { - const addRemoteClustersTest = ( - description: string, - { licenseCheckResult = { valid: true }, apiResponses = [], asserts, payload }: TestOptions - ) => { - test(description, async () => { - const elasticsearchMock = elasticsearchServiceMock.createLegacyClusterClient(); - - const mockRouteDependencies = { - router: httpServiceMock.createRouter(), - getLicenseStatus: () => licenseCheckResult, - elasticsearchService: elasticsearchServiceMock.createInternalSetup(), - elasticsearch: elasticsearchMock, - config: { - isCloudEnabled: false, - }, - }; + let handler: RequestHandler; + let mockRouteDependencies: ReturnType; + let mockContext: ReturnType; + let scopedClusterClientMock: ScopedClusterClientMock; + let remoteInfoMockFn: ScopedClusterClientMock['asCurrentUser']['cluster']['remoteInfo']; + let putSettingsMockFn: ScopedClusterClientMock['asCurrentUser']['cluster']['putSettings']; - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); + const createMockRouteDependencies = () => ({ + router: httpServiceMock.createRouter(), + getLicenseStatus: () => ({ valid: true }), + lib: { + handleEsError, + }, + config: { + isCloudEnabled: false, + }, + }); - elasticsearchServiceMock - .createLegacyClusterClient() - .asScoped.mockReturnValue(mockScopedClusterClient); + const createMockRequest = ( + body: Record = { + name: 'test', + seeds: ['127.0.0.1:9300'], + mode: 'sniff', + skipUnavailable: false, + } + ) => + httpServerMock.createKibanaRequest({ + method: 'post', + path: API_BASE_PATH, + body, + headers: { authorization: 'foo' }, + }); - for (const apiResponse of apiResponses) { - mockScopedClusterClient.callAsCurrentUser.mockImplementationOnce(apiResponse); - } + beforeEach(() => { + mockContext = xpackMocks.createRequestHandlerContext(); + scopedClusterClientMock = mockContext.core.elasticsearch.client; + remoteInfoMockFn = scopedClusterClientMock.asCurrentUser.cluster.remoteInfo; + putSettingsMockFn = scopedClusterClientMock.asCurrentUser.cluster.putSettings; + mockRouteDependencies = createMockRouteDependencies(); - register(mockRouteDependencies); - const [[{ validate }, handler]] = mockRouteDependencies.router.post.mock.calls; + register(mockRouteDependencies); + const [[, handlerFn]] = mockRouteDependencies.router.post.mock.calls; + handler = handlerFn; + }); - const mockRequest = httpServerMock.createKibanaRequest({ - method: 'post', - path: API_BASE_PATH, - body: payload !== undefined ? (validate as any).body.validate(payload) : undefined, - headers: { authorization: 'foo' }, - }); + describe('success', () => { + test(`adds remote cluster with "sniff" mode`, async () => { + remoteInfoMockFn.mockResolvedValueOnce(createApiResponse({ body: {} })); + putSettingsMockFn.mockResolvedValueOnce( + createApiResponse({ + body: { + acknowledged: true, + persistent: { + cluster: { + remote: { + test: { + connected: true, + mode: 'sniff', + seeds: ['127.0.0.1:9300'], + num_nodes_connected: 1, + max_connections_per_cluster: 3, + initial_connect_timeout: '30s', + skip_unavailable: false, + }, + }, + }, + }, + transient: {}, + }, + }) + ); - const mockContext = xpackMocks.createRequestHandlerContext(); - mockContext.core.elasticsearch.legacy.client = mockScopedClusterClient; + const mockRequest = createMockRequest(); const response = await handler(mockContext, mockRequest, kibanaResponseFactory); - expect(response.status).toBe(asserts.statusCode); - expect(response.payload).toEqual(asserts.result); + expect(response.status).toBe(200); + expect(response.payload).toEqual({ acknowledged: true }); - if (Array.isArray(asserts.apiArguments)) { - for (const apiArguments of asserts.apiArguments) { - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith(...apiArguments); - } - } else { - expect(mockScopedClusterClient.callAsCurrentUser).not.toHaveBeenCalled(); - } - }); - }; - - describe('success', () => { - addRemoteClustersTest(`adds remote cluster with "sniff" mode`, { - apiResponses: [ - async () => ({}), - async () => ({ - acknowledged: true, + expect(remoteInfoMockFn).toHaveBeenCalledWith(); + expect(putSettingsMockFn).toHaveBeenCalledWith({ + body: { persistent: { cluster: { remote: { test: { - connected: true, - mode: 'sniff', seeds: ['127.0.0.1:9300'], - num_nodes_connected: 1, - max_connections_per_cluster: 3, - initial_connect_timeout: '30s', skip_unavailable: false, + mode: 'sniff', + node_connections: null, + proxy_address: null, + proxy_socket_connections: null, + server_name: null, }, }, }, }, - transient: {}, - }), - ], - payload: { - name: 'test', - seeds: ['127.0.0.1:9300'], - mode: 'sniff', - skipUnavailable: false, - }, - asserts: { - apiArguments: [ - ['cluster.remoteInfo'], - [ - 'cluster.putSettings', - { - body: { - persistent: { - cluster: { - remote: { - test: { - seeds: ['127.0.0.1:9300'], - skip_unavailable: false, - mode: 'sniff', - node_connections: null, - proxy_address: null, - proxy_socket_connections: null, - server_name: null, - }, - }, - }, - }, - }, - }, - ], - ], - statusCode: 200, - result: { - acknowledged: true, }, - }, + }); }); - addRemoteClustersTest(`adds remote cluster with "proxy" mode`, { - apiResponses: [ - async () => ({}), - async () => ({ - acknowledged: true, - persistent: { - cluster: { - remote: { - test: { - connected: true, - mode: 'proxy', - seeds: ['127.0.0.1:9300'], - num_sockets_connected: 1, - max_socket_connections: 18, - initial_connect_timeout: '30s', - skip_unavailable: false, + + test(`adds remote cluster with "proxy" mode`, async () => { + remoteInfoMockFn.mockResolvedValueOnce(createApiResponse({ body: {} })); + putSettingsMockFn.mockResolvedValueOnce( + createApiResponse({ + body: { + acknowledged: true, + persistent: { + cluster: { + remote: { + test: { + connected: true, + mode: 'sniff', + seeds: ['127.0.0.1:9300'], + num_nodes_connected: 1, + max_connections_per_cluster: 3, + initial_connect_timeout: '30s', + skip_unavailable: false, + }, }, }, }, + transient: {}, }, - transient: {}, - }), - ], - payload: { + }) + ); + + const mockRequest = createMockRequest({ name: 'test', proxyAddress: '127.0.0.1:9300', mode: 'proxy', skipUnavailable: false, serverName: 'foobar', - }, - asserts: { - apiArguments: [ - ['cluster.remoteInfo'], - [ - 'cluster.putSettings', - { - body: { - persistent: { - cluster: { - remote: { - test: { - seeds: null, - skip_unavailable: false, - mode: 'proxy', - node_connections: null, - proxy_address: '127.0.0.1:9300', - proxy_socket_connections: null, - server_name: 'foobar', - }, - }, - }, + }); + + const response = await handler(mockContext, mockRequest, kibanaResponseFactory); + + expect(response.status).toBe(200); + expect(response.payload).toEqual({ acknowledged: true }); + + expect(remoteInfoMockFn).toHaveBeenCalledWith(); + expect(putSettingsMockFn).toHaveBeenCalledWith({ + body: { + persistent: { + cluster: { + remote: { + test: { + seeds: null, + skip_unavailable: false, + mode: 'proxy', + node_connections: null, + proxy_address: '127.0.0.1:9300', + proxy_socket_connections: null, + server_name: 'foobar', }, }, }, - ], - ], - statusCode: 200, - result: { - acknowledged: true, + }, }, - }, + }); }); }); describe('failure', () => { - addRemoteClustersTest('returns 409 if remote cluster already exists', { - apiResponses: [ - async () => ({ - test: { - connected: true, - mode: 'sniff', - seeds: ['127.0.0.1:9300'], - num_nodes_connected: 1, - max_connections_per_cluster: 3, - initial_connect_timeout: '30s', - skip_unavailable: false, + test('returns 409 if remote cluster already exists', async () => { + remoteInfoMockFn.mockResolvedValueOnce( + createApiResponse({ + body: { + test: { + connected: true, + mode: 'sniff', + seeds: ['127.0.0.1:9300'], + num_nodes_connected: 1, + max_connections_per_cluster: 3, + initial_connect_timeout: '30s', + skip_unavailable: false, + }, }, - }), - ], - payload: { - name: 'test', - seeds: ['127.0.0.1:9300'], - skipUnavailable: false, - mode: 'sniff', - }, - asserts: { - apiArguments: [['cluster.remoteInfo']], - statusCode: 409, - result: { - message: 'There is already a remote cluster with that name.', - }, - }, + }) + ); + + const mockRequest = createMockRequest(); + + const response = await handler(mockContext, mockRequest, kibanaResponseFactory); + + expect(response.status).toBe(409); + expect(response.payload).toEqual({ + message: 'There is already a remote cluster with that name.', + }); + + expect(remoteInfoMockFn).toHaveBeenCalledWith(); + expect(putSettingsMockFn).not.toHaveBeenCalled(); }); - addRemoteClustersTest('returns 400 ES did not acknowledge remote cluster', { - apiResponses: [async () => ({}), async () => ({})], - payload: { - name: 'test', - seeds: ['127.0.0.1:9300'], - skipUnavailable: false, - mode: 'sniff', - }, - asserts: { - apiArguments: [ - ['cluster.remoteInfo'], - [ - 'cluster.putSettings', - { - body: { - persistent: { - cluster: { - remote: { - test: { - seeds: ['127.0.0.1:9300'], - skip_unavailable: false, - mode: 'sniff', - node_connections: null, - proxy_address: null, - proxy_socket_connections: null, - server_name: null, - }, - }, - }, + test('returns 400 ES did not acknowledge remote cluster', async () => { + remoteInfoMockFn.mockResolvedValueOnce(createApiResponse({ body: {} })); + putSettingsMockFn.mockResolvedValueOnce(createApiResponse({ body: {} as any })); + + const mockRequest = createMockRequest(); + + const response = await handler(mockContext, mockRequest, kibanaResponseFactory); + + expect(response.status).toBe(400); + expect(response.payload).toEqual({ + message: 'Unable to add cluster, no response returned from ES.', + }); + + expect(remoteInfoMockFn).toHaveBeenCalledWith(); + expect(putSettingsMockFn).toHaveBeenCalledWith({ + body: { + persistent: { + cluster: { + remote: { + test: { + seeds: ['127.0.0.1:9300'], + skip_unavailable: false, + mode: 'sniff', + node_connections: null, + proxy_address: null, + proxy_socket_connections: null, + server_name: null, }, }, }, - ], - ], - statusCode: 400, - result: { - message: 'Unable to add cluster, no response returned from ES.', + }, }, - }, + }); }); }); }); diff --git a/x-pack/plugins/remote_clusters/server/routes/api/add_route.ts b/x-pack/plugins/remote_clusters/server/routes/api/add_route.ts index 685aee16dc665..3b6ba618c8975 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/add_route.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/add_route.ts @@ -14,7 +14,6 @@ import { serializeCluster, Cluster } from '../../../common/lib'; import { doesClusterExist } from '../../lib/does_cluster_exist'; import { API_BASE_PATH, PROXY_MODE, SNIFF_MODE } from '../../../common/constants'; import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory'; -import { isEsError } from '../../shared_imports'; import { RouteDependencies } from '../../types'; const bodyValidation = schema.object({ @@ -31,18 +30,23 @@ const bodyValidation = schema.object({ type RouteBody = TypeOf; export const register = (deps: RouteDependencies): void => { + const { + router, + lib: { handleEsError }, + } = deps; + const addHandler: RequestHandler = async ( ctx, request, response ) => { try { - const callAsCurrentUser = ctx.core.elasticsearch.legacy.client.callAsCurrentUser; + const { client: clusterClient } = ctx.core.elasticsearch; const { name } = request.body; // Check if cluster already exists. - const existingCluster = await doesClusterExist(callAsCurrentUser, name); + const existingCluster = await doesClusterExist(clusterClient, name); if (existingCluster) { return response.conflict({ body: { @@ -57,9 +61,11 @@ export const register = (deps: RouteDependencies): void => { } const addClusterPayload = serializeCluster(request.body as Cluster); - const updateClusterResponse = await callAsCurrentUser('cluster.putSettings', { - body: addClusterPayload, - }); + const { body: updateClusterResponse } = await clusterClient.asCurrentUser.cluster.putSettings( + { + body: addClusterPayload, + } + ); const acknowledged = get(updateClusterResponse, 'acknowledged'); const cluster = get(updateClusterResponse, `persistent.cluster.remote.${name}`); @@ -85,13 +91,10 @@ export const register = (deps: RouteDependencies): void => { }, }); } catch (error) { - if (isEsError(error)) { - return response.customError({ statusCode: error.statusCode, body: error }); - } - throw error; + return handleEsError({ error, response }); } }; - deps.router.post( + router.post( { path: API_BASE_PATH, validate: { diff --git a/x-pack/plugins/remote_clusters/server/routes/api/delete_route.test.ts b/x-pack/plugins/remote_clusters/server/routes/api/delete_route.test.ts index ce94f45bb8443..d83472fc564b0 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/delete_route.test.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/delete_route.test.ts @@ -5,10 +5,9 @@ * 2.0. */ -import { kibanaResponseFactory } from '../../../../../../src/core/server'; +import { kibanaResponseFactory, RequestHandler } from '../../../../../../src/core/server'; import { register } from './delete_route'; import { API_BASE_PATH } from '../../../common/constants'; -import { LicenseStatus } from '../../types'; import { licensingMock } from '../../../../../plugins/licensing/server/mocks'; @@ -19,6 +18,12 @@ import { coreMock, } from '../../../../../../src/core/server/mocks'; +import { handleEsError } from '../../shared_imports'; + +import { ScopedClusterClientMock } from './types'; + +const { createApiResponse } = elasticsearchServiceMock; + // Re-implement the mock that was imported directly from `x-pack/mocks` function createCoreRequestHandlerContextMock() { return { @@ -30,192 +35,193 @@ function createCoreRequestHandlerContextMock() { const xpackMocks = { createRequestHandlerContext: createCoreRequestHandlerContextMock, }; -interface TestOptions { - licenseCheckResult?: LicenseStatus; - apiResponses?: Array<() => Promise>; - asserts: { statusCode: number; result?: Record; apiArguments?: unknown[][] }; - params: { - nameOrNames: string; - }; -} describe('DELETE remote clusters', () => { - const deleteRemoteClustersTest = ( - description: string, - { licenseCheckResult = { valid: true }, apiResponses = [], asserts, params }: TestOptions - ) => { - test(description, async () => { - const elasticsearchMock = elasticsearchServiceMock.createLegacyClusterClient(); + let handler: RequestHandler; + let mockRouteDependencies: ReturnType; + let mockContext: ReturnType; + let scopedClusterClientMock: ScopedClusterClientMock; - const mockRouteDependencies = { - router: httpServiceMock.createRouter(), - getLicenseStatus: () => licenseCheckResult, - elasticsearchService: elasticsearchServiceMock.createInternalSetup(), - elasticsearch: elasticsearchMock, - config: { - isCloudEnabled: false, - }, - }; + let remoteInfoMockFn: ScopedClusterClientMock['asCurrentUser']['cluster']['remoteInfo']; + let getSettingsMockFn: ScopedClusterClientMock['asCurrentUser']['cluster']['getSettings']; + let putSettingsMockFn: ScopedClusterClientMock['asCurrentUser']['cluster']['putSettings']; - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); + const createMockRequest = ( + body: Record = { + name: 'test', + proxyAddress: '127.0.0.1:9300', + mode: 'proxy', + skipUnavailable: false, + serverName: 'foobar', + } + ) => + httpServerMock.createKibanaRequest({ + method: 'delete', + path: `${API_BASE_PATH}/{nameOrNames}`, + params: { + nameOrNames: 'test', + }, + body, + headers: { authorization: 'foo' }, + }); - elasticsearchServiceMock - .createLegacyClusterClient() - .asScoped.mockReturnValue(mockScopedClusterClient); + const createMockRouteDependencies = () => ({ + router: httpServiceMock.createRouter(), + getLicenseStatus: () => ({ valid: true }), + lib: { + handleEsError, + }, + config: { + isCloudEnabled: false, + }, + }); - for (const apiResponse of apiResponses) { - mockScopedClusterClient.callAsCurrentUser.mockImplementationOnce(apiResponse); - } + beforeEach(() => { + mockContext = xpackMocks.createRequestHandlerContext(); + scopedClusterClientMock = mockContext.core.elasticsearch.client; + remoteInfoMockFn = scopedClusterClientMock.asCurrentUser.cluster.remoteInfo; + getSettingsMockFn = scopedClusterClientMock.asCurrentUser.cluster.getSettings; + putSettingsMockFn = scopedClusterClientMock.asCurrentUser.cluster.putSettings; + mockRouteDependencies = createMockRouteDependencies(); - register(mockRouteDependencies); - const [[{ validate }, handler]] = mockRouteDependencies.router.delete.mock.calls; + register(mockRouteDependencies); + const [[, handlerFn]] = mockRouteDependencies.router.delete.mock.calls; + handler = handlerFn; + }); - const mockRequest = httpServerMock.createKibanaRequest({ - method: 'delete', - path: `${API_BASE_PATH}/{nameOrNames}`, - params: (validate as any).params.validate(params), - headers: { authorization: 'foo' }, - }); + describe('success', () => { + test('deletes remote cluster', async () => { + getSettingsMockFn.mockResolvedValueOnce( + createApiResponse({ + body: { + persistent: { + cluster: { + remote: { + test: { + seeds: ['127.0.0.1:9300'], + skip_unavailable: false, + mode: 'sniff', + }, + }, + }, + }, + transient: {}, + }, + }) + ); + remoteInfoMockFn.mockResolvedValueOnce( + createApiResponse({ + body: { + test: { + connected: true, + mode: 'sniff', + seeds: ['127.0.0.1:9300'], + num_nodes_connected: 1, + max_connections_per_cluster: 3, + initial_connect_timeout: '30s', + skip_unavailable: false, + }, + }, + }) + ); + putSettingsMockFn.mockResolvedValueOnce( + createApiResponse({ + body: { + acknowledged: true, + persistent: {}, + transient: {}, + }, + }) + ); - const mockContext = xpackMocks.createRequestHandlerContext(); - mockContext.core.elasticsearch.legacy.client = mockScopedClusterClient; - const response = await handler(mockContext, mockRequest, kibanaResponseFactory); + const mockRequest = createMockRequest(); - expect(response.status).toBe(asserts.statusCode); - expect(response.payload).toEqual(asserts.result); + const response = await handler(mockContext, mockRequest, kibanaResponseFactory); - if (Array.isArray(asserts.apiArguments)) { - for (const apiArguments of asserts.apiArguments) { - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith(...apiArguments); - } - } else { - expect(mockScopedClusterClient.callAsCurrentUser).not.toHaveBeenCalled(); - } - }); - }; + expect(response.status).toBe(200); + expect(response.payload).toEqual({ + itemsDeleted: ['test'], + errors: [], + }); - describe('success', () => { - deleteRemoteClustersTest('deletes remote cluster', { - apiResponses: [ - async () => ({ + expect(getSettingsMockFn).toHaveBeenCalledWith(); + expect(remoteInfoMockFn).toHaveBeenCalledWith(); + expect(putSettingsMockFn).toHaveBeenCalledWith({ + body: { persistent: { cluster: { remote: { test: { - seeds: ['127.0.0.1:9300'], - skip_unavailable: false, - mode: 'sniff', + seeds: null, + skip_unavailable: null, + mode: null, + proxy_address: null, + proxy_socket_connections: null, + server_name: null, + node_connections: null, }, }, }, }, - transient: {}, - }), - async () => ({ - test: { - connected: true, - mode: 'sniff', - seeds: ['127.0.0.1:9300'], - num_nodes_connected: 1, - max_connections_per_cluster: 3, - initial_connect_timeout: '30s', - skip_unavailable: false, - }, - }), - async () => ({ - acknowledged: true, - persistent: {}, - transient: {}, - }), - ], - params: { - nameOrNames: 'test', - }, - asserts: { - apiArguments: [ - ['cluster.remoteInfo'], - [ - 'cluster.putSettings', - { - body: { - persistent: { - cluster: { - remote: { - test: { - seeds: null, - skip_unavailable: null, - mode: null, - proxy_address: null, - proxy_socket_connections: null, - server_name: null, - node_connections: null, - }, - }, - }, - }, - }, - }, - ], - ], - statusCode: 200, - result: { - itemsDeleted: ['test'], - errors: [], }, - }, + }); }); }); describe('failure', () => { - deleteRemoteClustersTest( - 'returns errors array with 404 error if remote cluster does not exist', - { - apiResponses: [ - async () => ({ + test('returns errors array with 404 error if remote cluster does not exist', async () => { + getSettingsMockFn.mockResolvedValueOnce( + createApiResponse({ + body: { persistent: { cluster: { remote: {}, }, }, transient: {}, - }), - async () => ({}), - ], - params: { - nameOrNames: 'test', - }, - asserts: { - apiArguments: [['cluster.remoteInfo']], - statusCode: 200, - result: { - errors: [ - { - error: { - options: { - body: { - message: 'There is no remote cluster with that name.', - }, - statusCode: 404, - }, - payload: { - message: 'There is no remote cluster with that name.', - }, - status: 404, + }, + }) + ); + remoteInfoMockFn.mockResolvedValueOnce( + createApiResponse({ + body: {}, + }) + ); + + const mockRequest = createMockRequest(); + + const response = await handler(mockContext, mockRequest, kibanaResponseFactory); + + expect(response.status).toBe(200); + expect(response.payload).toEqual({ + errors: [ + { + error: { + options: { + body: { + message: 'There is no remote cluster with that name.', }, - name: 'test', + statusCode: 404, + }, + payload: { + message: 'There is no remote cluster with that name.', }, - ], - itemsDeleted: [], + status: 404, + }, + name: 'test', }, - }, - } - ); + ], + itemsDeleted: [], + }); - deleteRemoteClustersTest( - 'returns errors array with 400 error if ES still returns cluster information', - { - apiResponses: [ - async () => ({ + expect(getSettingsMockFn).toHaveBeenCalledWith(); + expect(remoteInfoMockFn).toHaveBeenCalledWith(); + }); + + test('returns errors array with 400 error if ES still returns cluster information', async () => { + getSettingsMockFn.mockResolvedValueOnce( + createApiResponse({ + body: { persistent: { cluster: { remote: { @@ -228,8 +234,12 @@ describe('DELETE remote clusters', () => { }, }, transient: {}, - }), - async () => ({ + }, + }) + ); + remoteInfoMockFn.mockResolvedValueOnce( + createApiResponse({ + body: { test: { connected: true, mode: 'sniff', @@ -239,8 +249,13 @@ describe('DELETE remote clusters', () => { initial_connect_timeout: '30s', skip_unavailable: false, }, - }), - async () => ({ + }, + }) + ); + + putSettingsMockFn.mockResolvedValueOnce( + createApiResponse({ + body: { acknowledged: true, persistent: { cluster: { @@ -258,60 +273,57 @@ describe('DELETE remote clusters', () => { }, }, transient: {}, - }), - ], - params: { - nameOrNames: 'test', - }, - asserts: { - apiArguments: [ - ['cluster.remoteInfo'], - [ - 'cluster.putSettings', - { + }, + }) + ); + + const mockRequest = createMockRequest(); + + const response = await handler(mockContext, mockRequest, kibanaResponseFactory); + + expect(response.status).toBe(200); + expect(response.payload).toEqual({ + errors: [ + { + error: { + options: { body: { - persistent: { - cluster: { - remote: { - test: { - seeds: null, - skip_unavailable: null, - mode: null, - node_connections: null, - proxy_address: null, - proxy_socket_connections: null, - server_name: null, - }, - }, - }, - }, + message: 'Unable to delete cluster, information still returned from ES.', }, + statusCode: 400, }, - ], - ], - statusCode: 200, - result: { - errors: [ - { - error: { - options: { - body: { - message: 'Unable to delete cluster, information still returned from ES.', - }, - statusCode: 400, - }, - payload: { - message: 'Unable to delete cluster, information still returned from ES.', - }, - status: 400, + payload: { + message: 'Unable to delete cluster, information still returned from ES.', + }, + status: 400, + }, + name: 'test', + }, + ], + itemsDeleted: [], + }); + + expect(getSettingsMockFn).toHaveBeenCalledWith(); + expect(remoteInfoMockFn).toHaveBeenCalledWith(); + expect(putSettingsMockFn).toHaveBeenCalledWith({ + body: { + persistent: { + cluster: { + remote: { + test: { + seeds: null, + skip_unavailable: null, + mode: null, + node_connections: null, + proxy_address: null, + proxy_socket_connections: null, + server_name: null, }, - name: 'test', }, - ], - itemsDeleted: [], + }, }, }, - } - ); + }); + }); }); }); diff --git a/x-pack/plugins/remote_clusters/server/routes/api/delete_route.ts b/x-pack/plugins/remote_clusters/server/routes/api/delete_route.ts index 89df5255d19e2..18aa9154fba42 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/delete_route.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/delete_route.ts @@ -15,7 +15,6 @@ import { serializeCluster } from '../../../common/lib'; import { API_BASE_PATH } from '../../../common/constants'; import { doesClusterExist } from '../../lib/does_cluster_exist'; import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory'; -import { isEsError } from '../../shared_imports'; const paramsValidation = schema.object({ nameOrNames: schema.string(), @@ -24,13 +23,18 @@ const paramsValidation = schema.object({ type RouteParams = TypeOf; export const register = (deps: RouteDependencies): void => { + const { + router, + lib: { handleEsError }, + } = deps; + const deleteHandler: RequestHandler = async ( ctx, request, response ) => { try { - const callAsCurrentUser = ctx.core.elasticsearch.legacy.client.callAsCurrentUser; + const { client: clusterClient } = ctx.core.elasticsearch; const { nameOrNames } = request.params; const names = nameOrNames.split(','); @@ -38,12 +42,12 @@ export const register = (deps: RouteDependencies): void => { const itemsDeleted: any[] = []; const errors: any[] = []; - const clusterSettings = await callAsCurrentUser('cluster.getSettings'); + const { body: clusterSettings } = await clusterClient.asCurrentUser.cluster.getSettings(); // Validator that returns an error if the remote cluster does not exist. const validateClusterDoesExist = async (name: string) => { try { - const existingCluster = await doesClusterExist(callAsCurrentUser, name); + const existingCluster = await doesClusterExist(clusterClient, name); if (!existingCluster) { return response.customError({ statusCode: 404, @@ -69,7 +73,12 @@ export const register = (deps: RouteDependencies): void => { ) => { try { const body = serializeCluster({ name, hasDeprecatedProxySetting }); - const updateClusterResponse = await callAsCurrentUser('cluster.putSettings', { body }); + + const { + body: updateClusterResponse, + } = await clusterClient.asCurrentUser.cluster.putSettings({ + body, + }); const acknowledged = get(updateClusterResponse, 'acknowledged'); const cluster = get(updateClusterResponse, `persistent.cluster.remote.${name}`); @@ -92,10 +101,7 @@ export const register = (deps: RouteDependencies): void => { }, }); } catch (error) { - if (isEsError(error)) { - return response.customError({ statusCode: error.statusCode, body: error }); - } - throw error; + return handleEsError({ error, response }); } }; @@ -129,14 +135,11 @@ export const register = (deps: RouteDependencies): void => { }, }); } catch (error) { - if (isEsError(error)) { - return response.customError({ statusCode: error.statusCode, body: error }); - } - throw error; + return handleEsError({ error, response }); } }; - deps.router.delete( + router.delete( { path: `${API_BASE_PATH}/{nameOrNames}`, validate: { diff --git a/x-pack/plugins/remote_clusters/server/routes/api/get_route.test.ts b/x-pack/plugins/remote_clusters/server/routes/api/get_route.test.ts index cfec01da943ab..b4c8d6d304b12 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/get_route.test.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/get_route.test.ts @@ -5,14 +5,9 @@ * 2.0. */ -import Boom from '@hapi/boom'; +import { errors } from '@elastic/elasticsearch'; -import { kibanaResponseFactory } from '../../../../../../src/core/server'; -import { register } from './get_route'; -import { API_BASE_PATH } from '../../../common/constants'; -import { LicenseStatus } from '../../types'; - -import { licensingMock } from '../../../../../plugins/licensing/server/mocks'; +import { RequestHandler } from 'src/core/server'; import { elasticsearchServiceMock, @@ -21,6 +16,18 @@ import { coreMock, } from '../../../../../../src/core/server/mocks'; +import { kibanaResponseFactory } from '../../../../../../src/core/server'; +import { licensingMock } from '../../../../../plugins/licensing/server/mocks'; + +import { API_BASE_PATH } from '../../../common/constants'; + +import { handleEsError } from '../../shared_imports'; + +import { register } from './get_route'; +import { ScopedClusterClientMock } from './types'; + +const { createApiResponse } = elasticsearchServiceMock; + // Re-implement the mock that was imported directly from `x-pack/mocks` function createCoreRequestHandlerContextMock() { return { @@ -32,182 +39,183 @@ function createCoreRequestHandlerContextMock() { const xpackMocks = { createRequestHandlerContext: createCoreRequestHandlerContextMock, }; -interface TestOptions { - licenseCheckResult?: LicenseStatus; - apiResponses?: Array<() => Promise>; - asserts: { statusCode: number; result?: Record; apiArguments?: unknown[][] }; -} - describe('GET remote clusters', () => { - const getRemoteClustersTest = ( - description: string, - { licenseCheckResult = { valid: true }, apiResponses = [], asserts }: TestOptions - ) => { - test(description, async () => { - const elasticsearchMock = elasticsearchServiceMock.createLegacyClusterClient(); - - const mockRouteDependencies = { - router: httpServiceMock.createRouter(), - getLicenseStatus: () => licenseCheckResult, - elasticsearchService: elasticsearchServiceMock.createInternalSetup(), - elasticsearch: elasticsearchMock, - config: { - isCloudEnabled: false, - }, - }; - - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - - elasticsearchServiceMock - .createLegacyClusterClient() - .asScoped.mockReturnValue(mockScopedClusterClient); - - for (const apiResponse of apiResponses) { - mockScopedClusterClient.callAsCurrentUser.mockImplementationOnce(apiResponse); - } + let handler: RequestHandler; + let mockRouteDependencies: ReturnType; + let mockContext: ReturnType; + let scopedClusterClientMock: ScopedClusterClientMock; + + let remoteInfoMockFn: ScopedClusterClientMock['asCurrentUser']['cluster']['remoteInfo']; + let getSettingsMockFn: ScopedClusterClientMock['asCurrentUser']['cluster']['getSettings']; + + const createMockRequest = () => + httpServerMock.createKibanaRequest({ + method: 'get', + path: API_BASE_PATH, + headers: { authorization: 'foo' }, + }); - register(mockRouteDependencies); - const [[, handler]] = mockRouteDependencies.router.get.mock.calls; + const createMockRouteDependencies = () => ({ + router: httpServiceMock.createRouter(), + getLicenseStatus: () => ({ valid: true }), + lib: { + handleEsError, + }, + config: { + isCloudEnabled: false, + }, + }); - const mockRequest = httpServerMock.createKibanaRequest({ - method: 'get', - path: API_BASE_PATH, - headers: { authorization: 'foo' }, - }); + beforeEach(() => { + mockContext = xpackMocks.createRequestHandlerContext(); + scopedClusterClientMock = mockContext.core.elasticsearch.client; + remoteInfoMockFn = scopedClusterClientMock.asCurrentUser.cluster.remoteInfo; + getSettingsMockFn = scopedClusterClientMock.asCurrentUser.cluster.getSettings; + mockRouteDependencies = createMockRouteDependencies(); - const mockContext = xpackMocks.createRequestHandlerContext(); - mockContext.core.elasticsearch.legacy.client = mockScopedClusterClient; - - if (asserts.statusCode === 500) { - await expect(handler(mockContext, mockRequest, kibanaResponseFactory)).rejects.toThrowError( - asserts.result as Error - ); - } else { - const response = await handler(mockContext, mockRequest, kibanaResponseFactory); - - expect(response.status).toBe(asserts.statusCode); - expect(response.payload).toEqual(asserts.result); - } - - if (Array.isArray(asserts.apiArguments)) { - for (const apiArguments of asserts.apiArguments) { - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith(...apiArguments); - } - } else { - expect(mockScopedClusterClient.callAsCurrentUser).not.toHaveBeenCalled(); - } - }); - }; + register(mockRouteDependencies); + const [[, handlerFn]] = mockRouteDependencies.router.get.mock.calls; + handler = handlerFn; + }); describe('success', () => { - getRemoteClustersTest('returns remote clusters', { - apiResponses: [ - async () => ({ - persistent: { - cluster: { - remote: { - test: { - seeds: ['127.0.0.1:9300'], - skip_unavailable: false, - mode: 'sniff', + test('returns remote clusters', async () => { + getSettingsMockFn.mockResolvedValueOnce( + createApiResponse({ + body: { + persistent: { + cluster: { + remote: { + test: { + seeds: ['127.0.0.1:9300'], + skip_unavailable: false, + mode: 'sniff', + }, }, }, }, + transient: {}, }, - transient: {}, - }), - async () => ({ - test: { - connected: true, - mode: 'sniff', - seeds: ['127.0.0.1:9300'], - num_nodes_connected: 1, - max_connections_per_cluster: 3, - initial_connect_timeout: '30s', - skip_unavailable: false, - }, - }), - ], - asserts: { - apiArguments: [['cluster.getSettings'], ['cluster.remoteInfo']], - statusCode: 200, - result: [ - { - name: 'test', - seeds: ['127.0.0.1:9300'], - isConnected: true, - connectedNodesCount: 1, - maxConnectionsPerCluster: 3, - initialConnectTimeout: '30s', - skipUnavailable: false, - isConfiguredByNode: false, - mode: 'sniff', + }) + ); + remoteInfoMockFn.mockResolvedValueOnce( + createApiResponse({ + body: { + test: { + connected: true, + mode: 'sniff', + seeds: ['127.0.0.1:9300'], + num_nodes_connected: 1, + max_connections_per_cluster: 3, + initial_connect_timeout: '30s', + skip_unavailable: false, + }, }, - ], - }, + }) + ); + + const mockRequest = createMockRequest(); + + const response = await handler(mockContext, mockRequest, kibanaResponseFactory); + + expect(response.status).toBe(200); + expect(response.payload).toEqual([ + { + name: 'test', + seeds: ['127.0.0.1:9300'], + isConnected: true, + connectedNodesCount: 1, + maxConnectionsPerCluster: 3, + initialConnectTimeout: '30s', + skipUnavailable: false, + isConfiguredByNode: false, + mode: 'sniff', + }, + ]); + + expect(getSettingsMockFn).toHaveBeenCalledWith(); + expect(remoteInfoMockFn).toHaveBeenCalledWith(); }); - getRemoteClustersTest('returns an empty array when ES responds with an empty object', { - apiResponses: [async () => ({}), async () => ({})], - asserts: { - apiArguments: [['cluster.getSettings'], ['cluster.remoteInfo']], - statusCode: 200, - result: [], - }, + + test('returns an empty array when ES responds with an empty object', async () => { + getSettingsMockFn.mockResolvedValueOnce(createApiResponse({ body: {} as any })); + remoteInfoMockFn.mockResolvedValueOnce(createApiResponse({ body: {} })); + + const mockRequest = createMockRequest(); + + const response = await handler(mockContext, mockRequest, kibanaResponseFactory); + + expect(response.status).toBe(200); + expect(response.payload).toEqual([]); + + expect(getSettingsMockFn).toHaveBeenCalledWith(); + expect(remoteInfoMockFn).toHaveBeenCalledWith(); }); }); describe('failure', () => { - const error = Boom.notAcceptable('test error'); + const error = new errors.ResponseError({ + body: { message: 'test error' }, + statusCode: 406, + headers: {}, + meta: {} as any, + warnings: [], + }); + + test('returns an error if failure to get cluster settings', async () => { + getSettingsMockFn.mockRejectedValueOnce(error); + + const mockRequest = createMockRequest(); + + const response = await handler(mockContext, mockRequest, kibanaResponseFactory); - getRemoteClustersTest('returns an error if failure to get cluster settings', { - apiResponses: [ - async () => { - throw error; + expect(response.status).toBe(406); + expect(response.payload).toEqual({ + attributes: { + causes: undefined, + error: undefined, }, - async () => ({ - test: { - connected: true, - mode: 'sniff', - seeds: ['127.0.0.1:9300'], - num_nodes_connected: 1, - max_connections_per_cluster: 3, - initial_connect_timeout: '30s', - skip_unavailable: false, - }, - }), - ], - asserts: { - apiArguments: [['cluster.getSettings']], - statusCode: 500, - result: error, - }, + message: 'Response Error', + }); + + expect(getSettingsMockFn).toHaveBeenCalled(); + expect(remoteInfoMockFn).not.toHaveBeenCalled(); }); - getRemoteClustersTest('returns an error if failure to get cluster remote info', { - apiResponses: [ - async () => ({ - persistent: { - cluster: { - remote: { - test: { - seeds: ['127.0.0.1:9300'], - skip_unavailable: false, - mode: 'sniff', + test('returns an error if failure to get cluster remote info', async () => { + getSettingsMockFn.mockResolvedValueOnce( + createApiResponse({ + body: { + persistent: { + cluster: { + remote: { + test: { + seeds: ['127.0.0.1:9300'], + skip_unavailable: false, + mode: 'sniff', + }, }, }, }, + transient: {}, }, - transient: {}, - }), - async () => { - throw error; - }, - ], - asserts: { - apiArguments: [['cluster.getSettings'], ['cluster.remoteInfo']], - statusCode: 500, - result: error, - }, + }) + ); + + remoteInfoMockFn.mockRejectedValueOnce(error); + + const mockRequest = httpServerMock.createKibanaRequest({ + method: 'get', + path: API_BASE_PATH, + headers: { authorization: 'foo' }, + }); + + const response = await handler(mockContext, mockRequest, kibanaResponseFactory); + + expect(response.status).toBe(406); + + expect(getSettingsMockFn).toHaveBeenCalledWith(); + expect(remoteInfoMockFn).toHaveBeenCalledWith(); }); }); }); diff --git a/x-pack/plugins/remote_clusters/server/routes/api/get_route.ts b/x-pack/plugins/remote_clusters/server/routes/api/get_route.ts index fbb345203e48a..c98f523450686 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/get_route.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/get_route.ts @@ -11,14 +11,18 @@ import { RequestHandler } from 'src/core/server'; import { deserializeCluster } from '../../../common/lib'; import { API_BASE_PATH } from '../../../common/constants'; import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory'; -import { isEsError } from '../../shared_imports'; import { RouteDependencies } from '../../types'; export const register = (deps: RouteDependencies): void => { + const { + router, + lib: { handleEsError }, + } = deps; + const allHandler: RequestHandler = async (ctx, request, response) => { try { - const callAsCurrentUser = await ctx.core.elasticsearch.legacy.client.callAsCurrentUser; - const clusterSettings = await callAsCurrentUser('cluster.getSettings'); + const { client: clusterClient } = ctx.core.elasticsearch; + const { body: clusterSettings } = await clusterClient.asCurrentUser.cluster.getSettings(); const transientClusterNames = Object.keys( get(clusterSettings, 'transient.cluster.remote') || {} @@ -27,7 +31,7 @@ export const register = (deps: RouteDependencies): void => { get(clusterSettings, 'persistent.cluster.remote') || {} ); - const clustersByName = await callAsCurrentUser('cluster.remoteInfo'); + const { body: clustersByName } = await clusterClient.asCurrentUser.cluster.remoteInfo(); const clusterNames = (clustersByName && Object.keys(clustersByName)) || []; const body = clusterNames.map((clusterName: string): any => { @@ -60,14 +64,11 @@ export const register = (deps: RouteDependencies): void => { return response.ok({ body }); } catch (error) { - if (isEsError(error)) { - return response.customError({ statusCode: error.statusCode, body: error }); - } - throw error; + return handleEsError({ error, response }); } }; - deps.router.get( + router.get( { path: API_BASE_PATH, validate: false, diff --git a/x-pack/plugins/remote_clusters/server/routes/api/types.ts b/x-pack/plugins/remote_clusters/server/routes/api/types.ts new file mode 100644 index 0000000000000..5a2fd20ba4d35 --- /dev/null +++ b/x-pack/plugins/remote_clusters/server/routes/api/types.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { elasticsearchServiceMock } from '../../../../../../src/core/server/mocks'; + +export type ScopedClusterClientMock = ReturnType< + typeof elasticsearchServiceMock.createScopedClusterClient +>; diff --git a/x-pack/plugins/remote_clusters/server/routes/api/update_route.test.ts b/x-pack/plugins/remote_clusters/server/routes/api/update_route.test.ts index 22c87786a585c..129326dea95ec 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/update_route.test.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/update_route.test.ts @@ -4,13 +4,8 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import { RequestHandler } from 'src/core/server'; import { kibanaResponseFactory } from '../../../../../../src/core/server'; -import { register } from './update_route'; -import { API_BASE_PATH } from '../../../common/constants'; -import { LicenseStatus } from '../../types'; - -import { licensingMock } from '../../../../../plugins/licensing/server/mocks'; import { elasticsearchServiceMock, @@ -19,6 +14,17 @@ import { coreMock, } from '../../../../../../src/core/server/mocks'; +import { licensingMock } from '../../../../../plugins/licensing/server/mocks'; + +import { API_BASE_PATH } from '../../../common/constants'; + +import { handleEsError } from '../../shared_imports'; + +import { register } from './update_route'; +import { ScopedClusterClientMock } from './types'; + +const { createApiResponse } = elasticsearchServiceMock; + // Re-implement the mock that was imported directly from `x-pack/mocks` function createCoreRequestHandlerContextMock() { return { @@ -30,315 +36,296 @@ function createCoreRequestHandlerContextMock() { const xpackMocks = { createRequestHandlerContext: createCoreRequestHandlerContextMock, }; -interface TestOptions { - licenseCheckResult?: LicenseStatus; - apiResponses?: Array<() => Promise>; - asserts: { statusCode: number; result?: Record; apiArguments?: unknown[][] }; - payload?: Record; - params: { - name: string; - }; -} describe('UPDATE remote clusters', () => { - const updateRemoteClustersTest = ( - description: string, - { - licenseCheckResult = { valid: true }, - apiResponses = [], - asserts, - payload, - params, - }: TestOptions - ) => { - test(description, async () => { - const elasticsearchMock = elasticsearchServiceMock.createLegacyClusterClient(); - - const mockRouteDependencies = { - router: httpServiceMock.createRouter(), - getLicenseStatus: () => licenseCheckResult, - elasticsearchService: elasticsearchServiceMock.createInternalSetup(), - elasticsearch: elasticsearchMock, - config: { - isCloudEnabled: false, - }, - }; + let handler: RequestHandler; + let mockRouteDependencies: ReturnType; + let mockContext: ReturnType; + let scopedClusterClientMock: ScopedClusterClientMock; - const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); + let remoteInfoMockFn: ScopedClusterClientMock['asCurrentUser']['cluster']['remoteInfo']; + let putSettingsMockFn: ScopedClusterClientMock['asCurrentUser']['cluster']['putSettings']; - elasticsearchServiceMock - .createLegacyClusterClient() - .asScoped.mockReturnValue(mockScopedClusterClient); + const createMockRequest = ( + body: Record = { seeds: ['127.0.0.1:9300'], skipUnavailable: true, mode: 'sniff' } + ) => + httpServerMock.createKibanaRequest({ + method: 'put', + path: `${API_BASE_PATH}/{name}`, + params: { + name: 'test', + }, + body, + headers: { authorization: 'foo' }, + }); - for (const apiResponse of apiResponses) { - mockScopedClusterClient.callAsCurrentUser.mockImplementationOnce(apiResponse); - } + const createMockRouteDependencies = () => ({ + router: httpServiceMock.createRouter(), + getLicenseStatus: () => ({ valid: true }), + lib: { + handleEsError, + }, + config: { + isCloudEnabled: false, + }, + }); - register(mockRouteDependencies); - const [[{ validate }, handler]] = mockRouteDependencies.router.put.mock.calls; + beforeEach(() => { + mockContext = xpackMocks.createRequestHandlerContext(); + scopedClusterClientMock = mockContext.core.elasticsearch.client; + remoteInfoMockFn = scopedClusterClientMock.asCurrentUser.cluster.remoteInfo; + putSettingsMockFn = scopedClusterClientMock.asCurrentUser.cluster.putSettings; + mockRouteDependencies = createMockRouteDependencies(); - const mockRequest = httpServerMock.createKibanaRequest({ - method: 'put', - path: `${API_BASE_PATH}/{name}`, - params: (validate as any).params.validate(params), - body: payload !== undefined ? (validate as any).body.validate(payload) : undefined, - headers: { authorization: 'foo' }, - }); + register(mockRouteDependencies); + const [[, handlerFn]] = mockRouteDependencies.router.put.mock.calls; + handler = handlerFn; + }); - const mockContext = xpackMocks.createRequestHandlerContext(); - mockContext.core.elasticsearch.legacy.client = mockScopedClusterClient; - const response = await handler(mockContext, mockRequest, kibanaResponseFactory); + describe('success', () => { + test('updates remote cluster', async () => { + remoteInfoMockFn.mockResolvedValueOnce( + createApiResponse({ + body: { + test: { + connected: true, + mode: 'sniff', + seeds: ['127.0.0.1:9300'], + num_nodes_connected: 1, + max_connections_per_cluster: 3, + initial_connect_timeout: '30s', + skip_unavailable: false, + }, + }, + }) + ); + putSettingsMockFn.mockResolvedValueOnce( + createApiResponse({ + body: { + acknowledged: true, + persistent: { + cluster: { + remote: { + test: { + connected: true, + mode: 'sniff', + seeds: ['127.0.0.1:9300'], + num_nodes_connected: 1, + max_connections_per_cluster: 3, + initial_connect_timeout: '30s', + skip_unavailable: true, + }, + }, + }, + }, + transient: {}, + }, + }) + ); - expect(response.status).toBe(asserts.statusCode); - expect(response.payload).toEqual(asserts.result); + const mockRequest = createMockRequest(); - if (Array.isArray(asserts.apiArguments)) { - for (const apiArguments of asserts.apiArguments) { - expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith(...apiArguments); - } - } else { - expect(mockScopedClusterClient.callAsCurrentUser).not.toHaveBeenCalled(); - } - }); - }; + const response = await handler(mockContext, mockRequest, kibanaResponseFactory); - describe('success', () => { - updateRemoteClustersTest('updates remote cluster', { - apiResponses: [ - async () => ({ - test: { - connected: true, - mode: 'sniff', - seeds: ['127.0.0.1:9300'], - num_nodes_connected: 1, - max_connections_per_cluster: 3, - initial_connect_timeout: '30s', - skip_unavailable: false, - }, - }), - async () => ({ - acknowledged: true, + expect(response.status).toBe(200); + expect(response.payload).toEqual({ + connectedNodesCount: 1, + initialConnectTimeout: '30s', + isConfiguredByNode: false, + isConnected: true, + maxConnectionsPerCluster: 3, + name: 'test', + seeds: ['127.0.0.1:9300'], + skipUnavailable: true, + mode: 'sniff', + }); + + expect(remoteInfoMockFn).toHaveBeenCalledWith(); + expect(putSettingsMockFn).toHaveBeenCalledWith({ + body: { persistent: { cluster: { remote: { test: { - connected: true, - mode: 'sniff', seeds: ['127.0.0.1:9300'], - num_nodes_connected: 1, - max_connections_per_cluster: 3, - initial_connect_timeout: '30s', skip_unavailable: true, + mode: 'sniff', + node_connections: null, + proxy_address: null, + proxy_socket_connections: null, + server_name: null, }, }, }, }, - transient: {}, - }), - ], - params: { - name: 'test', - }, - payload: { - seeds: ['127.0.0.1:9300'], - skipUnavailable: true, - mode: 'sniff', - }, - asserts: { - apiArguments: [ - ['cluster.remoteInfo'], - [ - 'cluster.putSettings', - { - body: { - persistent: { - cluster: { - remote: { - test: { - seeds: ['127.0.0.1:9300'], - skip_unavailable: true, - mode: 'sniff', - node_connections: null, - proxy_address: null, - proxy_socket_connections: null, - server_name: null, - }, - }, + }, + }); + }); + + test('updates v1 proxy cluster', async () => { + remoteInfoMockFn.mockResolvedValueOnce( + createApiResponse({ + body: { + test: { + connected: true, + initial_connect_timeout: '30s', + skip_unavailable: false, + seeds: ['127.0.0.1:9300'], + max_connections_per_cluster: 20, + num_nodes_connected: 1, + }, + }, + }) + ); + putSettingsMockFn.mockResolvedValueOnce( + createApiResponse({ + body: { + acknowledged: true, + persistent: { + cluster: { + remote: { + test: { + connected: true, + proxy_address: '127.0.0.1:9300', + initial_connect_timeout: '30s', + skip_unavailable: true, + mode: 'proxy', + proxy_socket_connections: 18, }, }, }, }, - ], - ], - statusCode: 200, - result: { - connectedNodesCount: 1, - initialConnectTimeout: '30s', - isConfiguredByNode: false, - isConnected: true, - maxConnectionsPerCluster: 3, - name: 'test', - seeds: ['127.0.0.1:9300'], - skipUnavailable: true, - mode: 'sniff', - }, - }, - }); - updateRemoteClustersTest('updates v1 proxy cluster', { - apiResponses: [ - async () => ({ - test: { - connected: true, - initial_connect_timeout: '30s', - skip_unavailable: false, - seeds: ['127.0.0.1:9300'], + transient: {}, }, - }), - async () => ({ - acknowledged: true, + }) + ); + + const mockRequest = createMockRequest({ + proxyAddress: '127.0.0.1:9300', + skipUnavailable: true, + mode: 'proxy', + hasDeprecatedProxySetting: true, + serverName: '', + proxySocketConnections: 18, + }); + + const response = await handler(mockContext, mockRequest, kibanaResponseFactory); + + expect(response.status).toBe(200); + expect(response.payload).toEqual({ + initialConnectTimeout: '30s', + isConfiguredByNode: false, + isConnected: true, + proxyAddress: '127.0.0.1:9300', + name: 'test', + skipUnavailable: true, + mode: 'proxy', + }); + + expect(remoteInfoMockFn).toHaveBeenCalledWith(); + expect(putSettingsMockFn).toHaveBeenCalledWith({ + body: { persistent: { cluster: { remote: { test: { - connected: true, proxy_address: '127.0.0.1:9300', - initial_connect_timeout: '30s', skip_unavailable: true, mode: 'proxy', + node_connections: null, + seeds: null, proxy_socket_connections: 18, + server_name: null, + proxy: null, }, }, }, }, - transient: {}, - }), - ], - params: { - name: 'test', - }, - payload: { - proxyAddress: '127.0.0.1:9300', - skipUnavailable: true, - mode: 'proxy', - hasDeprecatedProxySetting: true, - serverName: '', - proxySocketConnections: 18, - }, - asserts: { - apiArguments: [ - ['cluster.remoteInfo'], - [ - 'cluster.putSettings', - { - body: { - persistent: { - cluster: { - remote: { - test: { - proxy_address: '127.0.0.1:9300', - skip_unavailable: true, - mode: 'proxy', - node_connections: null, - seeds: null, - proxy_socket_connections: 18, - server_name: null, - proxy: null, - }, - }, - }, - }, - }, - }, - ], - ], - statusCode: 200, - result: { - initialConnectTimeout: '30s', - isConfiguredByNode: false, - isConnected: true, - proxyAddress: '127.0.0.1:9300', - name: 'test', - skipUnavailable: true, - mode: 'proxy', }, - }, + }); }); }); describe('failure', () => { - updateRemoteClustersTest('returns 404 if remote cluster does not exist', { - apiResponses: [async () => ({})], - payload: { + test('returns 404 if remote cluster does not exist', async () => { + remoteInfoMockFn.mockResolvedValueOnce( + createApiResponse({ + body: {} as any, + }) + ); + + const mockRequest = createMockRequest({ seeds: ['127.0.0.1:9300'], skipUnavailable: false, mode: 'sniff', - }, - params: { - name: 'test', - }, - asserts: { - apiArguments: [['cluster.remoteInfo']], - statusCode: 404, - result: { - message: 'There is no remote cluster with that name.', - }, - }, + }); + + const response = await handler(mockContext, mockRequest, kibanaResponseFactory); + + expect(response.status).toBe(404); + expect(response.payload).toEqual({ + message: 'There is no remote cluster with that name.', + }); + + expect(remoteInfoMockFn).toHaveBeenCalledWith(); + expect(putSettingsMockFn).not.toHaveBeenCalled(); }); - updateRemoteClustersTest('returns 400 if ES did not acknowledge remote cluster', { - apiResponses: [ - async () => ({ - test: { - connected: true, - mode: 'sniff', - seeds: ['127.0.0.1:9300'], - num_nodes_connected: 1, - max_connections_per_cluster: 3, - initial_connect_timeout: '30s', - skip_unavailable: false, + test('returns 400 if ES did not acknowledge remote cluster', async () => { + remoteInfoMockFn.mockResolvedValueOnce( + createApiResponse({ + body: { + test: { + connected: true, + mode: 'sniff', + seeds: ['127.0.0.1:9300'], + num_nodes_connected: 1, + max_connections_per_cluster: 3, + initial_connect_timeout: '30s', + skip_unavailable: false, + }, }, - }), - async () => ({}), - ], - payload: { + }) + ); + putSettingsMockFn.mockResolvedValueOnce( + createApiResponse({ + body: {} as any, + }) + ); + + const mockRequest = createMockRequest({ seeds: ['127.0.0.1:9300'], skipUnavailable: false, mode: 'sniff', - }, - params: { - name: 'test', - }, - asserts: { - apiArguments: [ - ['cluster.remoteInfo'], - [ - 'cluster.putSettings', - { - body: { - persistent: { - cluster: { - remote: { - test: { - seeds: ['127.0.0.1:9300'], - skip_unavailable: false, - mode: 'sniff', - node_connections: null, - proxy_address: null, - proxy_socket_connections: null, - server_name: null, - }, - }, - }, + }); + + const response = await handler(mockContext, mockRequest, kibanaResponseFactory); + + expect(response.status).toBe(400); + expect(response.payload).toEqual({ + message: 'Unable to edit cluster, no response returned from ES.', + }); + + expect(remoteInfoMockFn).toHaveBeenCalledWith(); + expect(putSettingsMockFn).toHaveBeenCalledWith({ + body: { + persistent: { + cluster: { + remote: { + test: { + seeds: ['127.0.0.1:9300'], + skip_unavailable: false, + mode: 'sniff', + node_connections: null, + proxy_address: null, + proxy_socket_connections: null, + server_name: null, }, }, }, - ], - ], - statusCode: 400, - result: { - message: 'Unable to edit cluster, no response returned from ES.', + }, }, - }, + }); }); }); }); diff --git a/x-pack/plugins/remote_clusters/server/routes/api/update_route.ts b/x-pack/plugins/remote_clusters/server/routes/api/update_route.ts index 99fb7dd01adb1..efe259b40ec25 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/update_route.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/update_route.ts @@ -15,7 +15,6 @@ import { serializeCluster, deserializeCluster, Cluster, ClusterInfoEs } from '.. import { doesClusterExist } from '../../lib/does_cluster_exist'; import { RouteDependencies } from '../../types'; import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory'; -import { isEsError } from '../../shared_imports'; const bodyValidation = schema.object({ skipUnavailable: schema.boolean(), @@ -37,18 +36,23 @@ type RouteParams = TypeOf; type RouteBody = TypeOf; export const register = (deps: RouteDependencies): void => { + const { + router, + lib: { handleEsError }, + } = deps; + const updateHandler: RequestHandler = async ( ctx, request, response ) => { try { - const callAsCurrentUser = ctx.core.elasticsearch.legacy.client.callAsCurrentUser; + const { client: clusterClient } = ctx.core.elasticsearch; const { name } = request.params; // Check if cluster does exist. - const existingCluster = await doesClusterExist(callAsCurrentUser, name); + const existingCluster = await doesClusterExist(clusterClient, name); if (!existingCluster) { return response.notFound({ body: { @@ -65,9 +69,11 @@ export const register = (deps: RouteDependencies): void => { // Update cluster as new settings const updateClusterPayload = serializeCluster({ ...request.body, name } as Cluster); - const updateClusterResponse = await callAsCurrentUser('cluster.putSettings', { - body: updateClusterPayload, - }); + const { body: updateClusterResponse } = await clusterClient.asCurrentUser.cluster.putSettings( + { + body: updateClusterPayload, + } + ); const acknowledged = get(updateClusterResponse, 'acknowledged'); const cluster = get( @@ -97,14 +103,11 @@ export const register = (deps: RouteDependencies): void => { }, }); } catch (error) { - if (isEsError(error)) { - return response.customError({ statusCode: error.statusCode, body: error }); - } - throw error; + return handleEsError({ error, response }); } }; - deps.router.put( + router.put( { path: `${API_BASE_PATH}/{name}`, validate: { diff --git a/x-pack/plugins/remote_clusters/server/shared_imports.ts b/x-pack/plugins/remote_clusters/server/shared_imports.ts index df9b3dd53cc1f..7f55d189457c7 100644 --- a/x-pack/plugins/remote_clusters/server/shared_imports.ts +++ b/x-pack/plugins/remote_clusters/server/shared_imports.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { isEsError } from '../../../../src/plugins/es_ui_shared/server'; +export { handleEsError } from '../../../../src/plugins/es_ui_shared/server'; diff --git a/x-pack/plugins/remote_clusters/server/types.ts b/x-pack/plugins/remote_clusters/server/types.ts index 00b73d614ec96..89d1ebfb5cf4e 100644 --- a/x-pack/plugins/remote_clusters/server/types.ts +++ b/x-pack/plugins/remote_clusters/server/types.ts @@ -6,10 +6,13 @@ */ import { IRouter } from 'kibana/server'; + import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { LicensingPluginSetup } from '../../licensing/server'; import { CloudSetup } from '../../cloud/server'; +import { handleEsError } from './shared_imports'; + export interface Dependencies { licensing: LicensingPluginSetup; cloud: CloudSetup; @@ -22,6 +25,9 @@ export interface RouteDependencies { config: { isCloudEnabled: boolean; }; + lib: { + handleEsError: typeof handleEsError; + }; } export interface LicenseStatus { diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts index b5f88aa144814..e085d500bee0a 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts @@ -76,14 +76,25 @@ export type Filters = t.TypeOf; // Filters are not easily type-a export const filtersOrUndefined = t.union([filters, t.undefined]); export type FiltersOrUndefined = t.TypeOf; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const saved_object_attribute_single: t.Type = t.recursion( 'saved_object_attribute_single', () => t.union([t.string, t.number, t.boolean, t.null, t.undefined, saved_object_attributes]) ); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const saved_object_attribute: t.Type = t.recursion( 'saved_object_attribute', () => t.union([saved_object_attribute_single, t.array(saved_object_attribute_single)]) ); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const saved_object_attributes: t.Type = t.recursion( 'saved_object_attributes', () => t.record(t.string, saved_object_attribute) @@ -172,10 +183,24 @@ export type Query = t.TypeOf; export const queryOrUndefined = t.union([query, t.undefined]); export type QueryOrUndefined = t.TypeOf; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const language = t.keyof({ eql: null, kuery: null, lucene: null }); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type Language = t.TypeOf; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const languageOrUndefined = t.union([language, t.undefined]); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type LanguageOrUndefined = t.TypeOf; export const license = t.string; @@ -246,7 +271,14 @@ export type Meta = t.TypeOf; export const metaOrUndefined = t.union([meta, t.undefined]); export type MetaOrUndefined = t.TypeOf; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const max_signals = PositiveIntegerGreaterThanZero; + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type MaxSignals = t.TypeOf; export const maxSignalsOrUndefined = t.union([max_signals, t.undefined]); @@ -258,10 +290,21 @@ export type Name = t.TypeOf; export const nameOrUndefined = t.union([name, t.undefined]); export type NameOrUndefined = t.TypeOf; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const operator = t.keyof({ equals: null, }); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type Operator = t.TypeOf; + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export enum OperatorEnum { EQUALS = 'equals', } @@ -272,8 +315,19 @@ export type RiskScore = t.TypeOf; export const riskScoreOrUndefined = t.union([risk_score, t.undefined]); export type RiskScoreOrUndefined = t.TypeOf; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const risk_score_mapping_field = t.string; + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const risk_score_mapping_value = t.string; + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const risk_score_mapping_item = t.exact( t.type({ field: risk_score_mapping_field, @@ -283,7 +337,14 @@ export const risk_score_mapping_item = t.exact( }) ); +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const risk_score_mapping = t.array(risk_score_mapping_item); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type RiskScoreMapping = t.TypeOf; export const riskScoreMappingOrUndefined = t.union([risk_score_mapping, t.undefined]); @@ -295,14 +356,39 @@ export type RuleNameOverride = t.TypeOf; export const ruleNameOverrideOrUndefined = t.union([rule_name_override, t.undefined]); export type RuleNameOverrideOrUndefined = t.TypeOf; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const severity = t.keyof({ low: null, medium: null, high: null, critical: null }); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type Severity = t.TypeOf; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const severityOrUndefined = t.union([severity, t.undefined]); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type SeverityOrUndefined = t.TypeOf; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const severity_mapping_field = t.string; + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const severity_mapping_value = t.string; + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const severity_mapping_item = t.exact( t.type({ field: severity_mapping_field, @@ -311,12 +397,30 @@ export const severity_mapping_item = t.exact( severity, }) ); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type SeverityMappingItem = t.TypeOf; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const severity_mapping = t.array(severity_mapping_item); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type SeverityMapping = t.TypeOf; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const severityMappingOrUndefined = t.union([severity_mapping, t.undefined]); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type SeverityMappingOrUndefined = t.TypeOf; export const status = t.keyof({ open: null, closed: null, 'in-progress': null }); @@ -407,29 +511,92 @@ export type Fields = t.TypeOf; export const fieldsOrUndefined = t.union([fields, t.undefined]); export type FieldsOrUndefined = t.TypeOf; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const threat_framework = t.string; + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const threat_tactic_id = t.string; + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const threat_tactic_name = t.string; + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const threat_tactic_reference = t.string; + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const threat_tactic = t.type({ id: threat_tactic_id, name: threat_tactic_name, reference: threat_tactic_reference, }); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type ThreatTactic = t.TypeOf; + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const threat_subtechnique_id = t.string; + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const threat_subtechnique_name = t.string; + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const threat_subtechnique_reference = t.string; + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const threat_subtechnique = t.type({ id: threat_subtechnique_id, name: threat_subtechnique_name, reference: threat_subtechnique_reference, }); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type ThreatSubtechnique = t.TypeOf; + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const threat_subtechniques = t.array(threat_subtechnique); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const threat_technique_id = t.string; + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const threat_technique_name = t.string; + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const threat_technique_reference = t.string; + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const threat_technique = t.intersection([ t.exact( t.type({ @@ -444,8 +611,20 @@ export const threat_technique = t.intersection([ }) ), ]); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type ThreatTechnique = t.TypeOf; + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const threat_techniques = t.array(threat_technique); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const threat = t.intersection([ t.exact( t.type({ @@ -459,9 +638,20 @@ export const threat = t.intersection([ }) ), ]); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type Threat = t.TypeOf; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const threats = t.array(threat); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type Threats = t.TypeOf; export const threatsOrUndefined = t.union([threats, t.undefined]); @@ -517,16 +707,38 @@ export type ThresholdNormalized = t.TypeOf; export const thresholdNormalizedOrUndefined = t.union([thresholdNormalized, t.undefined]); export type ThresholdNormalizedOrUndefined = t.TypeOf; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const created_at = IsoDateString; + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const updated_at = IsoDateString; + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const updated_by = t.string; + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const created_by = t.string; export const updatedByOrNull = t.union([updated_by, t.null]); export type UpdatedByOrNull = t.TypeOf; export const createdByOrNull = t.union([created_by, t.null]); export type CreatedByOrNull = t.TypeOf; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const version = PositiveIntegerGreaterThanZero; + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type Version = t.TypeOf; export const versionOrUndefined = t.union([version, t.undefined]); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_actions_array.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_actions_array.ts index 6d7c3d82d428a..49302102ddee9 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_actions_array.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_actions_array.ts @@ -12,6 +12,7 @@ import { actions, Actions } from '../common/schemas'; /** * Types the DefaultStringArray as: * - If undefined, then a default action array will be set + * @deprecated Use packages/kbn-securitysolution-io-ts-utils */ export const DefaultActionsArray = new t.Type( 'DefaultActionsArray', diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_array.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_array.ts index 9bd987c6d0c83..a6397754b23d7 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_array.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_array.ts @@ -12,6 +12,7 @@ import { Either } from 'fp-ts/lib/Either'; * Types the DefaultArray as: * - If undefined, then a default array will be set * - If an array is sent in, then the array will be validated to ensure all elements are type C + * @deprecated Use packages/kbn-securitysolution-io-ts-utils */ export const DefaultArray = (codec: C) => { const arrType = t.array(codec); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_boolean_false.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_boolean_false.ts index 7d094f749e17d..46e7bde55638a 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_boolean_false.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_boolean_false.ts @@ -11,6 +11,7 @@ import { Either } from 'fp-ts/lib/Either'; /** * Types the DefaultBooleanFalse as: * - If null or undefined, then a default false will be set + * @deprecated Use packages/kbn-securitysolution-io-ts-utils */ export const DefaultBooleanFalse = new t.Type( 'DefaultBooleanFalse', diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_boolean_true.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_boolean_true.ts index 68654083a745f..3845150841c22 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_boolean_true.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_boolean_true.ts @@ -11,6 +11,7 @@ import { Either } from 'fp-ts/lib/Either'; /** * Types the DefaultBooleanTrue as: * - If null or undefined, then a default true will be set + * @deprecated Use packages/kbn-securitysolution-io-ts-utils */ export const DefaultBooleanTrue = new t.Type( 'DefaultBooleanTrue', diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_empty_string.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_empty_string.ts index 4f561ee2f810e..77a06a928a2e3 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_empty_string.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_empty_string.ts @@ -11,6 +11,7 @@ import { Either } from 'fp-ts/lib/Either'; /** * Types the DefaultEmptyString as: * - If null or undefined, then a default of an empty string "" will be used + * @deprecated Use packages/kbn-securitysolution-io-ts-utils */ export const DefaultEmptyString = new t.Type( 'DefaultEmptyString', diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_export_file_name.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_export_file_name.ts index 53f0972baca21..a2b2fb6eaad31 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_export_file_name.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_export_file_name.ts @@ -11,6 +11,7 @@ import { Either } from 'fp-ts/lib/Either'; /** * Types the DefaultExportFileName as: * - If null or undefined, then a default of "export.ndjson" will be used + * @deprecated Use packages/kbn-securitysolution-io-ts-utils */ export const DefaultExportFileName = new t.Type( 'DefaultExportFileName', diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_from_string.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_from_string.ts index 23892d32127d4..a2eb2a2b9f46f 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_from_string.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_from_string.ts @@ -11,6 +11,7 @@ import { from } from '../common/schemas'; /** * Types the DefaultFromString as: * - If null or undefined, then a default of the string "now-6m" will be used + * @deprecated Use packages/kbn-securitysolution-io-ts-utils */ export const DefaultFromString = new t.Type( 'DefaultFromString', diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_interval_string.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_interval_string.ts index d743fa773f2ec..fb3a5931199a1 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_interval_string.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_interval_string.ts @@ -11,6 +11,7 @@ import { Either } from 'fp-ts/lib/Either'; /** * Types the DefaultIntervalString as: * - If null or undefined, then a default of the string "5m" will be used + * @deprecated Use packages/kbn-securitysolution-io-ts-utils */ export const DefaultIntervalString = new t.Type( 'DefaultIntervalString', diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_language_string.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_language_string.ts index 5c29e6500511a..a40b0d8eeacaa 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_language_string.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_language_string.ts @@ -12,6 +12,7 @@ import { language } from '../common/schemas'; /** * Types the DefaultLanguageString as: * - If null or undefined, then a default of the string "kuery" will be used + * @deprecated Use packages/kbn-securitysolution-io-ts-utils */ export const DefaultLanguageString = new t.Type( 'DefaultLanguageString', diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_max_signals_number.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_max_signals_number.ts index 1e85f3540c623..d9209cdba4d00 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_max_signals_number.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_max_signals_number.ts @@ -15,6 +15,7 @@ import { DEFAULT_MAX_SIGNALS } from '../../../constants'; * - Natural Number (positive integer and not a float), * - greater than 1 * - If undefined then it will use DEFAULT_MAX_SIGNALS (100) as the default + * @deprecated Use packages/kbn-securitysolution-io-ts-utils */ export const DefaultMaxSignalsNumber = new t.Type( 'DefaultMaxSignals', diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_page.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_page.ts index 8b9f3b115ae23..2f2cdaf3b6cd2 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_page.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_page.ts @@ -14,6 +14,7 @@ import { PositiveIntegerGreaterThanZero } from './positive_integer_greater_than_ * - If a string this will convert the string to a number * - If null or undefined, then a default of 1 will be used * - If the number is 0 or less this will not validate as it has to be a positive number greater than zero + * @deprecated Use packages/kbn-securitysolution-io-ts-utils */ export const DefaultPage = new t.Type( 'DefaultPerPage', diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_per_page.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_per_page.ts index e8c9ef5d7d7f9..124fdf25917fa 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_per_page.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_per_page.ts @@ -14,6 +14,7 @@ import { PositiveIntegerGreaterThanZero } from './positive_integer_greater_than_ * - If a string this will convert the string to a number * - If null or undefined, then a default of 20 will be used * - If the number is 0 or less this will not validate as it has to be a positive number greater than zero + * @deprecated Use packages/kbn-securitysolution-io-ts-utils */ export const DefaultPerPage = new t.Type( 'DefaultPerPage', diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_risk_score_mapping_array.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_risk_score_mapping_array.ts index 172ed78c10eab..219b9d30b52ea 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_risk_score_mapping_array.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_risk_score_mapping_array.ts @@ -12,6 +12,7 @@ import { risk_score_mapping, RiskScoreMapping } from '../common/schemas'; /** * Types the DefaultStringArray as: * - If null or undefined, then a default risk_score_mapping array will be set + * @deprecated Use packages/kbn-securitysolution-io-ts-utils */ export const DefaultRiskScoreMappingArray = new t.Type< RiskScoreMapping, diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_severity_mapping_array.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_severity_mapping_array.ts index 4ee86b7d3793f..fe65574383294 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_severity_mapping_array.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_severity_mapping_array.ts @@ -12,6 +12,7 @@ import { severity_mapping, SeverityMapping } from '../common/schemas'; /** * Types the DefaultStringArray as: * - If null or undefined, then a default severity_mapping array will be set + * @deprecated Use packages/kbn-securitysolution-io-ts-utils */ export const DefaultSeverityMappingArray = new t.Type< SeverityMapping, diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_string_array.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_string_array.ts index 064ec9536d11c..be4b5563ddb13 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_string_array.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_string_array.ts @@ -12,6 +12,7 @@ import { Either } from 'fp-ts/lib/Either'; * Types the DefaultStringArray as: * - If undefined, then a default array will be set * - If an array is sent in, then the array will be validated to ensure all elements are a string + * @deprecated Use packages/kbn-securitysolution-io-ts-utils */ export const DefaultStringArray = new t.Type( 'DefaultStringArray', diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_string_boolean_false.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_string_boolean_false.ts index 02b18b079e3dd..69bd4564fd3bb 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_string_boolean_false.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_string_boolean_false.ts @@ -12,6 +12,7 @@ import { Either } from 'fp-ts/lib/Either'; * Types the DefaultStringBooleanFalse as: * - If a string this will convert the string to a boolean * - If null or undefined, then a default false will be set + * @deprecated Use packages/kbn-securitysolution-io-ts-utils */ export const DefaultStringBooleanFalse = new t.Type( 'DefaultStringBooleanFalse', diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_threat_array.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_threat_array.ts index 139183e8e4370..86c7ab245d6f3 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_threat_array.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_threat_array.ts @@ -12,6 +12,7 @@ import { Threats, threats } from '../common/schemas'; /** * Types the DefaultThreatArray as: * - If null or undefined, then an empty array will be set + * @deprecated Use packages/kbn-securitysolution-io-ts-utils */ export const DefaultThreatArray = new t.Type( 'DefaultThreatArray', diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_throttle_null.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_throttle_null.ts index 4c44b512a3750..db6eb5eab32ae 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_throttle_null.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_throttle_null.ts @@ -12,6 +12,7 @@ import { ThrottleOrNull, throttle } from '../common/schemas'; /** * Types the DefaultThrottleNull as: * - If null or undefined, then a null will be set + * @deprecated Use packages/kbn-securitysolution-io-ts-utils */ export const DefaultThrottleNull = new t.Type( 'DefaultThreatNull', diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_to_string.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_to_string.ts index d2e58f423b592..05c93951bfe1e 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_to_string.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_to_string.ts @@ -11,6 +11,7 @@ import { Either } from 'fp-ts/lib/Either'; /** * Types the DefaultToString as: * - If null or undefined, then a default of the string "now" will be used + * @deprecated Use packages/kbn-securitysolution-io-ts-utils */ export const DefaultToString = new t.Type( 'DefaultToString', diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_uuid.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_uuid.ts index 0546862748b36..ecb8cd00ff308 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_uuid.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_uuid.ts @@ -15,6 +15,7 @@ import { NonEmptyString } from './non_empty_string'; * Types the DefaultUuid as: * - If null or undefined, then a default string uuid.v4() will be * created otherwise it will be checked just against an empty string + * @deprecated Use packages/kbn-securitysolution-io-ts-utils */ export const DefaultUuid = new t.Type( 'DefaultUuid', diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_version_number.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_version_number.ts index ce9b46a4f0967..625973694a244 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_version_number.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_version_number.ts @@ -12,6 +12,7 @@ import { version, Version } from '../common/schemas'; /** * Types the DefaultVersionNumber as: * - If null or undefined, then a default of the number 1 will be used + * @deprecated Use packages/kbn-securitysolution-io-ts-utils */ export const DefaultVersionNumber = new t.Type( 'DefaultVersionNumber', diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/iso_date_string.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/iso_date_string.ts index 6b6be2b035068..9c8f490d2d59a 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/iso_date_string.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/iso_date_string.ts @@ -11,6 +11,7 @@ import { Either } from 'fp-ts/lib/Either'; /** * Types the IsoDateString as: * - A string that is an ISOString + * @deprecated Use packages/kbn-securitysolution-io-ts-utils */ export const IsoDateString = new t.Type( 'IsoDateString', diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.ts index 390257b5aa37c..79fd264808138 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists.ts @@ -11,6 +11,9 @@ import { exceptionListType, namespaceType } from '../../../shared_imports'; import { NonEmptyString } from './non_empty_string'; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const list = t.exact( t.type({ id: NonEmptyString, diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists_default_array.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists_default_array.ts index 64cb54eff0e99..eba48c0419ec2 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists_default_array.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/lists_default_array.ts @@ -13,6 +13,7 @@ import { ListArray, list } from './lists'; /** * Types the DefaultListArray as: * - If null or undefined, then a default array of type list will be set + * @deprecated Use packages/kbn-securitysolution-io-ts-utils */ export const DefaultListArray = new t.Type( 'DefaultListArray', diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/non_empty_array.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/non_empty_array.ts index 0c1a61a912062..1b5261931230d 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/non_empty_array.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/non_empty_array.ts @@ -8,6 +8,9 @@ import * as t from 'io-ts'; import { Either } from 'fp-ts/lib/Either'; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const NonEmptyArray = ( codec: C, name: string = `NonEmptyArray<${codec.name}>` diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/non_empty_string.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/non_empty_string.ts index 5ba85f2ab0249..e01eabd4c434e 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/non_empty_string.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/non_empty_string.ts @@ -11,6 +11,7 @@ import { Either } from 'fp-ts/lib/Either'; /** * Types the NonEmptyString as: * - A string that is not empty + * @deprecated Use packages/kbn-securitysolution-io-ts-utils */ export const NonEmptyString = new t.Type( 'NonEmptyString', diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/normalized_ml_job_id.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/normalized_ml_job_id.ts index c826bce92c8a0..7001cb8254d2b 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/normalized_ml_job_id.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/normalized_ml_job_id.ts @@ -10,13 +10,27 @@ import * as t from 'io-ts'; import { NonEmptyArray } from './non_empty_array'; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const machine_learning_job_id_normalized = NonEmptyArray(t.string); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type MachineLearningJobIdNormalized = t.TypeOf; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const machineLearningJobIdNormalizedOrUndefined = t.union([ machine_learning_job_id_normalized, t.undefined, ]); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type MachineLearningJobIdNormalizedOrUndefined = t.TypeOf< typeof machineLearningJobIdNormalizedOrUndefined >; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/only_false_allowed.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/only_false_allowed.ts index 5d0334bdec841..8150979cf2947 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/only_false_allowed.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/only_false_allowed.ts @@ -13,6 +13,7 @@ import { Either } from 'fp-ts/lib/Either'; * - If null or undefined, then a default false will be set * - If true is sent in then this will return an error * - If false is sent in then this will allow it only false + * @deprecated Use packages/kbn-securitysolution-io-ts-utils */ export const OnlyFalseAllowed = new t.Type( 'DefaultBooleanTrue', diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/positive_integer.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/positive_integer.ts index 4bcdff37b9667..1771d0515fa20 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/positive_integer.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/positive_integer.ts @@ -12,6 +12,7 @@ import { Either } from 'fp-ts/lib/Either'; * Types the positive integer are: * - Natural Number (positive integer and not a float), * - zero or greater + * @deprecated Use packages/kbn-securitysolution-io-ts-utils */ export const PositiveInteger = new t.Type( 'PositiveInteger', diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/positive_integer_greater_than_zero.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/positive_integer_greater_than_zero.ts index 57be0240e8250..ddfb82b748ee3 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/positive_integer_greater_than_zero.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/positive_integer_greater_than_zero.ts @@ -12,6 +12,7 @@ import { Either } from 'fp-ts/lib/Either'; * Types the positive integer greater than zero is: * - Natural Number (positive integer and not a float), * - 1 or greater + * @deprecated Use packages/kbn-securitysolution-io-ts-utils */ export const PositiveIntegerGreaterThanZero = new t.Type( 'PositiveIntegerGreaterThanZero', diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/references_default_array.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/references_default_array.ts index e5ccac3650434..a76606911f4ae 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/references_default_array.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/references_default_array.ts @@ -11,6 +11,7 @@ import { Either } from 'fp-ts/lib/Either'; /** * Types the ReferencesDefaultArray as: * - If null or undefined, then a default array will be set + * @deprecated Use packages/kbn-securitysolution-io-ts-utils */ export const ReferencesDefaultArray = new t.Type( 'referencesWithDefaultArray', diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/risk_score.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/risk_score.ts index d644798b667b3..e518bf13fd181 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/risk_score.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/risk_score.ts @@ -12,6 +12,7 @@ import { Either } from 'fp-ts/lib/Either'; * Types the risk score as: * - Natural Number (positive integer and not a float), * - Between the values [0 and 100] inclusive. + * @deprecated Use packages/kbn-securitysolution-io-ts-utils */ export const RiskScore = new t.Type( 'RiskScore', diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat_mapping.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat_mapping.ts index aab06941686c2..707b51e6b8965 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat_mapping.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat_mapping.ts @@ -13,21 +13,69 @@ import { NonEmptyArray } from './non_empty_array'; import { NonEmptyString } from './non_empty_string'; import { PositiveIntegerGreaterThanZero } from './positive_integer_greater_than_zero'; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const threat_query = t.string; + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type ThreatQuery = t.TypeOf; + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const threatQueryOrUndefined = t.union([threat_query, t.undefined]); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type ThreatQueryOrUndefined = t.TypeOf; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const threat_indicator_path = t.string; + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type ThreatIndicatorPath = t.TypeOf; + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const threatIndicatorPathOrUndefined = t.union([threat_indicator_path, t.undefined]); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type ThreatIndicatorPathOrUndefined = t.TypeOf; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const threat_filters = t.array(t.unknown); // Filters are not easily type-able yet + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type ThreatFilters = t.TypeOf; + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const threatFiltersOrUndefined = t.union([threat_filters, t.undefined]); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type ThreatFiltersOrUndefined = t.TypeOf; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const threatMapEntry = t.exact( t.type({ field: NonEmptyString, @@ -36,40 +84,131 @@ export const threatMapEntry = t.exact( }) ); +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type ThreatMapEntry = t.TypeOf; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const threatMappingEntries = t.array(threatMapEntry); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type ThreatMappingEntries = t.TypeOf; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const threatMap = t.exact( t.type({ entries: threatMappingEntries, }) ); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type ThreatMap = t.TypeOf; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const threat_mapping = NonEmptyArray(threatMap, 'NonEmptyArray'); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type ThreatMapping = t.TypeOf; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const threatMappingOrUndefined = t.union([threat_mapping, t.undefined]); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type ThreatMappingOrUndefined = t.TypeOf; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const threat_index = t.array(t.string); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type ThreatIndex = t.TypeOf; + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const threatIndexOrUndefined = t.union([threat_index, t.undefined]); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type ThreatIndexOrUndefined = t.TypeOf; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const threat_language = t.union([language, t.undefined]); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type ThreatLanguage = t.TypeOf; + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const threatLanguageOrUndefined = t.union([threat_language, t.undefined]); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type ThreatLanguageOrUndefined = t.TypeOf; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const concurrent_searches = PositiveIntegerGreaterThanZero; + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type ConcurrentSearches = t.TypeOf; + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const concurrentSearchesOrUndefined = t.union([concurrent_searches, t.undefined]); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type ConcurrentSearchesOrUndefined = t.TypeOf; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const items_per_search = PositiveIntegerGreaterThanZero; + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type ItemsPerSearch = t.TypeOf; + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export const itemsPerSearchOrUndefined = t.union([items_per_search, t.undefined]); + +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils + */ export type ItemsPerSearchOrUndefined = t.TypeOf; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/uuid.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/uuid.ts index ca90026db0b50..79a130b26a6fd 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/uuid.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/uuid.ts @@ -14,6 +14,7 @@ const regex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; * Types the risk score as: * - Natural Number (positive integer and not a float), * - Between the values [0 and 100] inclusive. + * @deprecated Use packages/kbn-securitysolution-io-ts-utils */ export const UUID = new t.Type( 'UUID', diff --git a/x-pack/plugins/security_solution/common/exact_check.ts b/x-pack/plugins/security_solution/common/exact_check.ts index b10328a4db233..5334989ea085b 100644 --- a/x-pack/plugins/security_solution/common/exact_check.ts +++ b/x-pack/plugins/security_solution/common/exact_check.ts @@ -25,6 +25,7 @@ import { isObject, get } from 'lodash/fp'; * @param original The original to check if it has additional keys * @param decoded The decoded either which has either an existing error or the * decoded object which could have additional keys stripped from it. + * @deprecated Use packages/kbn-securitysolution-io-ts-utils/src/exact_check/index.ts */ export const exactCheck = ( original: unknown, diff --git a/x-pack/plugins/security_solution/common/format_errors.ts b/x-pack/plugins/security_solution/common/format_errors.ts index 7b33612b4e887..16925699b0fcf 100644 --- a/x-pack/plugins/security_solution/common/format_errors.ts +++ b/x-pack/plugins/security_solution/common/format_errors.ts @@ -8,6 +8,9 @@ import * as t from 'io-ts'; import { isObject } from 'lodash/fp'; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils/src/format_errors/index.ts + */ export const formatErrors = (errors: t.Errors): string[] => { const err = errors.map((error) => { if (error.message != null) { diff --git a/x-pack/plugins/security_solution/common/test_utils.ts b/x-pack/plugins/security_solution/common/test_utils.ts index 5f5df98c08b4a..df9e9e12fc1d9 100644 --- a/x-pack/plugins/security_solution/common/test_utils.ts +++ b/x-pack/plugins/security_solution/common/test_utils.ts @@ -15,10 +15,16 @@ interface Message { schema: T | {}; } +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils/src/test_utils/index.ts + */ const onLeft = (errors: t.Errors): Message => { return { schema: {}, errors }; }; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils/src/test_utils/index.ts + */ const onRight = (schema: T): Message => { return { schema, @@ -26,11 +32,15 @@ const onRight = (schema: T): Message => { }; }; +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils/src/test_utils/index.ts + */ export const foldLeftRight = fold(onLeft, onRight); /** * Convenience utility to keep the error message handling within tests to be * very concise. + * @deprecated Use packages/kbn-securitysolution-io-ts-utils/src/test_utils/index.ts * @param validation The validation to get the errors from */ export const getPaths = (validation: t.Validation): string[] => { @@ -45,6 +55,7 @@ export const getPaths = (validation: t.Validation): string[] => { /** * Convenience utility to remove text appended to links by EUI + * @deprecated Use packages/kbn-securitysolution-io-ts-utils/src/test_utils/index.ts */ export const removeExternalLinkText = (str: string) => str.replace(/\(opens in a new tab or window\)/g, ''); diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/notes_tab.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/notes_tab.spec.ts index fdc003039afbc..cb3785c1aba25 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timelines/notes_tab.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timelines/notes_tab.spec.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { timeline } from '../../objects/timeline'; +import { timelineNonValidQuery } from '../../objects/timeline'; import { NOTES_TEXT, NOTES_TEXT_AREA } from '../../screens/timeline'; import { createTimeline } from '../../tasks/api_calls/timelines'; @@ -19,14 +19,14 @@ import { waitForTimelinesPanelToBeLoaded } from '../../tasks/timelines'; import { TIMELINES_URL } from '../../urls/navigation'; describe('Timeline notes tab', () => { - let timelineId: string | undefined; + let timelineId: string; before(() => { cleanKibana(); loginAndWaitForPageWithoutDateRange(TIMELINES_URL); waitForTimelinesPanelToBeLoaded(); - createTimeline(timeline) + createTimeline(timelineNonValidQuery) .then((response) => { if (response.body.data.persistTimeline.timeline.savedObjectId == null) { cy.log('"createTimeline" did not return expected response'); @@ -35,11 +35,8 @@ describe('Timeline notes tab', () => { waitForTimelinesPanelToBeLoaded(); }) .then(() => { - // TODO: It would be great to add response validation to avoid such things like - // the bang below and to more easily understand where failures are coming from - - // client vs server side - openTimelineById(timelineId!).then(() => { - addNotesToTimeline(timeline.notes); + openTimelineById(timelineId).then(() => { + addNotesToTimeline(timelineNonValidQuery.notes); }); }); }); @@ -49,7 +46,7 @@ describe('Timeline notes tab', () => { }); it('should contain notes', () => { - cy.get(NOTES_TEXT).should('have.text', timeline.notes); + cy.get(NOTES_TEXT).should('have.text', timelineNonValidQuery.notes); }); it('should render mockdown', () => { diff --git a/x-pack/plugins/security_solution/cypress/objects/timeline.ts b/x-pack/plugins/security_solution/cypress/objects/timeline.ts index a3d653840dfd9..b5089c9775666 100644 --- a/x-pack/plugins/security_solution/cypress/objects/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/objects/timeline.ts @@ -37,6 +37,16 @@ export const timeline: CompleteTimeline = { filter, }; +/** + * Timeline query that finds no valid data to cut down on test failures + * or other issues for when we want to test one specific thing and not also + * test the queries happening + */ +export const timelineNonValidQuery: CompleteTimeline = { + ...timeline, + query: 'query_to_intentionally_find_nothing: *', +}; + export const caseTimeline: Timeline = { title: 'SIEM test', description: 'description', diff --git a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts index a7842a7d98e83..e81cfa50a1a70 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts @@ -234,21 +234,14 @@ export const openTimelineTemplateFromSettings = (id: string) => { }; export const openTimelineById = (timelineId: string): Cypress.Chainable> => { - // Why are we checking for null if it is typed to 'string'? We don't currently validate the timeline response - // so technically we cannot guarantee that we will have the id. Changing the type to 'string | null' results in - // a lot of other changes being needed that would be best as part of a cleanup. Added a log, to give a dev a clue - // as to whether it's failing client or server side. if (timelineId == null) { + // Log out if for some reason this happens to be null just in case for our tests we experience + // value of null. Some tests return an "any" which is why this could happen. cy.log('"timelineId" is null or undefined'); } - - cy.root() - .pipe(($el) => { - $el.find(TIMELINE_TITLE_BY_ID(timelineId)).trigger('click'); - return $el.find(QUERY_TAB_BUTTON).find('.euiBadge'); - }) - .should('be.visible'); - return cy.root().find(TIMELINE_TITLE_BY_ID(timelineId)); + // We avoid use cypress.pipe() here and multiple clicks because each of these clicks + // can result in a new URL async operation occurring and then we get indeterminism as the URL loads multiple times. + return cy.get(TIMELINE_TITLE_BY_ID(timelineId)).should('be.visible').click({ force: true }); }; export const pinFirstEvent = () => { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/translations.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/translations.ts index 722a551a587a4..f3b697d12af60 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/translations.ts @@ -46,6 +46,18 @@ export const CREATED_BY = i18n.translate('xpack.securitySolution.exceptions.crea defaultMessage: 'Created by', }); +export const NAME = i18n.translate('xpack.securitySolution.exceptions.nameLabel', { + defaultMessage: 'Name', +}); + +export const DATE_MODIFIED = i18n.translate('xpack.securitySolution.exceptions.dateModifiedLabel', { + defaultMessage: 'Date modified', +}); + +export const MODIFIED_BY = i18n.translate('xpack.securitySolution.exceptions.modifiedByLabel', { + defaultMessage: 'Modified by', +}); + export const COMMENT = i18n.translate('xpack.securitySolution.exceptions.commentLabel', { defaultMessage: 'Comment', }); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.test.tsx index b6cf55585c0d3..e3e9ba1bfa132 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.test.tsx @@ -221,4 +221,32 @@ describe('ExceptionDetails', () => { expect(wrapper.find('EuiDescriptionListTitle').at(3).text()).toEqual('Description'); expect(wrapper.find('EuiDescriptionListDescription').at(3).text()).toEqual('some description'); }); + + test('it renders with Name and Modified info when showName and showModified props are true', () => { + const exceptionItem = getExceptionListItemSchemaMock(); + exceptionItem.comments = []; + + const wrapper = mount( + + + + ); + + expect(wrapper.find('EuiDescriptionListTitle').at(0).text()).toEqual('Name'); + expect(wrapper.find('EuiDescriptionListDescription').at(0).text()).toEqual('some name'); + + expect(wrapper.find('EuiDescriptionListTitle').at(4).text()).toEqual('Date modified'); + expect(wrapper.find('EuiDescriptionListDescription').at(4).text()).toEqual( + 'April 20th 2020 @ 15:25:31' + ); + + expect(wrapper.find('EuiDescriptionListTitle').at(5).text()).toEqual('Modified by'); + expect(wrapper.find('EuiDescriptionListDescription').at(5).text()).toEqual('some user'); + }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.tsx index 4e9fcded04013..86eca688ef082 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.tsx @@ -38,16 +38,20 @@ const MyDescriptionListDescription = styled(EuiDescriptionListDescription)` const ExceptionDetailsComponent = ({ showComments, + showModified = false, + showName = false, onCommentsClick, exceptionItem, }: { showComments: boolean; + showModified?: boolean; + showName?: boolean; exceptionItem: ExceptionListItemSchema; onCommentsClick: () => void; }): JSX.Element => { const descriptionListItems = useMemo( - (): DescriptionListItem[] => getDescriptionListContent(exceptionItem), - [exceptionItem] + (): DescriptionListItem[] => getDescriptionListContent(exceptionItem, showModified, showName), + [exceptionItem, showModified, showName] ); const commentsSection = useMemo((): JSX.Element => { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.test.tsx index 8b915379f48bb..7c55c0de68c64 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.test.tsx @@ -45,6 +45,31 @@ describe('ExceptionItem', () => { expect(wrapper.find('ExceptionEntries')).toHaveLength(1); }); + it('it renders ExceptionDetails with Name and Modified info when showName and showModified are true ', () => { + const exceptionItem = getExceptionListItemSchemaMock(); + + const wrapper = mount( + + + + ); + + expect(wrapper.find('ExceptionDetails').props()).toEqual( + expect.objectContaining({ + showModified: true, + showName: true, + }) + ); + }); + it('it invokes "onEditException" when edit button clicked', () => { const mockOnEditException = jest.fn(); const exceptionItem = getExceptionListItemSchemaMock(); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.tsx index bfc607edd5873..4b6ae29de3bbf 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.tsx @@ -30,12 +30,14 @@ const MyFlexItem = styled(EuiFlexItem)` } `; -interface ExceptionItemProps { +export interface ExceptionItemProps { loadingItemIds: ExceptionListItemIdentifiers[]; exceptionItem: ExceptionListItemSchema; commentsAccordionId: string; onDeleteException: (arg: ExceptionListItemIdentifiers) => void; onEditException: (item: ExceptionListItemSchema) => void; + showName?: boolean; + showModified?: boolean; } const ExceptionItemComponent = ({ @@ -44,6 +46,8 @@ const ExceptionItemComponent = ({ commentsAccordionId, onDeleteException, onEditException, + showModified = false, + showName = false, }: ExceptionItemProps): JSX.Element => { const [entryItems, setEntryItems] = useState([]); const [showComments, setShowComments] = useState(false); @@ -86,6 +90,8 @@ const ExceptionItemComponent = ({ showComments={showComments} exceptionItem={exceptionItem} onCommentsClick={onCommentsClick} + showModified={showModified} + showName={showName} /> { expect(result).toEqual(expected); }); + + test('it returns Modified By/On info. when `includeModified` is true', () => { + const result = getDescriptionListContent(getExceptionListItemSchemaMock(), true); + expect(result).toEqual([ + { + description: 'Linux', + title: 'OS', + }, + { + description: 'April 20th 2020 @ 15:25:31', + title: 'Date created', + }, + { + description: 'some user', + title: 'Created by', + }, + { + description: 'April 20th 2020 @ 15:25:31', + title: 'Date modified', + }, + { + description: 'some user', + title: 'Modified by', + }, + { + description: 'some description', + title: 'Description', + }, + ]); + }); + + test('it returns Name when `includeName` is true', () => { + const result = getDescriptionListContent(getExceptionListItemSchemaMock(), false, true); + expect(result).toEqual([ + { + description: 'some name', + title: 'Name', + }, + { + description: 'Linux', + title: 'OS', + }, + { + description: 'April 20th 2020 @ 15:25:31', + title: 'Date created', + }, + { + description: 'some user', + title: 'Created by', + }, + { + description: 'some description', + title: 'Description', + }, + ]); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.tsx index fb8756871ea36..29764625075d6 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.tsx @@ -70,11 +70,23 @@ export const getFormattedEntries = (entries: BuilderEntry[]): FormattedEntry[] = * Formats ExceptionItem details for description list component * * @param exceptionItem an ExceptionItem + * @param includeModified if modified information should be included + * @param includeName if the Name should be included */ export const getDescriptionListContent = ( - exceptionItem: ExceptionListItemSchema + exceptionItem: ExceptionListItemSchema, + includeModified: boolean = false, + includeName: boolean = false ): DescriptionListItem[] => { const details = [ + ...(includeName + ? [ + { + title: i18n.NAME, + value: exceptionItem.name, + }, + ] + : []), { title: i18n.OPERATING_SYSTEM, value: formatOperatingSystems(exceptionItem.os_types), @@ -87,6 +99,18 @@ export const getDescriptionListContent = ( title: i18n.CREATED_BY, value: exceptionItem.created_by, }, + ...(includeModified + ? [ + { + title: i18n.DATE_MODIFIED, + value: moment(exceptionItem.updated_at).format('MMMM Do YYYY @ HH:mm:ss'), + }, + { + title: i18n.MODIFIED_BY, + value: exceptionItem.updated_by, + }, + ] + : []), { title: i18n.DESCRIPTION, value: exceptionItem.description, diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts index 571cef8f6c484..605478900d066 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts +++ b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts @@ -15,7 +15,7 @@ import { getBreadcrumbs as getIPDetailsBreadcrumbs } from '../../../../network/p import { getBreadcrumbs as getCaseDetailsBreadcrumbs } from '../../../../cases/pages/utils'; import { getBreadcrumbs as getDetectionRulesBreadcrumbs } from '../../../../detections/pages/detection_engine/rules/utils'; import { getBreadcrumbs as getTimelinesBreadcrumbs } from '../../../../timelines/pages'; -import { getBreadcrumbs as getAdminBreadcrumbs } from '../../../../management/pages'; +import { getBreadcrumbs as getAdminBreadcrumbs } from '../../../../management/common/breadcrumbs'; import { SecurityPageName } from '../../../../app/types'; import { RouteSpyState, diff --git a/x-pack/plugins/security_solution/public/management/common/breadcrumbs.ts b/x-pack/plugins/security_solution/public/management/common/breadcrumbs.ts new file mode 100644 index 0000000000000..76acff7847671 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/common/breadcrumbs.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ChromeBreadcrumb } from 'kibana/public'; +import { isEmpty } from 'lodash/fp'; +import { AdministrationSubTab } from '../types'; +import { ENDPOINTS_TAB, EVENT_FILTERS_TAB, POLICIES_TAB, TRUSTED_APPS_TAB } from './translations'; +import { AdministrationRouteSpyState } from '../../common/utils/route/types'; +import { GetUrlForApp } from '../../common/components/navigation/types'; +import { ADMINISTRATION } from '../../app/home/translations'; +import { APP_ID, SecurityPageName } from '../../../common/constants'; + +const TabNameMappedToI18nKey: Record = { + [AdministrationSubTab.endpoints]: ENDPOINTS_TAB, + [AdministrationSubTab.policies]: POLICIES_TAB, + [AdministrationSubTab.trustedApps]: TRUSTED_APPS_TAB, + [AdministrationSubTab.eventFilters]: EVENT_FILTERS_TAB, +}; + +export function getBreadcrumbs( + params: AdministrationRouteSpyState, + search: string[], + getUrlForApp: GetUrlForApp +): ChromeBreadcrumb[] { + return [ + { + text: ADMINISTRATION, + href: getUrlForApp(`${APP_ID}:${SecurityPageName.administration}`, { + path: !isEmpty(search[0]) ? search[0] : '', + }), + }, + ...(params?.tabName ? [params?.tabName] : []).map((tabName) => ({ + text: TabNameMappedToI18nKey[tabName], + href: '', + })), + ]; +} diff --git a/x-pack/plugins/security_solution/public/management/components/paginated_content/paginated_content.tsx b/x-pack/plugins/security_solution/public/management/components/paginated_content/paginated_content.tsx index ce5d26feb3e75..ea97ce5e30706 100644 --- a/x-pack/plugins/security_solution/public/management/components/paginated_content/paginated_content.tsx +++ b/x-pack/plugins/security_solution/public/management/components/paginated_content/paginated_content.tsx @@ -32,12 +32,13 @@ import styled from 'styled-components'; import { FormattedMessage } from '@kbn/i18n/react'; import { v4 as generateUUI } from 'uuid'; import { useTestIdGenerator } from '../hooks/use_test_id_generator'; +import { MaybeImmutable } from '../../../../common/endpoint/types'; // eslint-disable-next-line @typescript-eslint/no-explicit-any type ComponentWithAnyProps = ComponentType; export interface PaginatedContentProps extends CommonProps { - items: T[]; + items: MaybeImmutable; onChange: (changes: { pageIndex: number; pageSize: number }) => void; /** * The React Component that will be used to render the `items`. use `itemComponentProps` below to @@ -79,6 +80,7 @@ interface TypedGenericComponentMemo { const RootContainer = styled.div` position: relative; + padding-top: ${({ theme }) => theme.eui.paddingSizes.xs}; .body { min-height: ${({ theme }) => theme.eui.gutterTypes.gutterExtraLarge}; @@ -174,7 +176,11 @@ export const PaginatedContent = memo( const Item = ItemComponent as ComponentType>; if (items.length) { - return items.map((item) => { + // Cast here is to get around the fact that TS does not seem to be able to narrow the types down when the only + // difference is that the array might be Readonly. The error output is: + // `...has signatures, but none of those signatures are compatible with each other.` + // Can read more about it here: https://github.com/microsoft/TypeScript/issues/33591 + return (items as T[]).map((item) => { let key: Key; if (itemId) { @@ -197,16 +203,15 @@ export const PaginatedContent = memo( return ( - {loading && } + {loading && }
-
{children ? children : generatedBodyItemContent}
- {pagination && ( + {pagination && (children || items.length > 0) && (
diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx index 17ebff603ccfb..304d67d8b6d6f 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx @@ -47,7 +47,7 @@ jest.mock('../../policy/store/services/ingest', () => { sendGetEndpointSecurityPackage: () => Promise.resolve({}), }; }); -describe('when on the list page', () => { +describe('when on the endpoint list page', () => { const docGenerator = new EndpointDocGenerator(); let render: () => ReturnType; let history: AppContextTestRender['history']; diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/index.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/index.tsx index 86c2f2364961d..7d2cb526367c5 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/index.tsx @@ -19,3 +19,4 @@ export const EventFiltersContainer = () => { ); }; +export { EventFiltersService } from './types'; diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/service/index.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/service/index.ts index b3521d7499362..9d186d590ca67 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/service/index.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/service/index.ts @@ -6,15 +6,15 @@ */ import { HttpStart } from 'kibana/public'; -import { ExceptionListItemSchema, CreateExceptionListItemSchema } from '../../../../shared_imports'; -import { Immutable } from '../../../../../common/endpoint/types'; +import { + CreateExceptionListItemSchema, + ENDPOINT_EVENT_FILTERS_LIST_ID, + ExceptionListItemSchema, +} from '../../../../shared_imports'; import { EVENT_FILTER_LIST, EXCEPTION_LIST_ITEM_URL, EXCEPTION_LIST_URL } from '../constants'; +import { FoundExceptionListItemSchema } from '../../../../../../lists/common/schemas'; +import { EventFiltersService } from '../types'; -export interface EventFiltersService { - addEventFilters( - exception: Immutable - ): Promise; -} export class EventFiltersHttpService implements EventFiltersService { private listHasBeenCreated: boolean; @@ -27,6 +27,7 @@ export class EventFiltersHttpService implements EventFiltersService { await this.http.post(EXCEPTION_LIST_URL, { body: JSON.stringify(EVENT_FILTER_LIST), }); + this.listHasBeenCreated = true; } catch (err) { // Ignore 409 errors. List already created if (err.response.status === 409) this.listHasBeenCreated = true; @@ -39,6 +40,30 @@ export class EventFiltersHttpService implements EventFiltersService { return this.http; } + async getList({ + perPage, + page, + sortField, + sortOrder, + }: Partial<{ + page: number; + perPage: number; + sortField: string; + sortOrder: string; + }> = {}): Promise { + const http = await this.httpWrapper(); + return http.get(`${EXCEPTION_LIST_ITEM_URL}/_find`, { + query: { + page, + per_page: perPage, + sort_field: sortField, + sort_order: sortOrder, + list_id: [ENDPOINT_EVENT_FILTERS_LIST_ID], + namespace_type: ['agnostic'], + }, + }); + } + async addEventFilters(exception: ExceptionListItemSchema | CreateExceptionListItemSchema) { return (await this.httpWrapper()).post(EXCEPTION_LIST_ITEM_URL, { body: JSON.stringify(exception), diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/state/index.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/state/index.ts index caaa48ddf1987..4bc90ce764ace 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/state/index.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/state/index.ts @@ -7,6 +7,8 @@ import { ExceptionListItemSchema, CreateExceptionListItemSchema } from '../../../../shared_imports'; import { AsyncResourceState } from '../../../state/async_resource_state'; +import { FoundExceptionListItemSchema } from '../../../../../../lists/common/schemas'; +import { EventFiltersServiceGetListOptions } from '../types'; export interface EventFiltersPageLocation { page_index: number; @@ -16,6 +18,7 @@ export interface EventFiltersPageLocation { id?: string; filter: string; } + export interface EventFiltersListPageState { entries: ExceptionListItemSchema[]; form: { @@ -26,4 +29,17 @@ export interface EventFiltersListPageState { submissionResourceState: AsyncResourceState; }; location: EventFiltersPageLocation; + /** State for the Event Filters List page */ + listPage: { + active: boolean; + forceRefresh: boolean; + data: AsyncResourceState<{ + /** The query that was used to retrieve the data */ + query: EventFiltersServiceGetListOptions; + /** The data retrieved from the API */ + content: FoundExceptionListItemSchema; + }>; + /** tracks if the overall list (not filtered or with invalid page numbers) contains data */ + dataExist: AsyncResourceState; + }; } diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/action.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/action.ts index 79ef5cbc4e42c..21c8ef63d3eb3 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/action.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/action.ts @@ -8,6 +8,19 @@ import { Action } from 'redux'; import { ExceptionListItemSchema, CreateExceptionListItemSchema } from '../../../../shared_imports'; import { AsyncResourceState } from '../../../state/async_resource_state'; +import { EventFiltersListPageState } from '../state'; + +export type EventFiltersListPageStateChanged = Action<'eventFiltersListPageStateChanged'> & { + payload: EventFiltersListPageState['listPage']; +}; + +export type EventFiltersListPageDataChanged = Action<'eventFiltersListPageDataChanged'> & { + payload: EventFiltersListPageState['listPage']['data']; +}; + +export type EventFiltersListPageDataExistsChanged = Action<'eventFiltersListPageDataExistsChanged'> & { + payload: EventFiltersListPageState['listPage']['dataExist']; +}; export type EventFiltersInitForm = Action<'eventFiltersInitForm'> & { payload: { @@ -37,10 +50,12 @@ export type EventFiltersFormStateChanged = Action<'eventFiltersFormStateChanged' }; export type EventFiltersPageAction = + | EventFiltersListPageStateChanged + | EventFiltersListPageDataChanged + | EventFiltersListPageDataExistsChanged | EventFiltersCreateStart | EventFiltersInitForm | EventFiltersChangeForm - | EventFiltersCreateStart | EventFiltersCreateSuccess | EventFiltersCreateError | EventFiltersFormStateChanged; diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/builders.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/builders.ts index 92290de4a24ed..8d483c007fe64 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/builders.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/builders.ts @@ -22,4 +22,11 @@ export const initialEventFiltersPageState = (): EventFiltersListPageState => ({ page_size: MANAGEMENT_DEFAULT_PAGE_SIZE, filter: '', }, + listPage: { + active: false, + forceRefresh: false, + data: { type: 'UninitialisedResourceState' }, + /** We started off assuming data exists, until we can confirm othewise */ + dataExist: { type: 'LoadedResourceState', data: true }, + }, }); diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/middleware.test.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/middleware.test.ts index b0b47bc1d6fa0..cbd9b7bf0e538 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/middleware.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/middleware.test.ts @@ -14,16 +14,17 @@ import { import { AppAction } from '../../../../common/store/actions'; import { createEventFiltersPageMiddleware } from './middleware'; import { eventFiltersPageReducer } from './reducer'; -import { EventFiltersService } from '../service'; import { EventFiltersListPageState } from '../state'; import { initialEventFiltersPageState } from './builders'; import { getInitialExceptionFromEvent } from './utils'; import { createdEventFilterEntryMock, ecsEventMock } from '../test_utils'; +import { EventFiltersService } from '../types'; const initialState: EventFiltersListPageState = initialEventFiltersPageState(); const createEventFiltersServiceMock = (): jest.Mocked => ({ addEventFilters: jest.fn(), + getList: jest.fn(), }); const createStoreSetup = (eventFiltersService: EventFiltersService) => { diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/middleware.ts index b379d0893c133..7aca45049f91a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/middleware.ts @@ -12,16 +12,27 @@ import { ImmutableMiddlewareFactory, } from '../../../../common/store'; -import { EventFiltersHttpService, EventFiltersService } from '../service'; +import { EventFiltersHttpService } from '../service'; import { EventFiltersListPageState } from '../state'; import { getLastLoadedResourceState } from '../../../state/async_resource_state'; import { CreateExceptionListItemSchema, transformNewItemOutput } from '../../../../shared_imports'; +import { + getCurrentListPageDataState, + getCurrentLocation, + getListIsLoading, + getListPageDataExistsState, + getListPageIsActive, + listDataNeedsRefresh, +} from './selector'; +import { EventFiltersService, EventFiltersServiceGetListOptions } from '../types'; -const eventFiltersCreate = async ( +type MiddlewareActionHandler = ( store: ImmutableMiddlewareAPI, eventFiltersService: EventFiltersService -) => { +) => Promise; + +const eventFiltersCreate: MiddlewareActionHandler = async (store, eventFiltersService) => { const submissionResourceState = store.getState().form.submissionResourceState; try { const formEntry = store.getState().form.entry; @@ -62,6 +73,97 @@ const eventFiltersCreate = async ( } }; +const checkIfEventFilterDataExist: MiddlewareActionHandler = async ( + { dispatch, getState }, + eventFiltersService: EventFiltersService +) => { + dispatch({ + type: 'eventFiltersListPageDataExistsChanged', + payload: { + type: 'LoadingResourceState', + // Ignore will be fixed with when AsyncResourceState is refactored (#830) + // @ts-ignore + previousState: getListPageDataExistsState(getState()), + }, + }); + + try { + const anythingInListResults = await eventFiltersService.getList({ perPage: 1, page: 1 }); + + dispatch({ + type: 'eventFiltersListPageDataExistsChanged', + payload: { + type: 'LoadedResourceState', + data: Boolean(anythingInListResults.total), + }, + }); + } catch (error) { + dispatch({ + type: 'eventFiltersListPageDataExistsChanged', + payload: { + type: 'FailedResourceState', + error: error.body || error, + }, + }); + } +}; + +const refreshListDataIfNeeded: MiddlewareActionHandler = async (store, eventFiltersService) => { + const { dispatch, getState } = store; + const state = getState(); + const isLoading = getListIsLoading(state); + + if (!isLoading && listDataNeedsRefresh(state)) { + dispatch({ + type: 'eventFiltersListPageDataChanged', + payload: { + type: 'LoadingResourceState', + // Ignore will be fixed with when AsyncResourceState is refactored (#830) + // @ts-ignore + previousState: getCurrentListPageDataState(state), + }, + }); + + const { page_size: pageSize, page_index: pageIndex } = getCurrentLocation(state); + const query: EventFiltersServiceGetListOptions = { + page: pageIndex + 1, + perPage: pageSize, + sortField: 'created_at', + sortOrder: 'desc', + }; + + try { + const results = await eventFiltersService.getList(query); + + dispatch({ + type: 'eventFiltersListPageDataChanged', + payload: { + type: 'LoadedResourceState', + data: { + query, + content: results, + }, + }, + }); + + // If no results were returned, then just check to make sure data actually exists for + // event filters. This is used to drive the UI between showing "empty state" and "no items found" + // messages to the user + if (results.total === 0) { + await checkIfEventFilterDataExist(store, eventFiltersService); + } + } catch (error) { + dispatch({ + type: 'eventFiltersListPageDataChanged', + payload: { + type: 'FailedResourceState', + error: error.body || error, + }, + }); + } + } +}; + export const createEventFiltersPageMiddleware = ( eventFiltersService: EventFiltersService ): ImmutableMiddleware => { @@ -71,6 +173,13 @@ export const createEventFiltersPageMiddleware = ( if (action.type === 'eventFiltersCreateStart') { await eventFiltersCreate(store, eventFiltersService); } + + // Middleware that only applies to the List Page for Event Filters + if (getListPageIsActive(store.getState())) { + if (action.type === 'userChangedUrl' || action.type === 'eventFiltersCreateSuccess') { + refreshListDataIfNeeded(store, eventFiltersService); + } + } }; }; diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/reducer.test.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/reducer.test.ts index f14f8ecc5f8ad..c78cb030fd3d3 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/reducer.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/reducer.test.ts @@ -123,6 +123,10 @@ describe('reducer', () => { id: undefined, show: 'create', }, + listPage: { + ...initialState.listPage, + active: true, + }, }); }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/reducer.ts index a52de492f9a66..d9e7a3c6611e5 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/reducer.ts @@ -20,10 +20,14 @@ import { EventFiltersChangeForm, EventFiltersFormStateChanged, EventFiltersCreateSuccess, + EventFiltersListPageStateChanged, + EventFiltersListPageDataChanged, + EventFiltersListPageDataExistsChanged, } from './action'; import { EventFiltersListPageState } from '../state'; import { initialEventFiltersPageState } from './builders'; +import { getListPageIsActive } from './selector'; type StateReducer = ImmutableReducer; type CaseReducer = ( @@ -40,6 +44,44 @@ const isEventFiltersPageLocation = (location: Immutable) => { ); }; +// FIXME:PT might not need this. maybe delete +const handleEventFiltersListPageChanges: CaseReducer = ( + state, + action +) => { + return { + ...state, + listPage: action.payload, + }; +}; + +const handleEventFiltersListPageDataChanges: CaseReducer = ( + state, + action +) => { + return { + ...state, + listPage: { + ...state.listPage, + forceRefresh: false, + data: action.payload, + }, + }; +}; + +const handleEventFiltersListPageDataExistChanges: CaseReducer = ( + state, + action +) => { + return { + ...state, + listPage: { + ...state.listPage, + dataExist: action.payload, + }, + }; +}; + const eventFiltersInitForm: CaseReducer = (state, action) => { return { ...state, @@ -89,14 +131,38 @@ const eventFiltersCreateSuccess: CaseReducer = (state return { ...state, entries: [action.payload.exception, ...state.entries], + // If we are on the List page, then force a refresh of data + listPage: getListPageIsActive(state) + ? { + ...state.listPage, + forceRefresh: true, + } + : state.listPage, }; }; const userChangedUrl: CaseReducer = (state, action) => { if (isEventFiltersPageLocation(action.payload)) { const location = extractEventFiltetrsPageLocation(parse(action.payload.search.slice(1))); - return { ...state, location }; + return { + ...state, + location, + listPage: { + ...state.listPage, + active: true, + }, + }; } else { + // Reset the list page state if needed + if (state.listPage.active) { + const { listPage } = initialEventFiltersPageState(); + + return { + ...state, + listPage, + }; + } + return state; } }; @@ -118,5 +184,17 @@ export const eventFiltersPageReducer: StateReducer = ( return userChangedUrl(state, action); } + // actions only handled if we're on the List Page + if (getListPageIsActive(state)) { + switch (action.type) { + case 'eventFiltersListPageStateChanged': + return handleEventFiltersListPageChanges(state, action); + case 'eventFiltersListPageDataChanged': + return handleEventFiltersListPageDataChanges(state, action); + case 'eventFiltersListPageDataExistsChanged': + return handleEventFiltersListPageDataExistChanges(state, action); + } + } + return state; }; diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selector.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selector.ts index dca16be0b1668..671e39bd3f950 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selector.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selector.ts @@ -5,7 +5,9 @@ * 2.0. */ -import { EventFiltersListPageState, EventFiltersPageLocation } from '../state'; +import { createSelector } from 'reselect'; +import { Pagination } from '@elastic/eui'; +import { EventFiltersListPageState } from '../state'; import { ExceptionListItemSchema, CreateExceptionListItemSchema } from '../../../../shared_imports'; import { ServerApiError } from '../../../../common/types'; import { @@ -13,6 +15,111 @@ import { isLoadedResourceState, isFailedResourceState, } from '../../../state/async_resource_state'; +import { FoundExceptionListItemSchema } from '../../../../../../lists/common/schemas'; +import { + MANAGEMENT_DEFAULT_PAGE_SIZE, + MANAGEMENT_PAGE_SIZE_OPTIONS, +} from '../../../common/constants'; +import { Immutable } from '../../../../../common/endpoint/types'; +import { EventFiltersServiceGetListOptions } from '../types'; + +type StoreState = Immutable; +type EventFiltersSelector = (state: StoreState) => T; + +export const getCurrentListPageState: EventFiltersSelector = (state) => { + return state.listPage; +}; + +export const getListPageIsActive: EventFiltersSelector = createSelector( + getCurrentListPageState, + (listPage) => listPage.active +); + +export const getCurrentListPageDataState: EventFiltersSelector = ( + state +) => state.listPage.data; + +/** + * Will return the API response with event filters. If the current state is attempting to load a new + * page of content, then return the previous API response if we have one + */ +export const getListApiSuccessResponse: EventFiltersSelector< + Immutable | undefined +> = createSelector(getCurrentListPageDataState, (listPageData) => { + if (isLoadedResourceState(listPageData)) { + return listPageData.data.content; + } else if ( + isLoadingResourceState(listPageData) && + isLoadedResourceState(listPageData.previousState) + ) { + return listPageData.previousState.data.content; + } + return undefined; +}); + +export const getListItems: EventFiltersSelector< + Immutable +> = createSelector(getListApiSuccessResponse, (apiResponseData) => { + return apiResponseData?.data || []; +}); + +/** + * Will return the query that was used with the currently displayed list of content. If a new page + * of content is being loaded, this selector will then attempt to use the previousState to return + * the query used. + */ +export const getCurrentListItemsQuery: EventFiltersSelector = createSelector( + getCurrentListPageDataState, + (pageDataState) => { + return ( + (isLoadedResourceState(pageDataState) && pageDataState.data.query) || + (isLoadingResourceState(pageDataState) && + isLoadedResourceState(pageDataState.previousState) && + pageDataState.previousState.data.query) || + {} + ); + } +); + +export const getListPagination: EventFiltersSelector = createSelector( + getListApiSuccessResponse, + // memoized via `reselect` until the API response changes + (response) => { + return { + totalItemCount: response?.total ?? 0, + pageSize: response?.per_page ?? MANAGEMENT_DEFAULT_PAGE_SIZE, + pageSizeOptions: [...MANAGEMENT_PAGE_SIZE_OPTIONS], + pageIndex: (response?.page ?? 1) - 1, + }; + } +); + +export const getListFetchError: EventFiltersSelector< + Immutable | undefined +> = createSelector(getCurrentListPageDataState, (listPageDataState) => { + return (isFailedResourceState(listPageDataState) && listPageDataState.error) || undefined; +}); + +export const getListIsLoading: EventFiltersSelector = createSelector( + getCurrentListPageDataState, + (listDataState) => isLoadingResourceState(listDataState) +); + +export const getListPageDataExistsState: EventFiltersSelector< + StoreState['listPage']['dataExist'] +> = ({ listPage: { dataExist } }) => dataExist; + +export const getListPageDoesDataExist: EventFiltersSelector = createSelector( + getListPageDataExistsState, + (dataExistsState) => { + if (isLoadedResourceState(dataExistsState)) { + return dataExistsState.data; + } + + // Until we know for sure that data exists (LoadedState), we assume `true` + return true; + } +); export const getFormEntry = ( state: EventFiltersListPageState @@ -38,5 +145,19 @@ export const getCreationError = (state: EventFiltersListPageState): ServerApiErr return isFailedResourceState(submissionResourceState) ? submissionResourceState.error : undefined; }; -export const getCurrentLocation = (state: EventFiltersListPageState): EventFiltersPageLocation => +export const getCurrentLocation: EventFiltersSelector = (state) => state.location; + +/** Compares the URL param values to the values used in the last data query */ +export const listDataNeedsRefresh: EventFiltersSelector = createSelector( + getCurrentLocation, + getCurrentListItemsQuery, + (state) => state.listPage.forceRefresh, + (location, currentQuery, forceRefresh) => { + return ( + forceRefresh || + location.page_index + 1 !== currentQuery.page || + location.page_size !== currentQuery.perPage + ); + } +); diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selectors.test.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selectors.test.ts index dbc962f1beaf1..a058c95388faa 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selectors.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/selectors.test.ts @@ -6,15 +6,236 @@ */ import { initialEventFiltersPageState } from './builders'; -import { getFormEntry, getFormHasError, getCurrentLocation } from './selector'; +import { + getFormEntry, + getFormHasError, + getCurrentLocation, + getCurrentListPageState, + getListPageIsActive, + getCurrentListPageDataState, + getListApiSuccessResponse, + getListItems, + getCurrentListItemsQuery, + getListPagination, + getListFetchError, + getListIsLoading, + getListPageDoesDataExist, + listDataNeedsRefresh, +} from './selector'; import { ecsEventMock } from '../test_utils'; import { getInitialExceptionFromEvent } from './utils'; -import { EventFiltersPageLocation } from '../state'; +import { EventFiltersListPageState, EventFiltersPageLocation } from '../state'; import { MANAGEMENT_DEFAULT_PAGE, MANAGEMENT_DEFAULT_PAGE_SIZE } from '../../../common/constants'; +import { getFoundExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/found_exception_list_item_schema.mock'; +import { + createFailedResourceState, + createLoadedResourceState, + createLoadingResourceState, + createUninitialisedResourceState, + getLastLoadedResourceState, +} from '../../../state'; -const initialState = initialEventFiltersPageState(); +describe('event filters selectors', () => { + let initialState: EventFiltersListPageState; + + // When `setToLoadingState()` is called, this variable will hold the prevousState in order to + // avoid ts-ignores due to know issues (#830) around the LoadingResourceState + let previousStateWhileLoading: EventFiltersListPageState['listPage']['data'] | undefined; + + const setToLoadedState = () => { + initialState.listPage.data = createLoadedResourceState({ + query: { page: 2, perPage: 10 }, + content: getFoundExceptionListItemSchemaMock(), + }); + }; + + const setToLoadingState = ( + previousState: EventFiltersListPageState['listPage']['data'] = createLoadedResourceState({ + query: { page: 5, perPage: 50 }, + content: getFoundExceptionListItemSchemaMock(), + }) + ) => { + previousStateWhileLoading = previousState; + + // will be fixed when AsyncResourceState is refactored (#830) + // @ts-ignore + initialState.listPage.data = createLoadingResourceState(previousState); + }; + + beforeEach(() => { + initialState = initialEventFiltersPageState(); + }); + + describe('getCurrentListPageState()', () => { + it('should retrieve list page state', () => { + expect(getCurrentListPageState(initialState)).toEqual(initialState.listPage); + }); + }); + + describe('getListPageIsActive()', () => { + it('should return active state', () => { + expect(getListPageIsActive(initialState)).toBe(false); + }); + }); + + describe('getCurrentListPageDataState()', () => { + it('should return list data state', () => { + expect(getCurrentListPageDataState(initialState)).toEqual(initialState.listPage.data); + }); + }); + + describe('getListApiSuccessResponse()', () => { + it('should return api response', () => { + setToLoadedState(); + expect(getListApiSuccessResponse(initialState)).toEqual( + getLastLoadedResourceState(initialState.listPage.data)?.data.content + ); + }); + + it('should return undefined if not available', () => { + setToLoadingState(createUninitialisedResourceState()); + expect(getListApiSuccessResponse(initialState)).toBeUndefined(); + }); + + it('should return previous success response if currently loading', () => { + setToLoadingState(); + expect(getListApiSuccessResponse(initialState)).toEqual( + getLastLoadedResourceState(previousStateWhileLoading!)?.data.content + ); + }); + }); + + describe('getListItems()', () => { + it('should return the list items from api response', () => { + setToLoadedState(); + expect(getListItems(initialState)).toEqual( + getLastLoadedResourceState(initialState.listPage.data)?.data.content.data + ); + }); + + it('should return empty array if no api response', () => { + expect(getListItems(initialState)).toEqual([]); + }); + }); + + describe('getCurrentListItemsQuery()', () => { + it('should return empty object if Uninitialized', () => { + expect(getCurrentListItemsQuery(initialState)).toEqual({}); + }); + + it('should return query from current loaded state', () => { + setToLoadedState(); + expect(getCurrentListItemsQuery(initialState)).toEqual({ page: 2, perPage: 10 }); + }); + + it('should return query from previous state while Loading new page', () => { + setToLoadingState(); + expect(getCurrentListItemsQuery(initialState)).toEqual({ page: 5, perPage: 50 }); + }); + }); + + describe('getListPagination()', () => { + it('should return pagination defaults if no API response is available', () => { + expect(getListPagination(initialState)).toEqual({ + totalItemCount: 0, + pageSize: 10, + pageSizeOptions: [10, 20, 50], + pageIndex: 0, + }); + }); + + it('should return pagination based on API response', () => { + setToLoadedState(); + expect(getListPagination(initialState)).toEqual({ + totalItemCount: 1, + pageSize: 1, + pageSizeOptions: [10, 20, 50], + pageIndex: 0, + }); + }); + }); + + describe('getListFetchError()', () => { + it('should return undefined if no error exists', () => { + expect(getListFetchError(initialState)).toBeUndefined(); + }); + + it('should return the API error', () => { + const error = { + statusCode: 500, + error: 'Internal Server Error', + message: 'Something is not right', + }; + + initialState.listPage.data = createFailedResourceState(error); + expect(getListFetchError(initialState)).toBe(error); + }); + }); + + describe('getListIsLoading()', () => { + it('should return false if not in a Loading state', () => { + expect(getListIsLoading(initialState)).toBe(false); + }); + + it('should return true if in a Loading state', () => { + setToLoadingState(); + expect(getListIsLoading(initialState)).toBe(true); + }); + }); + + describe('getListPageDoesDataExist()', () => { + it('should return true (default) until we get a Loaded Resource state', () => { + expect(getListPageDoesDataExist(initialState)).toBe(true); + + // Set DataExists to Loading + // ts-ignore will be fixed when AsyncResourceState is refactored (#830) + // @ts-ignore + initialState.listPage.dataExist = createLoadingResourceState(initialState.listPage.dataExist); + expect(getListPageDoesDataExist(initialState)).toBe(true); + + // Set DataExists to Failure + initialState.listPage.dataExist = createFailedResourceState({ + statusCode: 500, + error: 'Internal Server Error', + message: 'Something is not right', + }); + expect(getListPageDoesDataExist(initialState)).toBe(true); + }); + + it('should return false if no data exists', () => { + initialState.listPage.dataExist = createLoadedResourceState(false); + expect(getListPageDoesDataExist(initialState)).toBe(false); + }); + }); + + describe('listDataNeedsRefresh()', () => { + beforeEach(() => { + setToLoadedState(); + + initialState.location = { + page_index: 1, + page_size: 10, + filter: '', + id: '', + show: undefined, + }; + }); + + it('should return false if location url params match those that were used in api call', () => { + expect(listDataNeedsRefresh(initialState)).toBe(false); + }); + + it('should return true if `forceRefresh` is set', () => { + initialState.listPage.forceRefresh = true; + expect(listDataNeedsRefresh(initialState)).toBe(true); + }); + + it('should should return true if any of the url params differ from last api call', () => { + initialState.location.page_index = 10; + expect(listDataNeedsRefresh(initialState)).toBe(true); + }); + }); -describe('selectors', () => { describe('getFormEntry()', () => { it('returns undefined when there is no entry', () => { expect(getFormEntry(initialState)).toBe(undefined); diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/types.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/types.ts index 0aceca88efbc7..cad24b9f6ccf6 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/types.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/types.ts @@ -5,7 +5,29 @@ * 2.0. */ +import { Immutable } from '../../../../common/endpoint/types'; +import { + CreateExceptionListItemSchema, + ExceptionListItemSchema, +} from '../../../../../lists/common'; +import { FoundExceptionListItemSchema } from '../../../../../lists/common/schemas'; + export interface EventFiltersListPageUrlSearchParams { page_index: number; page_size: number; } + +export type EventFiltersServiceGetListOptions = Partial<{ + page: number; + perPage: number; + sortField: keyof ExceptionListItemSchema; + sortOrder: 'asc' | 'desc'; +}>; + +export interface EventFiltersService { + addEventFilters( + exception: Immutable + ): Promise; + + getList(options?: EventFiltersServiceGetListOptions): Promise; +} diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/empty/index.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/empty/index.tsx index 5298578c38e17..81a119af34e5b 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/empty/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/empty/index.tsx @@ -47,7 +47,7 @@ export const EventFiltersListEmptyState = memo<{ data-test-subj="eventFiltersListAddButton" > diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx index ac38b57fdb635..d32a4cbe1ca24 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx @@ -8,14 +8,51 @@ import React, { memo, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { AdministrationListPage } from '../../../components/administration_list_page'; +import { EuiButton } from '@elastic/eui'; +import styled from 'styled-components'; +import { AdministrationListPage as _AdministrationListPage } from '../../../components/administration_list_page'; import { EventFiltersListEmptyState } from './components/empty'; import { useEventFiltersNavigateCallback, useEventFiltersSelector } from './hooks'; -import { getCurrentLocation } from '../store/selector'; import { EventFiltersFlyout } from './components/flyout'; +import { + getListFetchError, + getListIsLoading, + getListItems, + getListPagination, + getCurrentLocation, + getListPageDoesDataExist, +} from '../store/selector'; +import { PaginatedContent, PaginatedContentProps } from '../../../components/paginated_content'; +import { ExceptionListItemSchema } from '../../../../../../lists/common'; +import { Immutable } from '../../../../../common/endpoint/types'; +import { + ExceptionItem, + ExceptionItemProps, +} from '../../../../common/components/exceptions/viewer/exception_item'; + +type EventListPaginatedContent = PaginatedContentProps< + Immutable, + typeof ExceptionItem +>; + +const AdministrationListPage = styled(_AdministrationListPage)` + .event-filter-container > * { + margin-bottom: ${({ theme }) => theme.eui.spacerSizes.l}; + + &:last-child { + margin-bottom: 0; + } + } +`; export const EventFiltersListPage = memo(() => { + const listItems = useEventFiltersSelector(getListItems); + const pagination = useEventFiltersSelector(getListPagination); + const isLoading = useEventFiltersSelector(getListIsLoading); + const fetchError = useEventFiltersSelector(getListFetchError); const location = useEventFiltersSelector(getCurrentLocation); + const doesDataExist = useEventFiltersSelector(getListPageDoesDataExist); + const navigateCallback = useEventFiltersNavigateCallback(); const showFlyout = !!location.show; @@ -28,7 +65,7 @@ export const EventFiltersListPage = memo(() => { [navigateCallback] ); - const handleCancelButtonClick = useCallback( + const handleAddCancelButtonClick = useCallback( () => navigateCallback({ show: undefined, @@ -36,6 +73,38 @@ export const EventFiltersListPage = memo(() => { }), [navigateCallback] ); + + const handleItemEdit: ExceptionItemProps['onEditException'] = useCallback((item) => { + // TODO: implement edit item + }, []); + + const handleItemDelete: ExceptionItemProps['onDeleteException'] = useCallback((args) => { + // TODO: implement delete item + }, []); + + const handleItemComponentProps: EventListPaginatedContent['itemComponentProps'] = useCallback( + (exceptionItem) => ({ + exceptionItem: exceptionItem as ExceptionListItemSchema, + loadingItemIds: [], + commentsAccordionId: '', + onEditException: handleItemEdit, + onDeleteException: handleItemDelete, + showModified: true, + showName: true, + }), + [handleItemDelete, handleItemEdit] + ); + + const handlePaginatedContentChange: EventListPaginatedContent['onChange'] = useCallback( + ({ pageIndex, pageSize }) => { + navigateCallback({ + page_index: pageIndex, + page_size: pageSize, + }); + }, + [navigateCallback] + ); + return ( { /> } subtitle={i18n.translate('xpack.securitySolution.eventFilters.aboutInfo', { - defaultMessage: 'Something here about Event Filtering....', + defaultMessage: + 'Add an event filter to exclude high volume or unwanted events from being written to Elasticsearch. Event ' + + 'filters are processed by the Endpoint Security integration, and are applied to hosts running this integration on their agents.', })} + actions={ + doesDataExist && ( + + + + ) + } > - {/* */} - {/* TODO: Display this only when list is empty (there are no endpoint events) */} - - {showFlyout ? : null} + {showFlyout && } + + , typeof ExceptionItem> + items={listItems} + ItemComponent={ExceptionItem} + itemComponentProps={handleItemComponentProps} + onChange={handlePaginatedContentChange} + error={fetchError?.message} + loading={isLoading} + pagination={pagination} + contentClassName="event-filter-container" + noItemsMessage={ + !doesDataExist && ( + + ) + } + /> ); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/hooks.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/hooks.ts index df87d150891c1..bf2e187c127c2 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/hooks.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/hooks.ts @@ -20,7 +20,7 @@ import { useToasts } from '../../../../common/lib/kibana'; import { getCreationSuccessMessage, getCreationErrorMessage } from './translations'; import { State } from '../../../../common/store'; -import { EventFiltersListPageState } from '../state'; +import { EventFiltersListPageState, EventFiltersPageLocation } from '../state'; import { getEventFiltersListPath } from '../../../common/routing'; import { @@ -54,8 +54,9 @@ export function useEventFiltersNavigateCallback() { const location = useEventFiltersSelector(getCurrentLocation); const history = useHistory(); - return useCallback((args) => history.push(getEventFiltersListPath({ ...location, ...args })), [ - history, - location, - ]); + return useCallback( + (args: Partial) => + history.push(getEventFiltersListPath({ ...location, ...args })), + [history, location] + ); } diff --git a/x-pack/plugins/security_solution/public/management/pages/index.tsx b/x-pack/plugins/security_solution/public/management/pages/index.tsx index 9e1a90a031d99..4be75117daeda 100644 --- a/x-pack/plugins/security_solution/public/management/pages/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/index.tsx @@ -5,12 +5,9 @@ * 2.0. */ -import { isEmpty } from 'lodash/fp'; import React, { memo } from 'react'; -import { useHistory, Route, Switch } from 'react-router-dom'; - -import { ChromeBreadcrumb } from 'kibana/public'; -import { EuiText, EuiEmptyPrompt } from '@elastic/eui'; +import { Route, Switch, useHistory } from 'react-router-dom'; +import { EuiEmptyPrompt, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { MANAGEMENT_ROUTING_ENDPOINTS_PATH, @@ -24,48 +21,12 @@ import { EndpointsContainer } from './endpoint_hosts'; import { PolicyContainer } from './policy'; import { TrustedAppsContainer } from './trusted_apps'; import { getEndpointListPath } from '../common/routing'; -import { APP_ID, SecurityPageName } from '../../../common/constants'; -import { GetUrlForApp } from '../../common/components/navigation/types'; -import { AdministrationRouteSpyState } from '../../common/utils/route/types'; -import { ADMINISTRATION } from '../../app/home/translations'; -import { AdministrationSubTab } from '../types'; -import { - ENDPOINTS_TAB, - EVENT_FILTERS_TAB, - POLICIES_TAB, - TRUSTED_APPS_TAB, -} from '../common/translations'; +import { SecurityPageName } from '../../../common/constants'; import { SpyRoute } from '../../common/utils/route/spy_routes'; import { useIngestEnabledCheck } from '../../common/hooks/endpoint/ingest_enabled'; import { EventFiltersContainer } from './event_filters'; import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features'; -const TabNameMappedToI18nKey: Record = { - [AdministrationSubTab.endpoints]: ENDPOINTS_TAB, - [AdministrationSubTab.policies]: POLICIES_TAB, - [AdministrationSubTab.trustedApps]: TRUSTED_APPS_TAB, - [AdministrationSubTab.eventFilters]: EVENT_FILTERS_TAB, -}; - -export function getBreadcrumbs( - params: AdministrationRouteSpyState, - search: string[], - getUrlForApp: GetUrlForApp -): ChromeBreadcrumb[] { - return [ - { - text: ADMINISTRATION, - href: getUrlForApp(`${APP_ID}:${SecurityPageName.administration}`, { - path: !isEmpty(search[0]) ? search[0] : '', - }), - }, - ...(params?.tabName ? [params?.tabName] : []).map((tabName) => ({ - text: TabNameMappedToI18nKey[tabName], - href: '', - })), - ]; -} - const NoPermissions = memo(() => { return ( <> diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap index c4a58a3b99d3f..31ce0bc208827 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/__snapshots__/index.test.tsx.snap @@ -3,6 +3,7 @@ exports[`TrustedAppsGrid renders correctly initially 1`] = ` .c1 { position: relative; + padding-top: 4px; } .c1 .body { @@ -27,9 +28,6 @@ exports[`TrustedAppsGrid renders correctly initially 1`] = `
-
@@ -49,108 +47,6 @@ exports[`TrustedAppsGrid renders correctly initially 1`] = `
-
-
-
-
-
-
- -
-
-
-
- -
-
-
@@ -159,6 +55,7 @@ exports[`TrustedAppsGrid renders correctly initially 1`] = ` exports[`TrustedAppsGrid renders correctly when failed loading data for the first time 1`] = ` .c1 { position: relative; + padding-top: 4px; } .c1 .body { @@ -183,9 +80,6 @@ exports[`TrustedAppsGrid renders correctly when failed loading data for the firs
-
@@ -211,108 +105,6 @@ exports[`TrustedAppsGrid renders correctly when failed loading data for the firs
-
-
-
-
-
-
- -
-
-
-
- -
-
-
@@ -321,6 +113,7 @@ exports[`TrustedAppsGrid renders correctly when failed loading data for the firs exports[`TrustedAppsGrid renders correctly when failed loading data for the second time 1`] = ` .c1 { position: relative; + padding-top: 4px; } .c1 .body { @@ -345,9 +138,6 @@ exports[`TrustedAppsGrid renders correctly when failed loading data for the seco
-
@@ -605,6 +395,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` .c1 { position: relative; + padding-top: 4px; } .c1 .body { @@ -629,9 +420,6 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = `
-
@@ -3542,6 +3330,7 @@ exports[`TrustedAppsGrid renders correctly when loaded data 1`] = ` exports[`TrustedAppsGrid renders correctly when loading data for the first time 1`] = ` .c1 { position: relative; + padding-top: 4px; } .c1 .body { @@ -3564,14 +3353,11 @@ exports[`TrustedAppsGrid renders correctly when loading data for the first time class="c1" >
-
@@ -3591,108 +3377,6 @@ exports[`TrustedAppsGrid renders correctly when loading data for the first time
-
-
-
-
-
-
- -
-
-
-
- -
-
-
@@ -3718,6 +3402,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time .c1 { position: relative; + padding-top: 4px; } .c1 .body { @@ -3740,14 +3425,11 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time class="c1" >
-
@@ -6675,6 +6357,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not .c1 { position: relative; + padding-top: 4px; } .c1 .body { @@ -6699,9 +6382,6 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not
-
diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts index fc031a63b84b1..54f803f5af317 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts @@ -18,8 +18,8 @@ export { OS_TITLES } from '../../../common/translations'; export const ABOUT_TRUSTED_APPS = i18n.translate('xpack.securitySolution.trustedapps.aboutInfo', { defaultMessage: - 'Add a trusted application to improve performance or alleviate conflicts with other applications ' + - 'running on your hosts. Trusted applications will be applied to hosts running Endpoint Security.', + 'Add a trusted application to improve performance or alleviate conflicts with other applications running on ' + + 'your hosts. Trusted applications are applied to hosts running the Endpoint Security integration on their agents.', }); export const CONDITION_FIELD_TITLE: { [K in ConditionEntryField]: string } = { diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx index f9fc5f32aa63a..5efe8cfc16185 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx @@ -45,7 +45,9 @@ const useIsExperimentalFeatureEnabledMock = useIsExperimentalFeatureEnabled as j describe('When on the Trusted Apps Page', () => { const expectedAboutInfo = - 'Add a trusted application to improve performance or alleviate conflicts with other applications running on your hosts. Trusted applications will be applied to hosts running Endpoint Security.'; + 'Add a trusted application to improve performance or alleviate conflicts with other ' + + 'applications running on your hosts. Trusted applications are applied to hosts running the Endpoint Security ' + + 'integration on their agents.'; const generator = new EndpointDocGenerator('policy-list'); diff --git a/x-pack/plugins/security_solution/public/management/state/async_resource_builders.ts b/x-pack/plugins/security_solution/public/management/state/async_resource_builders.ts new file mode 100644 index 0000000000000..f7de2f68aecd9 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/state/async_resource_builders.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + FailedResourceState, + LoadedResourceState, + LoadingResourceState, + StaleResourceState, + UninitialisedResourceState, +} from './async_resource_state'; + +export const createUninitialisedResourceState = (): UninitialisedResourceState => { + return { type: 'UninitialisedResourceState' }; +}; + +export const createLoadingResourceState = ( + previousState: StaleResourceState +): LoadingResourceState => { + return { + type: 'LoadingResourceState', + previousState, + }; +}; + +export const createLoadedResourceState = (data: Data): LoadedResourceState => { + return { + type: 'LoadedResourceState', + data, + }; +}; + +export const createFailedResourceState = ( + error: Error, + lastLoadedState?: LoadedResourceState +): FailedResourceState => { + return { + type: 'FailedResourceState', + error, + lastLoadedState, + }; +}; diff --git a/x-pack/plugins/security_solution/public/management/state/index.ts b/x-pack/plugins/security_solution/public/management/state/index.ts new file mode 100644 index 0000000000000..dbaad8d266886 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/state/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './async_resource_state'; +export * from './async_resource_builders'; diff --git a/x-pack/plugins/security_solution/scripts/beat_docs/build.js b/x-pack/plugins/security_solution/scripts/beat_docs/build.js index 15b1ec7c84d81..b8bcedda9356a 100644 --- a/x-pack/plugins/security_solution/scripts/beat_docs/build.js +++ b/x-pack/plugins/security_solution/scripts/beat_docs/build.js @@ -26,35 +26,32 @@ const zlib = require('zlib'); const OUTPUT_DIRECTORY = resolve('scripts', 'beat_docs'); const OUTPUT_SERVER_DIRECTORY = resolve('server', 'utils', 'beat_schema'); +const BEATS_VERSION = '7.12.0'; const beats = [ { - filePath: `${OUTPUT_DIRECTORY}/auditbeat-7.9.0-darwin-x86_64.tar.gz`, + filePath: `${OUTPUT_DIRECTORY}/auditbeat-${BEATS_VERSION}-darwin-x86_64.tar.gz`, index: 'auditbeat-*', - outputDir: `${OUTPUT_DIRECTORY}/auditbeat-7.9.0-darwin-x86_64`, - url: - 'https://artifacts.elastic.co/downloads/beats/auditbeat/auditbeat-7.9.0-darwin-x86_64.tar.gz', + outputDir: `${OUTPUT_DIRECTORY}/auditbeat-${BEATS_VERSION}-darwin-x86_64`, + url: `https://artifacts.elastic.co/downloads/beats/auditbeat/auditbeat-${BEATS_VERSION}-darwin-x86_64.tar.gz`, }, { - filePath: `${OUTPUT_DIRECTORY}/filebeat-7.9.0-darwin-x86_64.tar.gz`, + filePath: `${OUTPUT_DIRECTORY}/filebeat-${BEATS_VERSION}-darwin-x86_64.tar.gz`, index: 'filebeat-*', - outputDir: `${OUTPUT_DIRECTORY}/filebeat-7.9.0-darwin-x86_64`, - url: - 'https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-7.9.0-darwin-x86_64.tar.gz', + outputDir: `${OUTPUT_DIRECTORY}/filebeat-${BEATS_VERSION}-darwin-x86_64`, + url: `https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-${BEATS_VERSION}-darwin-x86_64.tar.gz`, }, { - filePath: `${OUTPUT_DIRECTORY}/packetbeat-7.9.0-darwin-x86_64.tar.gz`, + filePath: `${OUTPUT_DIRECTORY}/packetbeat-${BEATS_VERSION}-darwin-x86_64.tar.gz`, index: 'packetbeat-*', - outputDir: `${OUTPUT_DIRECTORY}/packetbeat-7.9.0-darwin-x86_64`, - url: - 'https://artifacts.elastic.co/downloads/beats/packetbeat/packetbeat-7.9.0-darwin-x86_64.tar.gz', + outputDir: `${OUTPUT_DIRECTORY}/packetbeat-${BEATS_VERSION}-darwin-x86_64`, + url: `https://artifacts.elastic.co/downloads/beats/packetbeat/packetbeat-${BEATS_VERSION}-darwin-x86_64.tar.gz`, }, { - filePath: `${OUTPUT_DIRECTORY}/winlogbeat-7.9.0-windows-x86_64.zip`, + filePath: `${OUTPUT_DIRECTORY}/winlogbeat-${BEATS_VERSION}-windows-x86_64.zip`, index: 'winlogbeat-*', outputDir: `${OUTPUT_DIRECTORY}`, - url: - 'https://artifacts.elastic.co/downloads/beats/winlogbeat/winlogbeat-7.9.0-windows-x86_64.zip', + url: `https://artifacts.elastic.co/downloads/beats/winlogbeat/winlogbeat-${BEATS_VERSION}-windows-x86_64.zip`, }, ]; @@ -141,13 +138,13 @@ const manageZipFields = async (beat, filePath, beatFields) => { await extract(filePath, { dir: beat.outputDir }); console.log('building fields', beat.index); const obj = yaml.load( - fs.readFileSync(`${beat.outputDir}/winlogbeat-7.9.0-windows-x86_64/fields.yml`, { + fs.readFileSync(`${beat.outputDir}/winlogbeat-${BEATS_VERSION}-windows-x86_64/fields.yml`, { encoding: 'utf-8', }) ); const eBeatFields = convertSchemaToHash(obj, beatFields); console.log('deleting files', beat.index); - rimraf.sync(`${beat.outputDir}/winlogbeat-7.9.0-windows-x86_64`); + rimraf.sync(`${beat.outputDir}/winlogbeat-${BEATS_VERSION}-windows-x86_64`); rimraf.sync(beat.filePath); return eBeatFields; @@ -221,7 +218,7 @@ async function main() { * 2.0. */ - import { BeatFields } from '../../../common/search_strategy/security_solution/beat_fields'; + import { BeatFields } from '../../../common/search_strategy/index_fields'; /* eslint-disable @typescript-eslint/naming-convention */ export const fieldsBeat: BeatFields = diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.ts index 56ce810a89412..b883b7b3462e7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.ts @@ -68,8 +68,8 @@ export const updateRulesRoute = (router: SecuritySolutionPluginRouter, ml: Setup alertsClient, savedObjectsClient, enabled: request.body.enabled ?? true, - actions: request.body.actions, - throttle: request.body.throttle, + actions: request.body.actions ?? [], + throttle: request.body.throttle ?? 'no_actions', name: request.body.name, }); const ruleStatuses = await ruleStatusClient.find({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts index b0c8cd6c4dd69..38cae8d1cf50f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts @@ -33,7 +33,6 @@ export const updateRules = async ({ } const typeSpecificParams = typeSpecificSnakeToCamel(ruleUpdate); - const throttle = ruleUpdate.throttle ?? null; const enabled = ruleUpdate.enabled ?? true; const newInternalRule: InternalRuleUpdate = { name: ruleUpdate.name, @@ -73,7 +72,10 @@ export const updateRules = async ({ ...typeSpecificParams, }, schedule: { interval: ruleUpdate.interval ?? '5m' }, - actions: throttle === 'rule' ? (ruleUpdate.actions ?? []).map(transformRuleToAlertAction) : [], + actions: + ruleUpdate.throttle === 'rule' + ? (ruleUpdate.actions ?? []).map(transformRuleToAlertAction) + : [], throttle: null, notifyWhen: null, }; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/utils/common.ts b/x-pack/plugins/security_solution/server/lib/timeline/utils/common.ts index 18fa84c9cf3ae..f457a1a11422c 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/utils/common.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/utils/common.ts @@ -40,6 +40,9 @@ export const buildFrameworkRequest = async ( export const escapeHatch = schema.object({}, { unknowns: 'allow' }); +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils/src/format_errors/index.ts + */ export const formatErrors = (errors: rt.Errors): string[] => { const err = errors.map((error) => { if (error.message != null) { diff --git a/x-pack/plugins/security_solution/server/search_strategy/index_fields/index.test.ts b/x-pack/plugins/security_solution/server/search_strategy/index_fields/index.test.ts index f6d78f2f1259f..51892a1a05d55 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/index_fields/index.test.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/index_fields/index.test.ts @@ -126,7 +126,7 @@ describe('Index Fields', () => { }, { description: - 'Type of the agent. The agent type stays always the same and should be given by the agent used. In case of Filebeat the agent would always be Filebeat also if two Filebeat instances are run on the same machine.', + 'Type of the agent. The agent type always stays the same and should be given by the agent used. In case of Filebeat the agent would always be Filebeat also if two Filebeat instances are run on the same machine.', example: 'filebeat', name: 'agent.type', type: 'string', @@ -252,7 +252,7 @@ describe('Index Fields', () => { { category: 'agent', description: - 'Type of the agent. The agent type stays always the same and should be given by the agent used. In case of Filebeat the agent would always be Filebeat also if two Filebeat instances are run on the same machine.', + 'Type of the agent. The agent type always stays the same and should be given by the agent used. In case of Filebeat the agent would always be Filebeat also if two Filebeat instances are run on the same machine.', example: 'filebeat', name: 'agent.type', type: 'string', @@ -426,7 +426,7 @@ describe('Index Fields', () => { { category: 'agent', description: - 'Type of the agent. The agent type stays always the same and should be given by the agent used. In case of Filebeat the agent would always be Filebeat also if two Filebeat instances are run on the same machine.', + 'Type of the agent. The agent type always stays the same and should be given by the agent used. In case of Filebeat the agent would always be Filebeat also if two Filebeat instances are run on the same machine.', example: 'filebeat', name: 'agent.type', type: 'string', diff --git a/x-pack/plugins/security_solution/server/utils/beat_schema/fields.ts b/x-pack/plugins/security_solution/server/utils/beat_schema/fields.ts index 4f1dc0079b236..e308c8866c9d3 100644 --- a/x-pack/plugins/security_solution/server/utils/beat_schema/fields.ts +++ b/x-pack/plugins/security_solution/server/utils/beat_schema/fields.ts @@ -55,6 +55,15 @@ export const fieldsBeat: BeatFields = { name: 'tags', type: 'keyword', }, + 'agent.build.original': { + category: 'agent', + description: + 'Extended build information for the agent. This field is intended to contain any build information that a data source may provide, no specific formatting is required.', + example: + 'metricbeat version 7.6.0 (amd64), libbeat 7.6.0 [6a23e8f8f30f5001ba344e4e54d8d9cb82cb107c built 2020-02-05 23:10:10 +0000 UTC]', + name: 'agent.build.original', + type: 'keyword', + }, 'agent.ephemeral_id': { category: 'agent', description: @@ -82,7 +91,7 @@ export const fieldsBeat: BeatFields = { 'agent.type': { category: 'agent', description: - 'Type of the agent. The agent type stays always the same and should be given by the agent used. In case of Filebeat the agent would always be Filebeat also if two Filebeat instances are run on the same machine.', + 'Type of the agent. The agent type always stays the same and should be given by the agent used. In case of Filebeat the agent would always be Filebeat also if two Filebeat instances are run on the same machine.', example: 'filebeat', name: 'agent.type', type: 'keyword', @@ -204,7 +213,7 @@ export const fieldsBeat: BeatFields = { }, 'client.ip': { category: 'client', - description: 'IP address of the client. Can be one or multiple IPv4 or IPv6 addresses.', + description: 'IP address of the client (IPv4 or IPv6).', name: 'client.ip', type: 'ip', }, @@ -246,15 +255,23 @@ export const fieldsBeat: BeatFields = { 'client.registered_domain': { category: 'client', description: - 'The highest registered client domain, stripped of the subdomain. For example, the registered domain for "foo.google.com" is "google.com". This value can be determined precisely with a list like the public suffix list (http://publicsuffix.org). Trying to approximate this by simply taking the last two labels will not work well for TLDs such as "co.uk".', - example: 'google.com', + 'The highest registered client domain, stripped of the subdomain. For example, the registered domain for "foo.example.com" is "example.com". This value can be determined precisely with a list like the public suffix list (http://publicsuffix.org). Trying to approximate this by simply taking the last two labels will not work well for TLDs such as "co.uk".', + example: 'example.com', name: 'client.registered_domain', type: 'keyword', }, + 'client.subdomain': { + category: 'client', + description: + 'The subdomain portion of a fully qualified domain name includes all of the names except the host name under the registered_domain. In a partially qualified domain, or if the the qualification level of the full name cannot be determined, subdomain contains all of the names below the registered domain. For example the subdomain portion of "www.east.mydomain.co.uk" is "east". If the domain has multiple levels of subdomain, such as "sub2.sub1.example.com", the subdomain field should contain "sub2.sub1", with no trailing period.', + example: 'east', + name: 'client.subdomain', + type: 'keyword', + }, 'client.top_level_domain': { category: 'client', description: - 'The effective top level domain (eTLD), also known as the domain suffix, is the last part of the domain name. For example, the top level domain for google.com is "com". This value can be determined precisely with a list like the public suffix list (http://publicsuffix.org). Trying to approximate this by simply taking the last label will not work well for effective TLDs such as "co.uk".', + 'The effective top level domain (eTLD), also known as the domain suffix, is the last part of the domain name. For example, the top level domain for example.com is "com". This value can be determined precisely with a list like the public suffix list (http://publicsuffix.org). Trying to approximate this by simply taking the last label will not work well for effective TLDs such as "co.uk".', example: 'co.uk', name: 'client.top_level_domain', type: 'keyword', @@ -307,7 +324,7 @@ export const fieldsBeat: BeatFields = { }, 'client.user.id': { category: 'client', - description: 'Unique identifiers of the user.', + description: 'Unique identifier of the user.', name: 'client.user.id', type: 'keyword', }, @@ -318,6 +335,13 @@ export const fieldsBeat: BeatFields = { name: 'client.user.name', type: 'keyword', }, + 'client.user.roles': { + category: 'client', + description: 'Array of user roles at the time of the event.', + example: '["kibana_admin", "reporting_user"]', + name: 'client.user.roles', + type: 'keyword', + }, 'cloud.account.id': { category: 'cloud', description: @@ -326,6 +350,14 @@ export const fieldsBeat: BeatFields = { name: 'cloud.account.id', type: 'keyword', }, + 'cloud.account.name': { + category: 'cloud', + description: + 'The cloud account name or alias used to identify different entities in a multi-tenant environment. Examples: AWS account name, Google Cloud ORG display name.', + example: 'elastic-dev', + name: 'cloud.account.name', + type: 'keyword', + }, 'cloud.availability_zone': { category: 'cloud', description: 'Availability zone in which this host is running.', @@ -353,6 +385,21 @@ export const fieldsBeat: BeatFields = { name: 'cloud.machine.type', type: 'keyword', }, + 'cloud.project.id': { + category: 'cloud', + description: + 'The cloud project identifier. Examples: Google Cloud Project id, Azure Project id.', + example: 'my-project', + name: 'cloud.project.id', + type: 'keyword', + }, + 'cloud.project.name': { + category: 'cloud', + description: 'The cloud project name. Examples: Google Cloud Project name, Azure Project name.', + example: 'my project', + name: 'cloud.project.name', + type: 'keyword', + }, 'cloud.provider': { category: 'cloud', description: 'Name of the cloud provider. Example values are aws, azure, gcp, or digitalocean.', @@ -537,7 +584,7 @@ export const fieldsBeat: BeatFields = { }, 'destination.ip': { category: 'destination', - description: 'IP address of the destination. Can be one or multiple IPv4 or IPv6 addresses.', + description: 'IP address of the destination (IPv4 or IPv6).', name: 'destination.ip', type: 'ip', }, @@ -579,15 +626,23 @@ export const fieldsBeat: BeatFields = { 'destination.registered_domain': { category: 'destination', description: - 'The highest registered destination domain, stripped of the subdomain. For example, the registered domain for "foo.google.com" is "google.com". This value can be determined precisely with a list like the public suffix list (http://publicsuffix.org). Trying to approximate this by simply taking the last two labels will not work well for TLDs such as "co.uk".', - example: 'google.com', + 'The highest registered destination domain, stripped of the subdomain. For example, the registered domain for "foo.example.com" is "example.com". This value can be determined precisely with a list like the public suffix list (http://publicsuffix.org). Trying to approximate this by simply taking the last two labels will not work well for TLDs such as "co.uk".', + example: 'example.com', name: 'destination.registered_domain', type: 'keyword', }, + 'destination.subdomain': { + category: 'destination', + description: + 'The subdomain portion of a fully qualified domain name includes all of the names except the host name under the registered_domain. In a partially qualified domain, or if the the qualification level of the full name cannot be determined, subdomain contains all of the names below the registered domain. For example the subdomain portion of "www.east.mydomain.co.uk" is "east". If the domain has multiple levels of subdomain, such as "sub2.sub1.example.com", the subdomain field should contain "sub2.sub1", with no trailing period.', + example: 'east', + name: 'destination.subdomain', + type: 'keyword', + }, 'destination.top_level_domain': { category: 'destination', description: - 'The effective top level domain (eTLD), also known as the domain suffix, is the last part of the domain name. For example, the top level domain for google.com is "com". This value can be determined precisely with a list like the public suffix list (http://publicsuffix.org). Trying to approximate this by simply taking the last label will not work well for effective TLDs such as "co.uk".', + 'The effective top level domain (eTLD), also known as the domain suffix, is the last part of the domain name. For example, the top level domain for example.com is "com". This value can be determined precisely with a list like the public suffix list (http://publicsuffix.org). Trying to approximate this by simply taking the last label will not work well for effective TLDs such as "co.uk".', example: 'co.uk', name: 'destination.top_level_domain', type: 'keyword', @@ -640,7 +695,7 @@ export const fieldsBeat: BeatFields = { }, 'destination.user.id': { category: 'destination', - description: 'Unique identifiers of the user.', + description: 'Unique identifier of the user.', name: 'destination.user.id', type: 'keyword', }, @@ -651,6 +706,13 @@ export const fieldsBeat: BeatFields = { name: 'destination.user.name', type: 'keyword', }, + 'destination.user.roles': { + category: 'destination', + description: 'Array of user roles at the time of the event.', + example: '["kibana_admin", "reporting_user"]', + name: 'destination.user.roles', + type: 'keyword', + }, 'dll.code_signature.exists': { category: 'dll', description: 'Boolean to capture if a signature is present.', @@ -727,6 +789,13 @@ export const fieldsBeat: BeatFields = { name: 'dll.path', type: 'keyword', }, + 'dll.pe.architecture': { + category: 'dll', + description: 'CPU architecture target for the file.', + example: 'x64', + name: 'dll.pe.architecture', + type: 'keyword', + }, 'dll.pe.company': { category: 'dll', description: 'Internal company name of the file, provided at compile-time.', @@ -748,6 +817,14 @@ export const fieldsBeat: BeatFields = { name: 'dll.pe.file_version', type: 'keyword', }, + 'dll.pe.imphash': { + category: 'dll', + description: + 'A hash of the imports in a PE file. An imphash -- or import hash -- can be used to fingerprint binaries even after recompilation or other code-level transformations have occurred, which would change more traditional hash values. Learn more at https://www.fireeye.com/blog/threat-research/2014/01/tracking-malware-import-hashing.html.', + example: '0c6803c4e922103c4dca5963aad36ddf', + name: 'dll.pe.imphash', + type: 'keyword', + }, 'dll.pe.original_file_name': { category: 'dll', description: 'Internal name of the file, provided at compile-time.', @@ -788,7 +865,7 @@ export const fieldsBeat: BeatFields = { category: 'dns', description: "The domain name to which this resource record pertains. If a chain of CNAME is being resolved, each answer's `name` should be the one that corresponds with the answer's `data`. It should not simply be the original `question.name` repeated.", - example: 'www.google.com', + example: 'www.example.com', name: 'dns.answers.name', type: 'keyword', }, @@ -811,7 +888,7 @@ export const fieldsBeat: BeatFields = { category: 'dns', description: 'Array of 2 letter DNS header flags. Expected values are: AA, TC, RD, RA, AD, CD, DO.', - example: '["RD","RA"]', + example: '["RD", "RA"]', name: 'dns.header_flags', type: 'keyword', }, @@ -842,15 +919,15 @@ export const fieldsBeat: BeatFields = { category: 'dns', description: 'The name being queried. If the name field contains non-printable characters (below 32 or above 126), those characters should be represented as escaped base 10 integers (\\DDD). Back slashes and quotes should be escaped. Tabs, carriage returns, and line feeds should be converted to \\t, \\r, and \\n respectively.', - example: 'www.google.com', + example: 'www.example.com', name: 'dns.question.name', type: 'keyword', }, 'dns.question.registered_domain': { category: 'dns', description: - 'The highest registered domain, stripped of the subdomain. For example, the registered domain for "foo.google.com" is "google.com". This value can be determined precisely with a list like the public suffix list (http://publicsuffix.org). Trying to approximate this by simply taking the last two labels will not work well for TLDs such as "co.uk".', - example: 'google.com', + 'The highest registered domain, stripped of the subdomain. For example, the registered domain for "foo.example.com" is "example.com". This value can be determined precisely with a list like the public suffix list (http://publicsuffix.org). Trying to approximate this by simply taking the last two labels will not work well for TLDs such as "co.uk".', + example: 'example.com', name: 'dns.question.registered_domain', type: 'keyword', }, @@ -865,7 +942,7 @@ export const fieldsBeat: BeatFields = { 'dns.question.top_level_domain': { category: 'dns', description: - 'The effective top level domain (eTLD), also known as the domain suffix, is the last part of the domain name. For example, the top level domain for google.com is "com". This value can be determined precisely with a list like the public suffix list (http://publicsuffix.org). Trying to approximate this by simply taking the last label will not work well for effective TLDs such as "co.uk".', + 'The effective top level domain (eTLD), also known as the domain suffix, is the last part of the domain name. For example, the top level domain for example.com is "com". This value can be determined precisely with a list like the public suffix list (http://publicsuffix.org). Trying to approximate this by simply taking the last label will not work well for effective TLDs such as "co.uk".', example: 'co.uk', name: 'dns.question.top_level_domain', type: 'keyword', @@ -881,7 +958,7 @@ export const fieldsBeat: BeatFields = { category: 'dns', description: 'Array containing all IPs seen in `answers.data`. The `answers` array can be difficult to use, because of the variety of data formats it can contain. Extracting all IP addresses seen in there to `dns.resolved_ip` makes it possible to index them as IP addresses, and makes them easier to visualize and query for.', - example: '["10.10.10.10","10.10.10.11"]', + example: '["10.10.10.10", "10.10.10.11"]', name: 'dns.resolved_ip', type: 'ip', }, @@ -1036,7 +1113,7 @@ export const fieldsBeat: BeatFields = { 'event.original': { category: 'event', description: - 'Raw text message of entire event. Used to demonstrate log integrity. This field is not indexed and doc_values are disabled. It cannot be searched, but it can be retrieved from `_source`.', + 'Raw text message of entire event. Used to demonstrate log integrity. This field is not indexed and doc_values are disabled. It cannot be searched, but it can be retrieved from `_source`. If users wish to override this and index this field, consider using the wildcard data type.', example: 'Sep 19 08:26:10 host CEF:0|Security| threatmanager|1.0|100| worm successfully stopped|10|src=10.0.0.1 dst=2.1.2.2spt=1232', name: 'event.original', @@ -1058,11 +1135,19 @@ export const fieldsBeat: BeatFields = { name: 'event.provider', type: 'keyword', }, + 'event.reason': { + category: 'event', + description: + 'Reason why this event happened, according to the source. This describes the why of a particular action or outcome captured in the event. Where `event.action` captures the action from the event, `event.reason` describes why that action was taken. For example, a web proxy with an `event.action` which denied the request may also populate `event.reason` with the reason why (e.g. `blocked site`).', + example: 'Terminated an unexpected process', + name: 'event.reason', + type: 'keyword', + }, 'event.reference': { category: 'event', description: - 'Reference URL linking to additional information about this event. This URL links to a static definition of the this event. Alert events, indicated by `event.kind:alert`, are a common use case for this field.', - example: 'https://system.vendor.com/event/#0001234', + 'Reference URL linking to additional information about this event. This URL links to a static definition of this event. Alert events, indicated by `event.kind:alert`, are a common use case for this field.', + example: 'https://system.example.com/event/#0001234', name: 'event.reference', type: 'keyword', }, @@ -1121,8 +1206,8 @@ export const fieldsBeat: BeatFields = { 'event.url': { category: 'event', description: - 'URL linking to an external system to continue investigation of this event. This URL links to another system where in-depth investigation of the specific occurence of this event can take place. Alert events, indicated by `event.kind:alert`, are a common use case for this field.', - example: 'https://mysystem.mydomain.com/alert/5271dedb-f5b0-4218-87f0-4ac4870a38fe', + 'URL linking to an external system to continue investigation of this event. This URL links to another system where in-depth investigation of the specific occurrence of this event can take place. Alert events, indicated by `event.kind:alert`, are a common use case for this field.', + example: 'https://mysystem.example.com/alert/5271dedb-f5b0-4218-87f0-4ac4870a38fe', name: 'event.url', type: 'keyword', }, @@ -1217,7 +1302,8 @@ export const fieldsBeat: BeatFields = { }, 'file.extension': { category: 'file', - description: 'File extension.', + description: + 'File extension, excluding the leading dot. Note that when the file name has multiple extensions (example.tar.gz), only the last one should be captured ("gz", not "tar.gz").', example: 'png', name: 'file.extension', type: 'keyword', @@ -1309,6 +1395,13 @@ export const fieldsBeat: BeatFields = { name: 'file.path', type: 'keyword', }, + 'file.pe.architecture': { + category: 'file', + description: 'CPU architecture target for the file.', + example: 'x64', + name: 'file.pe.architecture', + type: 'keyword', + }, 'file.pe.company': { category: 'file', description: 'Internal company name of the file, provided at compile-time.', @@ -1330,6 +1423,14 @@ export const fieldsBeat: BeatFields = { name: 'file.pe.file_version', type: 'keyword', }, + 'file.pe.imphash': { + category: 'file', + description: + 'A hash of the imports in a PE file. An imphash -- or import hash -- can be used to fingerprint binaries even after recompilation or other code-level transformations have occurred, which would change more traditional hash values. Learn more at https://www.fireeye.com/blog/threat-research/2014/01/tracking-malware-import-hashing.html.', + example: '0c6803c4e922103c4dca5963aad36ddf', + name: 'file.pe.imphash', + type: 'keyword', + }, 'file.pe.original_file_name': { category: 'file', description: 'Internal name of the file, provided at compile-time.', @@ -1371,6 +1472,177 @@ export const fieldsBeat: BeatFields = { name: 'file.uid', type: 'keyword', }, + 'file.x509.alternative_names': { + category: 'file', + description: + 'List of subject alternative names (SAN). Name types vary by certificate authority and certificate type but commonly contain IP addresses, DNS names (and wildcards), and email addresses.', + example: '*.elastic.co', + name: 'file.x509.alternative_names', + type: 'keyword', + }, + 'file.x509.issuer.common_name': { + category: 'file', + description: 'List of common name (CN) of issuing certificate authority.', + example: 'Example SHA2 High Assurance Server CA', + name: 'file.x509.issuer.common_name', + type: 'keyword', + }, + 'file.x509.issuer.country': { + category: 'file', + description: 'List of country (C) codes', + example: 'US', + name: 'file.x509.issuer.country', + type: 'keyword', + }, + 'file.x509.issuer.distinguished_name': { + category: 'file', + description: 'Distinguished name (DN) of issuing certificate authority.', + example: 'C=US, O=Example Inc, OU=www.example.com, CN=Example SHA2 High Assurance Server CA', + name: 'file.x509.issuer.distinguished_name', + type: 'keyword', + }, + 'file.x509.issuer.locality': { + category: 'file', + description: 'List of locality names (L)', + example: 'Mountain View', + name: 'file.x509.issuer.locality', + type: 'keyword', + }, + 'file.x509.issuer.organization': { + category: 'file', + description: 'List of organizations (O) of issuing certificate authority.', + example: 'Example Inc', + name: 'file.x509.issuer.organization', + type: 'keyword', + }, + 'file.x509.issuer.organizational_unit': { + category: 'file', + description: 'List of organizational units (OU) of issuing certificate authority.', + example: 'www.example.com', + name: 'file.x509.issuer.organizational_unit', + type: 'keyword', + }, + 'file.x509.issuer.state_or_province': { + category: 'file', + description: 'List of state or province names (ST, S, or P)', + example: 'California', + name: 'file.x509.issuer.state_or_province', + type: 'keyword', + }, + 'file.x509.not_after': { + category: 'file', + description: 'Time at which the certificate is no longer considered valid.', + example: '"2020-07-16T03:15:39.000Z"', + name: 'file.x509.not_after', + type: 'date', + }, + 'file.x509.not_before': { + category: 'file', + description: 'Time at which the certificate is first considered valid.', + example: '"2019-08-16T01:40:25.000Z"', + name: 'file.x509.not_before', + type: 'date', + }, + 'file.x509.public_key_algorithm': { + category: 'file', + description: 'Algorithm used to generate the public key.', + example: 'RSA', + name: 'file.x509.public_key_algorithm', + type: 'keyword', + }, + 'file.x509.public_key_curve': { + category: 'file', + description: + 'The curve used by the elliptic curve public key algorithm. This is algorithm specific.', + example: 'nistp521', + name: 'file.x509.public_key_curve', + type: 'keyword', + }, + 'file.x509.public_key_exponent': { + category: 'file', + description: 'Exponent used to derive the public key. This is algorithm specific.', + example: 65537, + name: 'file.x509.public_key_exponent', + type: 'long', + }, + 'file.x509.public_key_size': { + category: 'file', + description: 'The size of the public key space in bits.', + example: 2048, + name: 'file.x509.public_key_size', + type: 'long', + }, + 'file.x509.serial_number': { + category: 'file', + description: + 'Unique serial number issued by the certificate authority. For consistency, if this value is alphanumeric, it should be formatted without colons and uppercase characters.', + example: '55FBB9C7DEBF09809D12CCAA', + name: 'file.x509.serial_number', + type: 'keyword', + }, + 'file.x509.signature_algorithm': { + category: 'file', + description: + 'Identifier for certificate signature algorithm. We recommend using names found in Go Lang Crypto library. See https://github.com/golang/go/blob/go1.14/src/crypto/x509/x509.go#L337-L353.', + example: 'SHA256-RSA', + name: 'file.x509.signature_algorithm', + type: 'keyword', + }, + 'file.x509.subject.common_name': { + category: 'file', + description: 'List of common names (CN) of subject.', + example: 'shared.global.example.net', + name: 'file.x509.subject.common_name', + type: 'keyword', + }, + 'file.x509.subject.country': { + category: 'file', + description: 'List of country (C) code', + example: 'US', + name: 'file.x509.subject.country', + type: 'keyword', + }, + 'file.x509.subject.distinguished_name': { + category: 'file', + description: 'Distinguished name (DN) of the certificate subject entity.', + example: 'C=US, ST=California, L=San Francisco, O=Example, Inc., CN=shared.global.example.net', + name: 'file.x509.subject.distinguished_name', + type: 'keyword', + }, + 'file.x509.subject.locality': { + category: 'file', + description: 'List of locality names (L)', + example: 'San Francisco', + name: 'file.x509.subject.locality', + type: 'keyword', + }, + 'file.x509.subject.organization': { + category: 'file', + description: 'List of organizations (O) of subject.', + example: 'Example, Inc.', + name: 'file.x509.subject.organization', + type: 'keyword', + }, + 'file.x509.subject.organizational_unit': { + category: 'file', + description: 'List of organizational units (OU) of subject.', + name: 'file.x509.subject.organizational_unit', + type: 'keyword', + }, + 'file.x509.subject.state_or_province': { + category: 'file', + description: 'List of state or province names (ST, S, or P)', + example: 'California', + name: 'file.x509.subject.state_or_province', + type: 'keyword', + }, + 'file.x509.version_number': { + category: 'file', + description: 'Version of x509 format.', + example: 3, + name: 'file.x509.version_number', + type: 'keyword', + }, 'geo.city_name': { category: 'geo', description: 'City name.', @@ -1611,6 +1883,14 @@ export const fieldsBeat: BeatFields = { name: 'host.os.platform', type: 'keyword', }, + 'host.os.type': { + category: 'host', + description: + "Use the `os.type` field to categorize the operating system into one of the broad commercial families. One of these following values should be used (lowercase): linux, macos, unix, windows. If the OS you're dealing with is not in the list, the field should not be populated. Please let us know by opening an issue with ECS, to propose its addition.", + example: 'macos', + name: 'host.os.type', + type: 'keyword', + }, 'host.os.version': { category: 'host', description: 'Operating system version as a raw string.', @@ -1680,7 +1960,7 @@ export const fieldsBeat: BeatFields = { }, 'host.user.id': { category: 'host', - description: 'Unique identifiers of the user.', + description: 'Unique identifier of the user.', name: 'host.user.id', type: 'keyword', }, @@ -1691,6 +1971,13 @@ export const fieldsBeat: BeatFields = { name: 'host.user.name', type: 'keyword', }, + 'host.user.roles': { + category: 'host', + description: 'Array of user roles at the time of the event.', + example: '["kibana_admin", "reporting_user"]', + name: 'host.user.roles', + type: 'keyword', + }, 'http.request.body.bytes': { category: 'http', description: 'Size in bytes of the request body.', @@ -1717,11 +2004,19 @@ export const fieldsBeat: BeatFields = { 'http.request.method': { category: 'http', description: - 'HTTP request method. The field value must be normalized to lowercase for querying. See the documentation section "Implementing ECS".', - example: 'get, post, put', + 'HTTP request method. Prior to ECS 1.6.0 the following guidance was provided: "The field value must be normalized to lowercase for querying." As of ECS 1.6.0, the guidance is deprecated because the original case of the method may be useful in anomaly detection. Original case will be mandated in ECS 2.0.0', + example: 'GET, POST, PUT, PoST', name: 'http.request.method', type: 'keyword', }, + 'http.request.mime_type': { + category: 'http', + description: + "Mime type of the body of the request. This value must only be populated based on the content of the request body, not on the `Content-Type` header. Comparing the mime type of a request with the request's Content-Type header can be helpful in detecting threats or misconfigured clients.", + example: 'image/gif', + name: 'http.request.mime_type', + type: 'keyword', + }, 'http.request.referrer': { category: 'http', description: 'Referrer for this HTTP request.', @@ -1752,6 +2047,14 @@ export const fieldsBeat: BeatFields = { type: 'long', format: 'bytes', }, + 'http.response.mime_type': { + category: 'http', + description: + "Mime type of the body of the response. This value must only be populated based on the content of the response body, not on the `Content-Type` header. Comparing the mime type of a response with the response's Content-Type header can be helpful in detecting misconfigured servers.", + example: 'image/gif', + name: 'http.response.mime_type', + type: 'keyword', + }, 'http.response.status_code': { category: 'http', description: 'HTTP response status code.', @@ -1789,6 +2092,14 @@ export const fieldsBeat: BeatFields = { name: 'interface.name', type: 'keyword', }, + 'log.file.path': { + category: 'log', + description: + "Full path to the log file this event came from, including the file name. It should include the drive letter, when appropriate. If the event wasn't read from a log file, do not populate this field.", + example: '/var/log/fun-times.log', + name: 'log.file.path', + type: 'keyword', + }, 'log.level': { category: 'log', description: @@ -1816,7 +2127,7 @@ export const fieldsBeat: BeatFields = { 'log.origin.file.name': { category: 'log', description: - 'The name of the file containing the source code which originated the log event. Note that this is not the name of the log file.', + 'The name of the file containing the source code which originated the log event. Note that this field is not meant to capture the log file. The correct field to capture the log file is `log.file.path`.', example: 'Bootstrap.java', name: 'log.origin.file.name', type: 'keyword', @@ -1912,7 +2223,7 @@ export const fieldsBeat: BeatFields = { 'network.direction': { category: 'network', description: - "Direction of the network traffic. Recommended values are: * inbound * outbound * internal * external * unknown When mapping events from a host-based monitoring context, populate this field from the host's point of view. When mapping events from a network or perimeter-based monitoring context, populate this field from the point of view of your network perimeter.", + 'Direction of the network traffic. Recommended values are: * ingress * egress * inbound * outbound * internal * external * unknown When mapping events from a host-based monitoring context, populate this field from the host\'s point of view, using the values "ingress" or "egress". When mapping events from a network or perimeter-based monitoring context, populate this field from the point of view of the network perimeter, using the values "inbound", "outbound", "internal" or "external". Note that "internal" is not crossing perimeter boundaries, and is meant to describe communication between two hosts within the perimeter. Note also that "external" is meant to describe traffic between two hosts that are external to the perimeter. This could for example be useful for ISPs or VPN service providers.', example: 'inbound', name: 'network.direction', type: 'keyword', @@ -1935,7 +2246,7 @@ export const fieldsBeat: BeatFields = { 'network.inner': { category: 'network', description: - 'Network.inner fields are added in addition to network.vlan fields to describe the innermost VLAN when q-in-q VLAN tagging is present. Allowed fields include vlan.id and vlan.name. Inner vlan fields are typically used when sending traffic with multiple 802.1q encapsulations to a network sensor (e.g. Zeek, Wireshark.)', + 'Network.inner fields are added in addition to network.vlan fields to describe the innermost VLAN when q-in-q VLAN tagging is present. Allowed fields include vlan.id and vlan.name. Inner vlan fields are typically used when sending traffic with multiple 802.1q encapsulations to a network sensor (e.g. Zeek, Wireshark.)', name: 'network.inner', type: 'object', }, @@ -2226,6 +2537,14 @@ export const fieldsBeat: BeatFields = { name: 'observer.os.platform', type: 'keyword', }, + 'observer.os.type': { + category: 'observer', + description: + "Use the `os.type` field to categorize the operating system into one of the broad commercial families. One of these following values should be used (lowercase): linux, macos, unix, windows. If the OS you're dealing with is not in the list, the field should not be populated. Please let us know by opening an issue with ECS, to propose its addition.", + example: 'macos', + name: 'observer.os.type', + type: 'keyword', + }, 'observer.os.version': { category: 'observer', description: 'Operating system version as a raw string.', @@ -2314,6 +2633,14 @@ export const fieldsBeat: BeatFields = { name: 'os.platform', type: 'keyword', }, + 'os.type': { + category: 'os', + description: + "Use the `os.type` field to categorize the operating system into one of the broad commercial families. One of these following values should be used (lowercase): linux, macos, unix, windows. If the OS you're dealing with is not in the list, the field should not be populated. Please let us know by opening an issue with ECS, to propose its addition.", + example: 'macos', + name: 'os.type', + type: 'keyword', + }, 'os.version': { category: 'os', description: 'Operating system version as a raw string.', @@ -2415,6 +2742,13 @@ export const fieldsBeat: BeatFields = { name: 'package.version', type: 'keyword', }, + 'pe.architecture': { + category: 'pe', + description: 'CPU architecture target for the file.', + example: 'x64', + name: 'pe.architecture', + type: 'keyword', + }, 'pe.company': { category: 'pe', description: 'Internal company name of the file, provided at compile-time.', @@ -2436,6 +2770,14 @@ export const fieldsBeat: BeatFields = { name: 'pe.file_version', type: 'keyword', }, + 'pe.imphash': { + category: 'pe', + description: + 'A hash of the imports in a PE file. An imphash -- or import hash -- can be used to fingerprint binaries even after recompilation or other code-level transformations have occurred, which would change more traditional hash values. Learn more at https://www.fireeye.com/blog/threat-research/2014/01/tracking-malware-import-hashing.html.', + example: '0c6803c4e922103c4dca5963aad36ddf', + name: 'pe.imphash', + type: 'keyword', + }, 'pe.original_file_name': { category: 'pe', description: 'Internal name of the file, provided at compile-time.', @@ -2454,7 +2796,7 @@ export const fieldsBeat: BeatFields = { category: 'process', description: 'Array of process arguments, starting with the absolute path to the executable. May be filtered to protect sensitive information.', - example: '["/usr/bin/ssh","-l","user","10.0.0.16"]', + example: '["/usr/bin/ssh", "-l", "user", "10.0.0.16"]', name: 'process.args', type: 'keyword', }, @@ -2568,8 +2910,9 @@ export const fieldsBeat: BeatFields = { }, 'process.parent.args': { category: 'process', - description: 'Array of process arguments. May be filtered to protect sensitive information.', - example: '["ssh","-l","user","10.0.0.16"]', + description: + 'Array of process arguments, starting with the absolute path to the executable. May be filtered to protect sensitive information.', + example: '["/usr/bin/ssh", "-l", "user", "10.0.0.16"]', name: 'process.parent.args', type: 'keyword', }, @@ -2681,6 +3024,56 @@ export const fieldsBeat: BeatFields = { name: 'process.parent.name', type: 'keyword', }, + 'process.parent.pe.architecture': { + category: 'process', + description: 'CPU architecture target for the file.', + example: 'x64', + name: 'process.parent.pe.architecture', + type: 'keyword', + }, + 'process.parent.pe.company': { + category: 'process', + description: 'Internal company name of the file, provided at compile-time.', + example: 'Microsoft Corporation', + name: 'process.parent.pe.company', + type: 'keyword', + }, + 'process.parent.pe.description': { + category: 'process', + description: 'Internal description of the file, provided at compile-time.', + example: 'Paint', + name: 'process.parent.pe.description', + type: 'keyword', + }, + 'process.parent.pe.file_version': { + category: 'process', + description: 'Internal version of the file, provided at compile-time.', + example: '6.3.9600.17415', + name: 'process.parent.pe.file_version', + type: 'keyword', + }, + 'process.parent.pe.imphash': { + category: 'process', + description: + 'A hash of the imports in a PE file. An imphash -- or import hash -- can be used to fingerprint binaries even after recompilation or other code-level transformations have occurred, which would change more traditional hash values. Learn more at https://www.fireeye.com/blog/threat-research/2014/01/tracking-malware-import-hashing.html.', + example: '0c6803c4e922103c4dca5963aad36ddf', + name: 'process.parent.pe.imphash', + type: 'keyword', + }, + 'process.parent.pe.original_file_name': { + category: 'process', + description: 'Internal name of the file, provided at compile-time.', + example: 'MSPAINT.EXE', + name: 'process.parent.pe.original_file_name', + type: 'keyword', + }, + 'process.parent.pe.product': { + category: 'process', + description: 'Internal product name of the file, provided at compile-time.', + example: 'Microsoft® Windows® Operating System', + name: 'process.parent.pe.product', + type: 'keyword', + }, 'process.parent.pgid': { category: 'process', description: 'Identifier of the group of processes the process belongs to.', @@ -2747,6 +3140,13 @@ export const fieldsBeat: BeatFields = { name: 'process.parent.working_directory', type: 'keyword', }, + 'process.pe.architecture': { + category: 'process', + description: 'CPU architecture target for the file.', + example: 'x64', + name: 'process.pe.architecture', + type: 'keyword', + }, 'process.pe.company': { category: 'process', description: 'Internal company name of the file, provided at compile-time.', @@ -2768,6 +3168,14 @@ export const fieldsBeat: BeatFields = { name: 'process.pe.file_version', type: 'keyword', }, + 'process.pe.imphash': { + category: 'process', + description: + 'A hash of the imports in a PE file. An imphash -- or import hash -- can be used to fingerprint binaries even after recompilation or other code-level transformations have occurred, which would change more traditional hash values. Learn more at https://www.fireeye.com/blog/threat-research/2014/01/tracking-malware-import-hashing.html.', + example: '0c6803c4e922103c4dca5963aad36ddf', + name: 'process.pe.imphash', + type: 'keyword', + }, 'process.pe.original_file_name': { category: 'process', description: 'Internal name of the file, provided at compile-time.', @@ -2908,6 +3316,13 @@ export const fieldsBeat: BeatFields = { name: 'related.hash', type: 'keyword', }, + 'related.hosts': { + category: 'related', + description: + 'All hostnames or other host identifiers seen on your event. Example identifiers include FQDNs, domain names, workstation names, or aliases.', + name: 'related.hosts', + type: 'keyword', + }, 'related.ip': { category: 'related', description: 'All of the IPs seen on your event.', @@ -3092,7 +3507,7 @@ export const fieldsBeat: BeatFields = { }, 'server.ip': { category: 'server', - description: 'IP address of the server. Can be one or multiple IPv4 or IPv6 addresses.', + description: 'IP address of the server (IPv4 or IPv6).', name: 'server.ip', type: 'ip', }, @@ -3134,15 +3549,23 @@ export const fieldsBeat: BeatFields = { 'server.registered_domain': { category: 'server', description: - 'The highest registered server domain, stripped of the subdomain. For example, the registered domain for "foo.google.com" is "google.com". This value can be determined precisely with a list like the public suffix list (http://publicsuffix.org). Trying to approximate this by simply taking the last two labels will not work well for TLDs such as "co.uk".', - example: 'google.com', + 'The highest registered server domain, stripped of the subdomain. For example, the registered domain for "foo.example.com" is "example.com". This value can be determined precisely with a list like the public suffix list (http://publicsuffix.org). Trying to approximate this by simply taking the last two labels will not work well for TLDs such as "co.uk".', + example: 'example.com', name: 'server.registered_domain', type: 'keyword', }, + 'server.subdomain': { + category: 'server', + description: + 'The subdomain portion of a fully qualified domain name includes all of the names except the host name under the registered_domain. In a partially qualified domain, or if the the qualification level of the full name cannot be determined, subdomain contains all of the names below the registered domain. For example the subdomain portion of "www.east.mydomain.co.uk" is "east". If the domain has multiple levels of subdomain, such as "sub2.sub1.example.com", the subdomain field should contain "sub2.sub1", with no trailing period.', + example: 'east', + name: 'server.subdomain', + type: 'keyword', + }, 'server.top_level_domain': { category: 'server', description: - 'The effective top level domain (eTLD), also known as the domain suffix, is the last part of the domain name. For example, the top level domain for google.com is "com". This value can be determined precisely with a list like the public suffix list (http://publicsuffix.org). Trying to approximate this by simply taking the last label will not work well for effective TLDs such as "co.uk".', + 'The effective top level domain (eTLD), also known as the domain suffix, is the last part of the domain name. For example, the top level domain for example.com is "com". This value can be determined precisely with a list like the public suffix list (http://publicsuffix.org). Trying to approximate this by simply taking the last label will not work well for effective TLDs such as "co.uk".', example: 'co.uk', name: 'server.top_level_domain', type: 'keyword', @@ -3195,7 +3618,7 @@ export const fieldsBeat: BeatFields = { }, 'server.user.id': { category: 'server', - description: 'Unique identifiers of the user.', + description: 'Unique identifier of the user.', name: 'server.user.id', type: 'keyword', }, @@ -3206,6 +3629,13 @@ export const fieldsBeat: BeatFields = { name: 'server.user.name', type: 'keyword', }, + 'server.user.roles': { + category: 'server', + description: 'Array of user roles at the time of the event.', + example: '["kibana_admin", "reporting_user"]', + name: 'server.user.roles', + type: 'keyword', + }, 'service.ephemeral_id': { category: 'service', description: @@ -3355,7 +3785,7 @@ export const fieldsBeat: BeatFields = { }, 'source.ip': { category: 'source', - description: 'IP address of the source. Can be one or multiple IPv4 or IPv6 addresses.', + description: 'IP address of the source (IPv4 or IPv6).', name: 'source.ip', type: 'ip', }, @@ -3397,15 +3827,23 @@ export const fieldsBeat: BeatFields = { 'source.registered_domain': { category: 'source', description: - 'The highest registered source domain, stripped of the subdomain. For example, the registered domain for "foo.google.com" is "google.com". This value can be determined precisely with a list like the public suffix list (http://publicsuffix.org). Trying to approximate this by simply taking the last two labels will not work well for TLDs such as "co.uk".', - example: 'google.com', + 'The highest registered source domain, stripped of the subdomain. For example, the registered domain for "foo.example.com" is "example.com". This value can be determined precisely with a list like the public suffix list (http://publicsuffix.org). Trying to approximate this by simply taking the last two labels will not work well for TLDs such as "co.uk".', + example: 'example.com', name: 'source.registered_domain', type: 'keyword', }, + 'source.subdomain': { + category: 'source', + description: + 'The subdomain portion of a fully qualified domain name includes all of the names except the host name under the registered_domain. In a partially qualified domain, or if the the qualification level of the full name cannot be determined, subdomain contains all of the names below the registered domain. For example the subdomain portion of "www.east.mydomain.co.uk" is "east". If the domain has multiple levels of subdomain, such as "sub2.sub1.example.com", the subdomain field should contain "sub2.sub1", with no trailing period.', + example: 'east', + name: 'source.subdomain', + type: 'keyword', + }, 'source.top_level_domain': { category: 'source', description: - 'The effective top level domain (eTLD), also known as the domain suffix, is the last part of the domain name. For example, the top level domain for google.com is "com". This value can be determined precisely with a list like the public suffix list (http://publicsuffix.org). Trying to approximate this by simply taking the last label will not work well for effective TLDs such as "co.uk".', + 'The effective top level domain (eTLD), also known as the domain suffix, is the last part of the domain name. For example, the top level domain for example.com is "com". This value can be determined precisely with a list like the public suffix list (http://publicsuffix.org). Trying to approximate this by simply taking the last label will not work well for effective TLDs such as "co.uk".', example: 'co.uk', name: 'source.top_level_domain', type: 'keyword', @@ -3458,7 +3896,7 @@ export const fieldsBeat: BeatFields = { }, 'source.user.id': { category: 'source', - description: 'Unique identifiers of the user.', + description: 'Unique identifier of the user.', name: 'source.user.id', type: 'keyword', }, @@ -3469,6 +3907,13 @@ export const fieldsBeat: BeatFields = { name: 'source.user.name', type: 'keyword', }, + 'source.user.roles': { + category: 'source', + description: 'Array of user roles at the time of the event.', + example: '["kibana_admin", "reporting_user"]', + name: 'source.user.roles', + type: 'keyword', + }, 'threat.framework': { category: 'threat', description: @@ -3480,51 +3925,75 @@ export const fieldsBeat: BeatFields = { 'threat.tactic.id': { category: 'threat', description: - 'The id of tactic used by this threat. You can use the Mitre ATT&CK Matrix Tactic categorization, for example. (ex. https://attack.mitre.org/tactics/TA0040/ )', - example: 'TA0040', + 'The id of tactic used by this threat. You can use a MITRE ATT&CK® tactic, for example. (ex. https://attack.mitre.org/tactics/TA0002/ )', + example: 'TA0002', name: 'threat.tactic.id', type: 'keyword', }, 'threat.tactic.name': { category: 'threat', description: - 'Name of the type of tactic used by this threat. You can use the Mitre ATT&CK Matrix Tactic categorization, for example. (ex. https://attack.mitre.org/tactics/TA0040/ )', - example: 'impact', + 'Name of the type of tactic used by this threat. You can use a MITRE ATT&CK® tactic, for example. (ex. https://attack.mitre.org/tactics/TA0002/)', + example: 'Execution', name: 'threat.tactic.name', type: 'keyword', }, 'threat.tactic.reference': { category: 'threat', description: - 'The reference url of tactic used by this threat. You can use the Mitre ATT&CK Matrix Tactic categorization, for example. (ex. https://attack.mitre.org/tactics/TA0040/ )', - example: 'https://attack.mitre.org/tactics/TA0040/', + 'The reference url of tactic used by this threat. You can use a MITRE ATT&CK® tactic, for example. (ex. https://attack.mitre.org/tactics/TA0002/ )', + example: 'https://attack.mitre.org/tactics/TA0002/', name: 'threat.tactic.reference', type: 'keyword', }, 'threat.technique.id': { category: 'threat', description: - 'The id of technique used by this tactic. You can use the Mitre ATT&CK Matrix Tactic categorization, for example. (ex. https://attack.mitre.org/techniques/T1499/ )', - example: 'T1499', + 'The id of technique used by this threat. You can use a MITRE ATT&CK® technique, for example. (ex. https://attack.mitre.org/techniques/T1059/)', + example: 'T1059', name: 'threat.technique.id', type: 'keyword', }, 'threat.technique.name': { category: 'threat', description: - 'The name of technique used by this tactic. You can use the Mitre ATT&CK Matrix Tactic categorization, for example. (ex. https://attack.mitre.org/techniques/T1499/ )', - example: 'endpoint denial of service', + 'The name of technique used by this threat. You can use a MITRE ATT&CK® technique, for example. (ex. https://attack.mitre.org/techniques/T1059/)', + example: 'Command and Scripting Interpreter', name: 'threat.technique.name', type: 'keyword', }, 'threat.technique.reference': { category: 'threat', description: - 'The reference url of technique used by this tactic. You can use the Mitre ATT&CK Matrix Tactic categorization, for example. (ex. https://attack.mitre.org/techniques/T1499/ )', - example: 'https://attack.mitre.org/techniques/T1499/', + 'The reference url of technique used by this threat. You can use a MITRE ATT&CK® technique, for example. (ex. https://attack.mitre.org/techniques/T1059/)', + example: 'https://attack.mitre.org/techniques/T1059/', name: 'threat.technique.reference', type: 'keyword', }, + 'threat.technique.subtechnique.id': { + category: 'threat', + description: + 'The full id of subtechnique used by this threat. You can use a MITRE ATT&CK® subtechnique, for example. (ex. https://attack.mitre.org/techniques/T1059/001/)', + example: 'T1059.001', + name: 'threat.technique.subtechnique.id', + type: 'keyword', + }, + 'threat.technique.subtechnique.name': { + category: 'threat', + description: + 'The name of subtechnique used by this threat. You can use a MITRE ATT&CK® subtechnique, for example. (ex. https://attack.mitre.org/techniques/T1059/001/)', + example: 'PowerShell', + name: 'threat.technique.subtechnique.name', + type: 'keyword', + }, + 'threat.technique.subtechnique.reference': { + category: 'threat', + description: + 'The reference url of subtechnique used by this threat. You can use a MITRE ATT&CK® subtechnique, for example. (ex. https://attack.mitre.org/techniques/T1059/001/)', + example: 'https://attack.mitre.org/techniques/T1059/001/', + name: 'threat.technique.subtechnique.reference', + type: 'keyword', + }, 'tls.cipher': { category: 'tls', description: 'String indicating the cipher used during the current connection.', @@ -3544,7 +4013,7 @@ export const fieldsBeat: BeatFields = { category: 'tls', description: 'Array of PEM-encoded certificates that make up the certificate chain offered by the client. This is usually mutually-exclusive of `client.certificate` since that value should be the first certificate in the chain.', - example: '["MII...","MII..."]', + example: '["MII...", "MII..."]', name: 'tls.client.certificate_chain', type: 'keyword', }, @@ -3576,7 +4045,7 @@ export const fieldsBeat: BeatFields = { category: 'tls', description: 'Distinguished name of subject of the issuer of the x.509 certificate presented by the client.', - example: 'CN=MyDomain Root CA, OU=Infrastructure Team, DC=mydomain, DC=com', + example: 'CN=Example Root CA, OU=Infrastructure Team, DC=example, DC=com', name: 'tls.client.issuer', type: 'keyword', }, @@ -3604,7 +4073,7 @@ export const fieldsBeat: BeatFields = { 'tls.client.server_name': { category: 'tls', description: - 'Also called an SNI, this tells the server which hostname to which the client is attempting to connect. When this value is available, it should get copied to `destination.domain`.', + 'Also called an SNI, this tells the server which hostname to which the client is attempting to connect to. When this value is available, it should get copied to `destination.domain`.', example: 'www.elastic.co', name: 'tls.client.server_name', type: 'keyword', @@ -3612,7 +4081,7 @@ export const fieldsBeat: BeatFields = { 'tls.client.subject': { category: 'tls', description: 'Distinguished name of subject of the x.509 certificate presented by the client.', - example: 'CN=myclient, OU=Documentation Team, DC=mydomain, DC=com', + example: 'CN=myclient, OU=Documentation Team, DC=example, DC=com', name: 'tls.client.subject', type: 'keyword', }, @@ -3620,10 +4089,181 @@ export const fieldsBeat: BeatFields = { category: 'tls', description: 'Array of ciphers offered by the client during the client hello.', example: - '["TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384","TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384","..."]', + '["TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", "..."]', name: 'tls.client.supported_ciphers', type: 'keyword', }, + 'tls.client.x509.alternative_names': { + category: 'tls', + description: + 'List of subject alternative names (SAN). Name types vary by certificate authority and certificate type but commonly contain IP addresses, DNS names (and wildcards), and email addresses.', + example: '*.elastic.co', + name: 'tls.client.x509.alternative_names', + type: 'keyword', + }, + 'tls.client.x509.issuer.common_name': { + category: 'tls', + description: 'List of common name (CN) of issuing certificate authority.', + example: 'Example SHA2 High Assurance Server CA', + name: 'tls.client.x509.issuer.common_name', + type: 'keyword', + }, + 'tls.client.x509.issuer.country': { + category: 'tls', + description: 'List of country (C) codes', + example: 'US', + name: 'tls.client.x509.issuer.country', + type: 'keyword', + }, + 'tls.client.x509.issuer.distinguished_name': { + category: 'tls', + description: 'Distinguished name (DN) of issuing certificate authority.', + example: 'C=US, O=Example Inc, OU=www.example.com, CN=Example SHA2 High Assurance Server CA', + name: 'tls.client.x509.issuer.distinguished_name', + type: 'keyword', + }, + 'tls.client.x509.issuer.locality': { + category: 'tls', + description: 'List of locality names (L)', + example: 'Mountain View', + name: 'tls.client.x509.issuer.locality', + type: 'keyword', + }, + 'tls.client.x509.issuer.organization': { + category: 'tls', + description: 'List of organizations (O) of issuing certificate authority.', + example: 'Example Inc', + name: 'tls.client.x509.issuer.organization', + type: 'keyword', + }, + 'tls.client.x509.issuer.organizational_unit': { + category: 'tls', + description: 'List of organizational units (OU) of issuing certificate authority.', + example: 'www.example.com', + name: 'tls.client.x509.issuer.organizational_unit', + type: 'keyword', + }, + 'tls.client.x509.issuer.state_or_province': { + category: 'tls', + description: 'List of state or province names (ST, S, or P)', + example: 'California', + name: 'tls.client.x509.issuer.state_or_province', + type: 'keyword', + }, + 'tls.client.x509.not_after': { + category: 'tls', + description: 'Time at which the certificate is no longer considered valid.', + example: '"2020-07-16T03:15:39.000Z"', + name: 'tls.client.x509.not_after', + type: 'date', + }, + 'tls.client.x509.not_before': { + category: 'tls', + description: 'Time at which the certificate is first considered valid.', + example: '"2019-08-16T01:40:25.000Z"', + name: 'tls.client.x509.not_before', + type: 'date', + }, + 'tls.client.x509.public_key_algorithm': { + category: 'tls', + description: 'Algorithm used to generate the public key.', + example: 'RSA', + name: 'tls.client.x509.public_key_algorithm', + type: 'keyword', + }, + 'tls.client.x509.public_key_curve': { + category: 'tls', + description: + 'The curve used by the elliptic curve public key algorithm. This is algorithm specific.', + example: 'nistp521', + name: 'tls.client.x509.public_key_curve', + type: 'keyword', + }, + 'tls.client.x509.public_key_exponent': { + category: 'tls', + description: 'Exponent used to derive the public key. This is algorithm specific.', + example: 65537, + name: 'tls.client.x509.public_key_exponent', + type: 'long', + }, + 'tls.client.x509.public_key_size': { + category: 'tls', + description: 'The size of the public key space in bits.', + example: 2048, + name: 'tls.client.x509.public_key_size', + type: 'long', + }, + 'tls.client.x509.serial_number': { + category: 'tls', + description: + 'Unique serial number issued by the certificate authority. For consistency, if this value is alphanumeric, it should be formatted without colons and uppercase characters.', + example: '55FBB9C7DEBF09809D12CCAA', + name: 'tls.client.x509.serial_number', + type: 'keyword', + }, + 'tls.client.x509.signature_algorithm': { + category: 'tls', + description: + 'Identifier for certificate signature algorithm. We recommend using names found in Go Lang Crypto library. See https://github.com/golang/go/blob/go1.14/src/crypto/x509/x509.go#L337-L353.', + example: 'SHA256-RSA', + name: 'tls.client.x509.signature_algorithm', + type: 'keyword', + }, + 'tls.client.x509.subject.common_name': { + category: 'tls', + description: 'List of common names (CN) of subject.', + example: 'shared.global.example.net', + name: 'tls.client.x509.subject.common_name', + type: 'keyword', + }, + 'tls.client.x509.subject.country': { + category: 'tls', + description: 'List of country (C) code', + example: 'US', + name: 'tls.client.x509.subject.country', + type: 'keyword', + }, + 'tls.client.x509.subject.distinguished_name': { + category: 'tls', + description: 'Distinguished name (DN) of the certificate subject entity.', + example: 'C=US, ST=California, L=San Francisco, O=Example, Inc., CN=shared.global.example.net', + name: 'tls.client.x509.subject.distinguished_name', + type: 'keyword', + }, + 'tls.client.x509.subject.locality': { + category: 'tls', + description: 'List of locality names (L)', + example: 'San Francisco', + name: 'tls.client.x509.subject.locality', + type: 'keyword', + }, + 'tls.client.x509.subject.organization': { + category: 'tls', + description: 'List of organizations (O) of subject.', + example: 'Example, Inc.', + name: 'tls.client.x509.subject.organization', + type: 'keyword', + }, + 'tls.client.x509.subject.organizational_unit': { + category: 'tls', + description: 'List of organizational units (OU) of subject.', + name: 'tls.client.x509.subject.organizational_unit', + type: 'keyword', + }, + 'tls.client.x509.subject.state_or_province': { + category: 'tls', + description: 'List of state or province names (ST, S, or P)', + example: 'California', + name: 'tls.client.x509.subject.state_or_province', + type: 'keyword', + }, + 'tls.client.x509.version_number': { + category: 'tls', + description: 'Version of x509 format.', + example: 3, + name: 'tls.client.x509.version_number', + type: 'keyword', + }, 'tls.curve': { category: 'tls', description: 'String indicating the curve used for the given cipher, when applicable.', @@ -3665,7 +4305,7 @@ export const fieldsBeat: BeatFields = { category: 'tls', description: 'Array of PEM-encoded certificates that make up the certificate chain offered by the server. This is usually mutually-exclusive of `server.certificate` since that value should be the first certificate in the chain.', - example: '["MII...","MII..."]', + example: '["MII...", "MII..."]', name: 'tls.server.certificate_chain', type: 'keyword', }, @@ -3696,7 +4336,7 @@ export const fieldsBeat: BeatFields = { 'tls.server.issuer': { category: 'tls', description: 'Subject of the issuer of the x.509 certificate presented by the server.', - example: 'CN=MyDomain Root CA, OU=Infrastructure Team, DC=mydomain, DC=com', + example: 'CN=Example Root CA, OU=Infrastructure Team, DC=example, DC=com', name: 'tls.server.issuer', type: 'keyword', }, @@ -3724,10 +4364,181 @@ export const fieldsBeat: BeatFields = { 'tls.server.subject': { category: 'tls', description: 'Subject of the x.509 certificate presented by the server.', - example: 'CN=www.mydomain.com, OU=Infrastructure Team, DC=mydomain, DC=com', + example: 'CN=www.example.com, OU=Infrastructure Team, DC=example, DC=com', name: 'tls.server.subject', type: 'keyword', }, + 'tls.server.x509.alternative_names': { + category: 'tls', + description: + 'List of subject alternative names (SAN). Name types vary by certificate authority and certificate type but commonly contain IP addresses, DNS names (and wildcards), and email addresses.', + example: '*.elastic.co', + name: 'tls.server.x509.alternative_names', + type: 'keyword', + }, + 'tls.server.x509.issuer.common_name': { + category: 'tls', + description: 'List of common name (CN) of issuing certificate authority.', + example: 'Example SHA2 High Assurance Server CA', + name: 'tls.server.x509.issuer.common_name', + type: 'keyword', + }, + 'tls.server.x509.issuer.country': { + category: 'tls', + description: 'List of country (C) codes', + example: 'US', + name: 'tls.server.x509.issuer.country', + type: 'keyword', + }, + 'tls.server.x509.issuer.distinguished_name': { + category: 'tls', + description: 'Distinguished name (DN) of issuing certificate authority.', + example: 'C=US, O=Example Inc, OU=www.example.com, CN=Example SHA2 High Assurance Server CA', + name: 'tls.server.x509.issuer.distinguished_name', + type: 'keyword', + }, + 'tls.server.x509.issuer.locality': { + category: 'tls', + description: 'List of locality names (L)', + example: 'Mountain View', + name: 'tls.server.x509.issuer.locality', + type: 'keyword', + }, + 'tls.server.x509.issuer.organization': { + category: 'tls', + description: 'List of organizations (O) of issuing certificate authority.', + example: 'Example Inc', + name: 'tls.server.x509.issuer.organization', + type: 'keyword', + }, + 'tls.server.x509.issuer.organizational_unit': { + category: 'tls', + description: 'List of organizational units (OU) of issuing certificate authority.', + example: 'www.example.com', + name: 'tls.server.x509.issuer.organizational_unit', + type: 'keyword', + }, + 'tls.server.x509.issuer.state_or_province': { + category: 'tls', + description: 'List of state or province names (ST, S, or P)', + example: 'California', + name: 'tls.server.x509.issuer.state_or_province', + type: 'keyword', + }, + 'tls.server.x509.not_after': { + category: 'tls', + description: 'Time at which the certificate is no longer considered valid.', + example: '"2020-07-16T03:15:39.000Z"', + name: 'tls.server.x509.not_after', + type: 'date', + }, + 'tls.server.x509.not_before': { + category: 'tls', + description: 'Time at which the certificate is first considered valid.', + example: '"2019-08-16T01:40:25.000Z"', + name: 'tls.server.x509.not_before', + type: 'date', + }, + 'tls.server.x509.public_key_algorithm': { + category: 'tls', + description: 'Algorithm used to generate the public key.', + example: 'RSA', + name: 'tls.server.x509.public_key_algorithm', + type: 'keyword', + }, + 'tls.server.x509.public_key_curve': { + category: 'tls', + description: + 'The curve used by the elliptic curve public key algorithm. This is algorithm specific.', + example: 'nistp521', + name: 'tls.server.x509.public_key_curve', + type: 'keyword', + }, + 'tls.server.x509.public_key_exponent': { + category: 'tls', + description: 'Exponent used to derive the public key. This is algorithm specific.', + example: 65537, + name: 'tls.server.x509.public_key_exponent', + type: 'long', + }, + 'tls.server.x509.public_key_size': { + category: 'tls', + description: 'The size of the public key space in bits.', + example: 2048, + name: 'tls.server.x509.public_key_size', + type: 'long', + }, + 'tls.server.x509.serial_number': { + category: 'tls', + description: + 'Unique serial number issued by the certificate authority. For consistency, if this value is alphanumeric, it should be formatted without colons and uppercase characters.', + example: '55FBB9C7DEBF09809D12CCAA', + name: 'tls.server.x509.serial_number', + type: 'keyword', + }, + 'tls.server.x509.signature_algorithm': { + category: 'tls', + description: + 'Identifier for certificate signature algorithm. We recommend using names found in Go Lang Crypto library. See https://github.com/golang/go/blob/go1.14/src/crypto/x509/x509.go#L337-L353.', + example: 'SHA256-RSA', + name: 'tls.server.x509.signature_algorithm', + type: 'keyword', + }, + 'tls.server.x509.subject.common_name': { + category: 'tls', + description: 'List of common names (CN) of subject.', + example: 'shared.global.example.net', + name: 'tls.server.x509.subject.common_name', + type: 'keyword', + }, + 'tls.server.x509.subject.country': { + category: 'tls', + description: 'List of country (C) code', + example: 'US', + name: 'tls.server.x509.subject.country', + type: 'keyword', + }, + 'tls.server.x509.subject.distinguished_name': { + category: 'tls', + description: 'Distinguished name (DN) of the certificate subject entity.', + example: 'C=US, ST=California, L=San Francisco, O=Example, Inc., CN=shared.global.example.net', + name: 'tls.server.x509.subject.distinguished_name', + type: 'keyword', + }, + 'tls.server.x509.subject.locality': { + category: 'tls', + description: 'List of locality names (L)', + example: 'San Francisco', + name: 'tls.server.x509.subject.locality', + type: 'keyword', + }, + 'tls.server.x509.subject.organization': { + category: 'tls', + description: 'List of organizations (O) of subject.', + example: 'Example, Inc.', + name: 'tls.server.x509.subject.organization', + type: 'keyword', + }, + 'tls.server.x509.subject.organizational_unit': { + category: 'tls', + description: 'List of organizational units (OU) of subject.', + name: 'tls.server.x509.subject.organizational_unit', + type: 'keyword', + }, + 'tls.server.x509.subject.state_or_province': { + category: 'tls', + description: 'List of state or province names (ST, S, or P)', + example: 'California', + name: 'tls.server.x509.subject.state_or_province', + type: 'keyword', + }, + 'tls.server.x509.version_number': { + category: 'tls', + description: 'Version of x509 format.', + example: 3, + name: 'tls.server.x509.version_number', + type: 'keyword', + }, 'tls.version': { category: 'tls', description: 'Numeric part of the version parsed from the original string.', @@ -3742,26 +4553,34 @@ export const fieldsBeat: BeatFields = { name: 'tls.version_protocol', type: 'keyword', }, - 'tracing.trace.id': { - category: 'tracing', + 'span.id': { + category: 'span', + description: + 'Unique identifier of the span within the scope of its trace. A span represents an operation within a transaction, such as a request to another service, or a database query.', + example: '3ff9a8981b7ccd5a', + name: 'span.id', + type: 'keyword', + }, + 'trace.id': { + category: 'trace', description: 'Unique identifier of the trace. A trace groups multiple events like transactions that belong together. For example, a user request handled by multiple inter-connected services.', example: '4bf92f3577b34da6a3ce929d0e0e4736', - name: 'tracing.trace.id', + name: 'trace.id', type: 'keyword', }, - 'tracing.transaction.id': { - category: 'tracing', + 'transaction.id': { + category: 'transaction', description: - 'Unique identifier of the transaction. A transaction is the highest level of work measured within a service, such as a request to a server.', + 'Unique identifier of the transaction within the scope of its trace. A transaction is the highest level of work measured within a service, such as a request to a server.', example: '00f067aa0ba902b7', - name: 'tracing.transaction.id', + name: 'transaction.id', type: 'keyword', }, 'url.domain': { category: 'url', description: - 'Domain of the url, such as "www.elastic.co". In some cases a URL may refer to an IP and/or port directly, without a domain name. In this case, the IP address would go to the `domain` field.', + 'Domain of the url, such as "www.elastic.co". In some cases a URL may refer to an IP and/or port directly, without a domain name. In this case, the IP address would go to the `domain` field. If the URL contains a literal IPv6 address enclosed by `[` and `]` (IETF RFC 2732), the `[` and `]` characters should also be captured in the `domain` field.', example: 'www.elastic.co', name: 'url.domain', type: 'keyword', @@ -3769,7 +4588,7 @@ export const fieldsBeat: BeatFields = { 'url.extension': { category: 'url', description: - 'The field contains the file extension from the original request url. The file extension is only set if it exists, as not every url has a file extension. The leading period must not be included. For example, the value must be "png", not ".png".', + 'The field contains the file extension from the original request url, excluding the leading dot. The file extension is only set if it exists, as not every url has a file extension. The leading period must not be included. For example, the value must be "png", not ".png". Note that when the file name has multiple extensions (example.tar.gz), only the last one should be captured ("gz", not "tar.gz").', example: 'png', name: 'url.extension', type: 'keyword', @@ -3827,8 +4646,8 @@ export const fieldsBeat: BeatFields = { 'url.registered_domain': { category: 'url', description: - 'The highest registered url domain, stripped of the subdomain. For example, the registered domain for "foo.google.com" is "google.com". This value can be determined precisely with a list like the public suffix list (http://publicsuffix.org). Trying to approximate this by simply taking the last two labels will not work well for TLDs such as "co.uk".', - example: 'google.com', + 'The highest registered url domain, stripped of the subdomain. For example, the registered domain for "foo.example.com" is "example.com". This value can be determined precisely with a list like the public suffix list (http://publicsuffix.org). Trying to approximate this by simply taking the last two labels will not work well for TLDs such as "co.uk".', + example: 'example.com', name: 'url.registered_domain', type: 'keyword', }, @@ -3839,10 +4658,18 @@ export const fieldsBeat: BeatFields = { name: 'url.scheme', type: 'keyword', }, + 'url.subdomain': { + category: 'url', + description: + 'The subdomain portion of a fully qualified domain name includes all of the names except the host name under the registered_domain. In a partially qualified domain, or if the the qualification level of the full name cannot be determined, subdomain contains all of the names below the registered domain. For example the subdomain portion of "www.east.mydomain.co.uk" is "east". If the domain has multiple levels of subdomain, such as "sub2.sub1.example.com", the subdomain field should contain "sub2.sub1", with no trailing period.', + example: 'east', + name: 'url.subdomain', + type: 'keyword', + }, 'url.top_level_domain': { category: 'url', description: - 'The effective top level domain (eTLD), also known as the domain suffix, is the last part of the domain name. For example, the top level domain for google.com is "com". This value can be determined precisely with a list like the public suffix list (http://publicsuffix.org). Trying to approximate this by simply taking the last label will not work well for effective TLDs such as "co.uk".', + 'The effective top level domain (eTLD), also known as the domain suffix, is the last part of the domain name. For example, the top level domain for example.com is "com". This value can be determined precisely with a list like the public suffix list (http://publicsuffix.org). Trying to approximate this by simply taking the last label will not work well for effective TLDs such as "co.uk".', example: 'co.uk', name: 'url.top_level_domain', type: 'keyword', @@ -3853,6 +4680,72 @@ export const fieldsBeat: BeatFields = { name: 'url.username', type: 'keyword', }, + 'user.changes.domain': { + category: 'user', + description: + 'Name of the directory the user is a member of. For example, an LDAP or Active Directory domain name.', + name: 'user.changes.domain', + type: 'keyword', + }, + 'user.changes.email': { + category: 'user', + description: 'User email address.', + name: 'user.changes.email', + type: 'keyword', + }, + 'user.changes.full_name': { + category: 'user', + description: "User's full name, if available.", + example: 'Albert Einstein', + name: 'user.changes.full_name', + type: 'keyword', + }, + 'user.changes.group.domain': { + category: 'user', + description: + 'Name of the directory the group is a member of. For example, an LDAP or Active Directory domain name.', + name: 'user.changes.group.domain', + type: 'keyword', + }, + 'user.changes.group.id': { + category: 'user', + description: 'Unique identifier for the group on the system/platform.', + name: 'user.changes.group.id', + type: 'keyword', + }, + 'user.changes.group.name': { + category: 'user', + description: 'Name of the group.', + name: 'user.changes.group.name', + type: 'keyword', + }, + 'user.changes.hash': { + category: 'user', + description: + 'Unique user hash to correlate information for a user in anonymized form. Useful if `user.id` or `user.name` contain confidential information and cannot be used.', + name: 'user.changes.hash', + type: 'keyword', + }, + 'user.changes.id': { + category: 'user', + description: 'Unique identifier of the user.', + name: 'user.changes.id', + type: 'keyword', + }, + 'user.changes.name': { + category: 'user', + description: 'Short name or login of the user.', + example: 'albert', + name: 'user.changes.name', + type: 'keyword', + }, + 'user.changes.roles': { + category: 'user', + description: 'Array of user roles at the time of the event.', + example: '["kibana_admin", "reporting_user"]', + name: 'user.changes.roles', + type: 'keyword', + }, 'user.domain': { category: 'user', description: @@ -3860,6 +4753,72 @@ export const fieldsBeat: BeatFields = { name: 'user.domain', type: 'keyword', }, + 'user.effective.domain': { + category: 'user', + description: + 'Name of the directory the user is a member of. For example, an LDAP or Active Directory domain name.', + name: 'user.effective.domain', + type: 'keyword', + }, + 'user.effective.email': { + category: 'user', + description: 'User email address.', + name: 'user.effective.email', + type: 'keyword', + }, + 'user.effective.full_name': { + category: 'user', + description: "User's full name, if available.", + example: 'Albert Einstein', + name: 'user.effective.full_name', + type: 'keyword', + }, + 'user.effective.group.domain': { + category: 'user', + description: + 'Name of the directory the group is a member of. For example, an LDAP or Active Directory domain name.', + name: 'user.effective.group.domain', + type: 'keyword', + }, + 'user.effective.group.id': { + category: 'user', + description: 'Unique identifier for the group on the system/platform.', + name: 'user.effective.group.id', + type: 'keyword', + }, + 'user.effective.group.name': { + category: 'user', + description: 'Name of the group.', + name: 'user.effective.group.name', + type: 'keyword', + }, + 'user.effective.hash': { + category: 'user', + description: + 'Unique user hash to correlate information for a user in anonymized form. Useful if `user.id` or `user.name` contain confidential information and cannot be used.', + name: 'user.effective.hash', + type: 'keyword', + }, + 'user.effective.id': { + category: 'user', + description: 'Unique identifier of the user.', + name: 'user.effective.id', + type: 'keyword', + }, + 'user.effective.name': { + category: 'user', + description: 'Short name or login of the user.', + example: 'albert', + name: 'user.effective.name', + type: 'keyword', + }, + 'user.effective.roles': { + category: 'user', + description: 'Array of user roles at the time of the event.', + example: '["kibana_admin", "reporting_user"]', + name: 'user.effective.roles', + type: 'keyword', + }, 'user.email': { category: 'user', description: 'User email address.', @@ -3901,7 +4860,7 @@ export const fieldsBeat: BeatFields = { }, 'user.id': { category: 'user', - description: 'Unique identifiers of the user.', + description: 'Unique identifier of the user.', name: 'user.id', type: 'keyword', }, @@ -3912,6 +4871,79 @@ export const fieldsBeat: BeatFields = { name: 'user.name', type: 'keyword', }, + 'user.roles': { + category: 'user', + description: 'Array of user roles at the time of the event.', + example: '["kibana_admin", "reporting_user"]', + name: 'user.roles', + type: 'keyword', + }, + 'user.target.domain': { + category: 'user', + description: + 'Name of the directory the user is a member of. For example, an LDAP or Active Directory domain name.', + name: 'user.target.domain', + type: 'keyword', + }, + 'user.target.email': { + category: 'user', + description: 'User email address.', + name: 'user.target.email', + type: 'keyword', + }, + 'user.target.full_name': { + category: 'user', + description: "User's full name, if available.", + example: 'Albert Einstein', + name: 'user.target.full_name', + type: 'keyword', + }, + 'user.target.group.domain': { + category: 'user', + description: + 'Name of the directory the group is a member of. For example, an LDAP or Active Directory domain name.', + name: 'user.target.group.domain', + type: 'keyword', + }, + 'user.target.group.id': { + category: 'user', + description: 'Unique identifier for the group on the system/platform.', + name: 'user.target.group.id', + type: 'keyword', + }, + 'user.target.group.name': { + category: 'user', + description: 'Name of the group.', + name: 'user.target.group.name', + type: 'keyword', + }, + 'user.target.hash': { + category: 'user', + description: + 'Unique user hash to correlate information for a user in anonymized form. Useful if `user.id` or `user.name` contain confidential information and cannot be used.', + name: 'user.target.hash', + type: 'keyword', + }, + 'user.target.id': { + category: 'user', + description: 'Unique identifier of the user.', + name: 'user.target.id', + type: 'keyword', + }, + 'user.target.name': { + category: 'user', + description: 'Short name or login of the user.', + example: 'albert', + name: 'user.target.name', + type: 'keyword', + }, + 'user.target.roles': { + category: 'user', + description: 'Array of user roles at the time of the event.', + example: '["kibana_admin", "reporting_user"]', + name: 'user.target.roles', + type: 'keyword', + }, 'user_agent.device.name': { category: 'user_agent', description: 'Name of the device.', @@ -3969,6 +5001,14 @@ export const fieldsBeat: BeatFields = { name: 'user_agent.os.platform', type: 'keyword', }, + 'user_agent.os.type': { + category: 'user_agent', + description: + "Use the `os.type` field to categorize the operating system into one of the broad commercial families. One of these following values should be used (lowercase): linux, macos, unix, windows. If the OS you're dealing with is not in the list, the field should not be populated. Please let us know by opening an issue with ECS, to propose its addition.", + example: 'macos', + name: 'user_agent.os.type', + type: 'keyword', + }, 'user_agent.os.version': { category: 'user_agent', description: 'Operating system version as a raw string.', @@ -4098,54 +5138,219 @@ export const fieldsBeat: BeatFields = { name: 'vulnerability.severity', type: 'keyword', }, - 'agent.hostname': { - category: 'agent', + 'x509.alternative_names': { + category: 'x509', description: - 'Deprecated - use agent.name or agent.id to identify an agent. Hostname of the agent. ', - name: 'agent.hostname', + 'List of subject alternative names (SAN). Name types vary by certificate authority and certificate type but commonly contain IP addresses, DNS names (and wildcards), and email addresses.', + example: '*.elastic.co', + name: 'x509.alternative_names', type: 'keyword', }, - 'beat.timezone': { - category: 'beat', - name: 'beat.timezone', - type: 'alias', + 'x509.issuer.common_name': { + category: 'x509', + description: 'List of common name (CN) of issuing certificate authority.', + example: 'Example SHA2 High Assurance Server CA', + name: 'x509.issuer.common_name', + type: 'keyword', }, - fields: { - category: 'base', - description: 'Contains user configurable fields. ', - name: 'fields', - type: 'object', + 'x509.issuer.country': { + category: 'x509', + description: 'List of country (C) codes', + example: 'US', + name: 'x509.issuer.country', + type: 'keyword', }, - 'beat.name': { - category: 'beat', - name: 'beat.name', - type: 'alias', + 'x509.issuer.distinguished_name': { + category: 'x509', + description: 'Distinguished name (DN) of issuing certificate authority.', + example: 'C=US, O=Example Inc, OU=www.example.com, CN=Example SHA2 High Assurance Server CA', + name: 'x509.issuer.distinguished_name', + type: 'keyword', }, - 'beat.hostname': { - category: 'beat', - name: 'beat.hostname', - type: 'alias', + 'x509.issuer.locality': { + category: 'x509', + description: 'List of locality names (L)', + example: 'Mountain View', + name: 'x509.issuer.locality', + type: 'keyword', }, - 'timeseries.instance': { - category: 'timeseries', - description: 'Time series instance id', - name: 'timeseries.instance', + 'x509.issuer.organization': { + category: 'x509', + description: 'List of organizations (O) of issuing certificate authority.', + example: 'Example Inc', + name: 'x509.issuer.organization', type: 'keyword', }, - 'cloud.project.id': { - category: 'cloud', - description: 'Name of the project in Google Cloud. ', - example: 'project-x', - name: 'cloud.project.id', + 'x509.issuer.organizational_unit': { + category: 'x509', + description: 'List of organizational units (OU) of issuing certificate authority.', + example: 'www.example.com', + name: 'x509.issuer.organizational_unit', + type: 'keyword', }, - 'cloud.image.id': { - category: 'cloud', - description: 'Image ID for the cloud instance. ', - example: 'ami-abcd1234', - name: 'cloud.image.id', + 'x509.issuer.state_or_province': { + category: 'x509', + description: 'List of state or province names (ST, S, or P)', + example: 'California', + name: 'x509.issuer.state_or_province', + type: 'keyword', }, - 'meta.cloud.provider': { - category: 'meta', + 'x509.not_after': { + category: 'x509', + description: 'Time at which the certificate is no longer considered valid.', + example: '"2020-07-16T03:15:39.000Z"', + name: 'x509.not_after', + type: 'date', + }, + 'x509.not_before': { + category: 'x509', + description: 'Time at which the certificate is first considered valid.', + example: '"2019-08-16T01:40:25.000Z"', + name: 'x509.not_before', + type: 'date', + }, + 'x509.public_key_algorithm': { + category: 'x509', + description: 'Algorithm used to generate the public key.', + example: 'RSA', + name: 'x509.public_key_algorithm', + type: 'keyword', + }, + 'x509.public_key_curve': { + category: 'x509', + description: + 'The curve used by the elliptic curve public key algorithm. This is algorithm specific.', + example: 'nistp521', + name: 'x509.public_key_curve', + type: 'keyword', + }, + 'x509.public_key_exponent': { + category: 'x509', + description: 'Exponent used to derive the public key. This is algorithm specific.', + example: 65537, + name: 'x509.public_key_exponent', + type: 'long', + }, + 'x509.public_key_size': { + category: 'x509', + description: 'The size of the public key space in bits.', + example: 2048, + name: 'x509.public_key_size', + type: 'long', + }, + 'x509.serial_number': { + category: 'x509', + description: + 'Unique serial number issued by the certificate authority. For consistency, if this value is alphanumeric, it should be formatted without colons and uppercase characters.', + example: '55FBB9C7DEBF09809D12CCAA', + name: 'x509.serial_number', + type: 'keyword', + }, + 'x509.signature_algorithm': { + category: 'x509', + description: + 'Identifier for certificate signature algorithm. We recommend using names found in Go Lang Crypto library. See https://github.com/golang/go/blob/go1.14/src/crypto/x509/x509.go#L337-L353.', + example: 'SHA256-RSA', + name: 'x509.signature_algorithm', + type: 'keyword', + }, + 'x509.subject.common_name': { + category: 'x509', + description: 'List of common names (CN) of subject.', + example: 'shared.global.example.net', + name: 'x509.subject.common_name', + type: 'keyword', + }, + 'x509.subject.country': { + category: 'x509', + description: 'List of country (C) code', + example: 'US', + name: 'x509.subject.country', + type: 'keyword', + }, + 'x509.subject.distinguished_name': { + category: 'x509', + description: 'Distinguished name (DN) of the certificate subject entity.', + example: 'C=US, ST=California, L=San Francisco, O=Example, Inc., CN=shared.global.example.net', + name: 'x509.subject.distinguished_name', + type: 'keyword', + }, + 'x509.subject.locality': { + category: 'x509', + description: 'List of locality names (L)', + example: 'San Francisco', + name: 'x509.subject.locality', + type: 'keyword', + }, + 'x509.subject.organization': { + category: 'x509', + description: 'List of organizations (O) of subject.', + example: 'Example, Inc.', + name: 'x509.subject.organization', + type: 'keyword', + }, + 'x509.subject.organizational_unit': { + category: 'x509', + description: 'List of organizational units (OU) of subject.', + name: 'x509.subject.organizational_unit', + type: 'keyword', + }, + 'x509.subject.state_or_province': { + category: 'x509', + description: 'List of state or province names (ST, S, or P)', + example: 'California', + name: 'x509.subject.state_or_province', + type: 'keyword', + }, + 'x509.version_number': { + category: 'x509', + description: 'Version of x509 format.', + example: 3, + name: 'x509.version_number', + type: 'keyword', + }, + 'agent.hostname': { + category: 'agent', + description: + 'Deprecated - use agent.name or agent.id to identify an agent. Hostname of the agent. ', + name: 'agent.hostname', + type: 'keyword', + }, + 'beat.timezone': { + category: 'beat', + name: 'beat.timezone', + type: 'alias', + }, + fields: { + category: 'base', + description: 'Contains user configurable fields. ', + name: 'fields', + type: 'object', + }, + 'beat.name': { + category: 'beat', + name: 'beat.name', + type: 'alias', + }, + 'beat.hostname': { + category: 'beat', + name: 'beat.hostname', + type: 'alias', + }, + 'timeseries.instance': { + category: 'timeseries', + description: 'Time series instance id', + name: 'timeseries.instance', + type: 'keyword', + }, + 'cloud.image.id': { + category: 'cloud', + description: 'Image ID for the cloud instance. ', + example: 'ami-abcd1234', + name: 'cloud.image.id', + }, + 'meta.cloud.provider': { + category: 'meta', name: 'meta.cloud.provider', type: 'alias', }, @@ -4244,6 +5449,12 @@ export const fieldsBeat: BeatFields = { name: 'kubernetes.node.name', type: 'keyword', }, + 'kubernetes.node.hostname': { + category: 'kubernetes', + description: 'Kubernetes hostname as reported by the node’s kernel ', + name: 'kubernetes.node.hostname', + type: 'keyword', + }, 'kubernetes.labels.*': { category: 'kubernetes', description: 'Kubernetes labels map ', @@ -4256,6 +5467,12 @@ export const fieldsBeat: BeatFields = { name: 'kubernetes.annotations.*', type: 'object', }, + 'kubernetes.service.selectors.*': { + category: 'kubernetes', + description: 'Kubernetes Service selectors map ', + name: 'kubernetes.service.selectors.*', + type: 'object', + }, 'kubernetes.replicaset.name': { category: 'kubernetes', description: 'Kubernetes replicaset name ', @@ -4392,30 +5609,6 @@ export const fieldsBeat: BeatFields = { name: 'user.audit.name', type: 'keyword', }, - 'user.effective.id': { - category: 'user', - description: 'Effective user ID.', - name: 'user.effective.id', - type: 'keyword', - }, - 'user.effective.name': { - category: 'user', - description: 'Effective user name.', - name: 'user.effective.name', - type: 'keyword', - }, - 'user.effective.group.id': { - category: 'user', - description: 'Effective group ID.', - name: 'user.effective.group.id', - type: 'keyword', - }, - 'user.effective.group.name': { - category: 'user', - description: 'Effective group name.', - name: 'user.effective.group.name', - type: 'keyword', - }, 'user.filesystem.id': { category: 'user', description: 'Filesystem user ID.', @@ -4474,11 +5667,6 @@ export const fieldsBeat: BeatFields = { name: 'user.uid', type: 'alias', }, - 'user.euid': { - category: 'user', - name: 'user.euid', - type: 'alias', - }, 'user.fsuid': { category: 'user', name: 'user.fsuid', @@ -4494,11 +5682,6 @@ export const fieldsBeat: BeatFields = { name: 'user.gid', type: 'alias', }, - 'user.egid': { - category: 'user', - name: 'user.egid', - type: 'alias', - }, 'user.sgid': { category: 'user', name: 'user.sgid', @@ -4519,11 +5702,6 @@ export const fieldsBeat: BeatFields = { name: 'user.name_map.uid', type: 'alias', }, - 'user.name_map.euid': { - category: 'user', - name: 'user.name_map.euid', - type: 'alias', - }, 'user.name_map.fsuid': { category: 'user', name: 'user.name_map.fsuid', @@ -4539,11 +5717,6 @@ export const fieldsBeat: BeatFields = { name: 'user.name_map.gid', type: 'alias', }, - 'user.name_map.egid': { - category: 'user', - name: 'user.name_map.egid', - type: 'alias', - }, 'user.name_map.sgid': { category: 'user', name: 'user.name_map.sgid', @@ -6273,6 +7446,12 @@ export const fieldsBeat: BeatFields = { name: 'system.audit.host.os.kernel', type: 'keyword', }, + 'system.audit.host.os.type': { + category: 'system', + description: 'OS type (see ECS os.type). ', + name: 'system.audit.host.os.type', + type: 'keyword', + }, 'system.audit.package.entity_id': { category: 'system', description: @@ -6394,13 +7573,6 @@ export const fieldsBeat: BeatFields = { name: 'system.audit.user.password.last_changed', type: 'date', }, - 'log.file.path': { - category: 'log', - description: - 'The file from which the line was read. This field contains the absolute path to the file. For example: `/var/log/system.log`. ', - name: 'log.file.path', - type: 'keyword', - }, 'log.source.address': { category: 'log', description: 'Source address from which the log event was read / sent from. ', @@ -7093,6 +8265,21 @@ export const fieldsBeat: BeatFields = { name: 'elasticsearch.audit.user.roles', type: 'keyword', }, + 'elasticsearch.audit.user.run_as.name': { + category: 'elasticsearch', + name: 'elasticsearch.audit.user.run_as.name', + type: 'keyword', + }, + 'elasticsearch.audit.user.run_as.realm': { + category: 'elasticsearch', + name: 'elasticsearch.audit.user.run_as.realm', + type: 'keyword', + }, + 'elasticsearch.audit.component': { + category: 'elasticsearch', + name: 'elasticsearch.audit.component', + type: 'keyword', + }, 'elasticsearch.audit.action': { category: 'elasticsearch', description: 'The name of the action that was executed', @@ -7152,6 +8339,11 @@ export const fieldsBeat: BeatFields = { name: 'elasticsearch.audit.message', type: 'text', }, + 'elasticsearch.audit.invalidate.apikeys.owned_by_authenticated_user': { + category: 'elasticsearch', + name: 'elasticsearch.audit.invalidate.apikeys.owned_by_authenticated_user', + type: 'boolean', + }, 'elasticsearch.deprecation': { category: 'elasticsearch', description: '', @@ -7970,6 +9162,77 @@ export const fieldsBeat: BeatFields = { name: 'kafka.log.trace.message', type: 'text', }, + 'kibana.session_id': { + category: 'kibana', + description: + 'The ID of the user session associated with this event. Each login attempt results in a unique session id.', + example: '123e4567-e89b-12d3-a456-426614174000', + name: 'kibana.session_id', + type: 'keyword', + }, + 'kibana.space_id': { + category: 'kibana', + description: 'The id of the space associated with this event.', + example: 'default', + name: 'kibana.space_id', + type: 'keyword', + }, + 'kibana.saved_object.type': { + category: 'kibana', + description: 'The type of the saved object associated with this event.', + example: 'dashboard', + name: 'kibana.saved_object.type', + type: 'keyword', + }, + 'kibana.saved_object.id': { + category: 'kibana', + description: 'The id of the saved object associated with this event.', + example: '6295bdd0-0a0e-11e7-825f-6748cda7d858', + name: 'kibana.saved_object.id', + type: 'keyword', + }, + 'kibana.add_to_spaces': { + category: 'kibana', + description: 'The set of space ids that a saved object was shared to.', + example: "['default', 'marketing']", + name: 'kibana.add_to_spaces', + type: 'keyword', + }, + 'kibana.delete_from_spaces': { + category: 'kibana', + description: 'The set of space ids that a saved object was removed from.', + example: "['default', 'marketing']", + name: 'kibana.delete_from_spaces', + type: 'keyword', + }, + 'kibana.authentication_provider': { + category: 'kibana', + description: 'The authentication provider associated with a login event.', + example: 'basic1', + name: 'kibana.authentication_provider', + type: 'keyword', + }, + 'kibana.authentication_type': { + category: 'kibana', + description: 'The authentication provider type associated with a login event.', + example: 'basic', + name: 'kibana.authentication_type', + type: 'keyword', + }, + 'kibana.authentication_realm': { + category: 'kibana', + description: 'The Elasticsearch authentication realm name which fulfilled a login event.', + example: 'native', + name: 'kibana.authentication_realm', + type: 'keyword', + }, + 'kibana.lookup_realm': { + category: 'kibana', + description: 'The Elasticsearch lookup realm which fulfilled a login event.', + example: 'native', + name: 'kibana.lookup_realm', + type: 'keyword', + }, 'kibana.log.tags': { category: 'kibana', description: 'Kibana logging tags. ', @@ -8040,6 +9303,11 @@ export const fieldsBeat: BeatFields = { name: 'logstash.log.log_event', type: 'object', }, + 'logstash.log.log_event.action': { + category: 'logstash', + name: 'logstash.log.log_event.action', + type: 'keyword', + }, 'logstash.log.pipeline_id': { category: 'logstash', description: 'The ID of the pipeline. ', @@ -8637,6 +9905,34 @@ export const fieldsBeat: BeatFields = { name: 'nginx.ingress_controller.remote_ip_list', type: 'array', }, + 'nginx.ingress_controller.upstream_address_list': { + category: 'nginx', + description: + 'An array of the upstream addresses. It is a list because it is common that several upstream servers were contacted during request processing. ', + name: 'nginx.ingress_controller.upstream_address_list', + type: 'keyword', + }, + 'nginx.ingress_controller.upstream.response.length_list': { + category: 'nginx', + description: + 'An array of upstream response lengths. It is a list because it is common that several upstream servers were contacted during request processing. ', + name: 'nginx.ingress_controller.upstream.response.length_list', + type: 'keyword', + }, + 'nginx.ingress_controller.upstream.response.time_list': { + category: 'nginx', + description: + 'An array of upstream response durations. It is a list because it is common that several upstream servers were contacted during request processing. ', + name: 'nginx.ingress_controller.upstream.response.time_list', + type: 'keyword', + }, + 'nginx.ingress_controller.upstream.response.status_code_list': { + category: 'nginx', + description: + 'An array of upstream response status codes. It is a list because it is common that several upstream servers were contacted during request processing. ', + name: 'nginx.ingress_controller.upstream.response.status_code_list', + type: 'keyword', + }, 'nginx.ingress_controller.http.request.length': { category: 'nginx', description: 'The request length (including request line, header, and request body) ', @@ -8665,7 +9961,8 @@ export const fieldsBeat: BeatFields = { }, 'nginx.ingress_controller.upstream.response.length': { category: 'nginx', - description: 'The length of the response obtained from the upstream server ', + description: + 'The length of the response obtained from the upstream server. If several servers were contacted during request process, the summary of the multiple response lengths is stored. ', name: 'nginx.ingress_controller.upstream.response.length', type: 'long', format: 'bytes', @@ -8673,36 +9970,38 @@ export const fieldsBeat: BeatFields = { 'nginx.ingress_controller.upstream.response.time': { category: 'nginx', description: - 'The time spent on receiving the response from the upstream server as seconds with millisecond resolution ', + 'The time spent on receiving the response from the upstream as seconds with millisecond resolution. If several servers were contacted during request process, the summary of the multiple response times is stored. ', name: 'nginx.ingress_controller.upstream.response.time', type: 'double', format: 'duration', }, 'nginx.ingress_controller.upstream.response.status_code': { category: 'nginx', - description: 'The status code of the response obtained from the upstream server ', + description: + 'The status code of the response obtained from the upstream server. If several servers were contacted during request process, only the status code of the response from the last one is stored in this field. ', name: 'nginx.ingress_controller.upstream.response.status_code', type: 'long', }, - 'nginx.ingress_controller.http.request.id': { - category: 'nginx', - description: 'The randomly generated ID of the request ', - name: 'nginx.ingress_controller.http.request.id', - type: 'keyword', - }, 'nginx.ingress_controller.upstream.ip': { category: 'nginx', description: - 'The IP address of the upstream server. If several servers were contacted during request processing, their addresses are separated by commas. ', + 'The IP address of the upstream server. If several servers were contacted during request process, only the last one is stored in this field. ', name: 'nginx.ingress_controller.upstream.ip', type: 'ip', }, 'nginx.ingress_controller.upstream.port': { category: 'nginx', - description: 'The port of the upstream server. ', + description: + 'The port of the upstream server. If several servers were contacted during request process, only the last one is stored in this field. ', name: 'nginx.ingress_controller.upstream.port', type: 'long', }, + 'nginx.ingress_controller.http.request.id': { + category: 'nginx', + description: 'The randomly generated ID of the request ', + name: 'nginx.ingress_controller.http.request.id', + type: 'keyword', + }, 'nginx.ingress_controller.body_sent.bytes': { category: 'nginx', name: 'nginx.ingress_controller.body_sent.bytes', @@ -8831,6 +10130,78 @@ export const fieldsBeat: BeatFields = { name: 'osquery.result.calendar_time', type: 'keyword', }, + 'pensando.dfw.action': { + category: 'pensando', + description: 'Action on the flow. ', + name: 'pensando.dfw.action', + type: 'keyword', + }, + 'pensando.dfw.app_id': { + category: 'pensando', + description: 'Application ID ', + name: 'pensando.dfw.app_id', + type: 'integer', + }, + 'pensando.dfw.destination_address': { + category: 'pensando', + description: 'Address of destination. ', + name: 'pensando.dfw.destination_address', + type: 'keyword', + }, + 'pensando.dfw.destination_port': { + category: 'pensando', + description: 'Port of destination. ', + name: 'pensando.dfw.destination_port', + type: 'integer', + }, + 'pensando.dfw.direction': { + category: 'pensando', + description: 'Direction of the flow ', + name: 'pensando.dfw.direction', + type: 'keyword', + }, + 'pensando.dfw.protocol': { + category: 'pensando', + description: 'Protocol of the flow ', + name: 'pensando.dfw.protocol', + type: 'keyword', + }, + 'pensando.dfw.rule_id': { + category: 'pensando', + description: 'Rule ID that was matched. ', + name: 'pensando.dfw.rule_id', + type: 'keyword', + }, + 'pensando.dfw.session_id': { + category: 'pensando', + description: 'Session ID of the flow ', + name: 'pensando.dfw.session_id', + type: 'integer', + }, + 'pensando.dfw.session_state': { + category: 'pensando', + description: 'Session state of the flow. ', + name: 'pensando.dfw.session_state', + type: 'keyword', + }, + 'pensando.dfw.source_address': { + category: 'pensando', + description: 'Source address of the flow. ', + name: 'pensando.dfw.source_address', + type: 'keyword', + }, + 'pensando.dfw.source_port': { + category: 'pensando', + description: 'Source port of the flow. ', + name: 'pensando.dfw.source_port', + type: 'integer', + }, + 'pensando.dfw.timestamp': { + category: 'pensando', + description: 'Timestamp of the log. ', + name: 'pensando.dfw.timestamp', + type: 'date', + }, 'postgresql.log.timestamp': { category: 'postgresql', description: 'The timestamp from the log line. ', @@ -8838,26 +10209,52 @@ export const fieldsBeat: BeatFields = { }, 'postgresql.log.core_id': { category: 'postgresql', - description: 'Core id ', + description: + 'Core id. (deprecated, there is no core_id in PostgreSQL logs, this is actually session_line_number). ', name: 'postgresql.log.core_id', + type: 'alias', + }, + 'postgresql.log.client_addr': { + category: 'postgresql', + description: 'Host where the connection originated from. ', + example: '127.0.0.1', + name: 'postgresql.log.client_addr', + }, + 'postgresql.log.client_port': { + category: 'postgresql', + description: 'Port where the connection originated from. ', + example: '59700', + name: 'postgresql.log.client_port', + }, + 'postgresql.log.session_id': { + category: 'postgresql', + description: 'PostgreSQL session. ', + example: '5ff1dd98.22', + name: 'postgresql.log.session_id', + }, + 'postgresql.log.session_line_number': { + category: 'postgresql', + description: 'Line number inside a session. (%l in `log_line_prefix`). ', + name: 'postgresql.log.session_line_number', type: 'long', }, 'postgresql.log.database': { category: 'postgresql', - description: 'Name of database ', - example: 'mydb', + description: 'Name of database. ', + example: 'postgres', name: 'postgresql.log.database', }, 'postgresql.log.query': { category: 'postgresql', - description: 'Query statement. ', + description: + 'Query statement. In the case of CSV parse, look at command_tag to get more context. ', example: 'SELECT * FROM users;', name: 'postgresql.log.query', }, 'postgresql.log.query_step': { category: 'postgresql', description: - 'Statement step when using extended query protocol (one of statement, parse, bind or execute) ', + 'Statement step when using extended query protocol (one of statement, parse, bind or execute). ', example: 'parse', name: 'postgresql.log.query_step', }, @@ -8868,20 +10265,98 @@ export const fieldsBeat: BeatFields = { example: 'pdo_stmt_00000001', name: 'postgresql.log.query_name', }, - 'postgresql.log.error.code': { + 'postgresql.log.command_tag': { category: 'postgresql', - description: 'Error code returned by Postgres (if any)', - name: 'postgresql.log.error.code', + description: + "Type of session's current command. The complete list can be found at: src/include/tcop/cmdtaglist.h ", + example: 'SELECT', + name: 'postgresql.log.command_tag', + }, + 'postgresql.log.session_start_time': { + category: 'postgresql', + description: 'Time when this session started. ', + name: 'postgresql.log.session_start_time', + type: 'date', + }, + 'postgresql.log.virtual_transaction_id': { + category: 'postgresql', + description: 'Backend local transaction id. ', + name: 'postgresql.log.virtual_transaction_id', + }, + 'postgresql.log.transaction_id': { + category: 'postgresql', + description: 'The id of current transaction. ', + name: 'postgresql.log.transaction_id', type: 'long', }, - 'postgresql.log.timezone': { + 'postgresql.log.sql_state_code': { category: 'postgresql', - name: 'postgresql.log.timezone', + description: + 'State code returned by Postgres (if any). See also https://www.postgresql.org/docs/current/errcodes-appendix.html ', + name: 'postgresql.log.sql_state_code', + type: 'keyword', + }, + 'postgresql.log.detail': { + category: 'postgresql', + description: + "More information about the message, parameters in case of a parametrized query. e.g. 'Role \\\"user\\\" does not exist.', 'parameters: $1 = 42', etc. ", + name: 'postgresql.log.detail', + }, + 'postgresql.log.hint': { + category: 'postgresql', + description: 'A possible solution to solve an error. ', + name: 'postgresql.log.hint', + }, + 'postgresql.log.internal_query': { + category: 'postgresql', + description: 'Internal query that led to the error (if any). ', + name: 'postgresql.log.internal_query', + }, + 'postgresql.log.internal_query_pos': { + category: 'postgresql', + description: 'Character count of the internal query (if any). ', + name: 'postgresql.log.internal_query_pos', + type: 'long', + }, + 'postgresql.log.context': { + category: 'postgresql', + description: 'Error context. ', + name: 'postgresql.log.context', + }, + 'postgresql.log.query_pos': { + category: 'postgresql', + description: 'Character count of the error position (if any). ', + name: 'postgresql.log.query_pos', + type: 'long', + }, + 'postgresql.log.location': { + category: 'postgresql', + description: + 'Location of the error in the PostgreSQL source code (if log_error_verbosity is set to verbose). ', + name: 'postgresql.log.location', + }, + 'postgresql.log.application_name': { + category: 'postgresql', + description: 'Name of the application of this event. It is defined by the client. ', + name: 'postgresql.log.application_name', + }, + 'postgresql.log.backend_type': { + category: 'postgresql', + description: + 'Type of backend of this event. Possible types are autovacuum launcher, autovacuum worker, logical replication launcher, logical replication worker, parallel worker, background writer, client backend, checkpointer, startup, walreceiver, walsender and walwriter. In addition, background workers registered by extensions may have additional types. ', + example: 'client backend', + name: 'postgresql.log.backend_type', + }, + 'postgresql.log.error.code': { + category: 'postgresql', + description: + 'Error code returned by Postgres (if any). Deprecated: errors can have letters. Use sql_state_code instead. ', + name: 'postgresql.log.error.code', type: 'alias', }, - 'postgresql.log.thread_id': { + 'postgresql.log.timezone': { category: 'postgresql', - name: 'postgresql.log.thread_id', + name: 'postgresql.log.timezone', type: 'alias', }, 'postgresql.log.user': { @@ -8891,6 +10366,9 @@ export const fieldsBeat: BeatFields = { }, 'postgresql.log.level': { category: 'postgresql', + description: + 'Valid values are DEBUG5, DEBUG4, DEBUG3, DEBUG2, DEBUG1, INFO, NOTICE, WARNING, ERROR, LOG, FATAL, and PANIC. ', + example: 'LOG', name: 'postgresql.log.level', type: 'alias', }, @@ -9537,6 +11015,13 @@ export const fieldsBeat: BeatFields = { name: 'aws.cloudtrail.vpc_endpoint_id', type: 'keyword', }, + 'aws.cloudtrail.event_category': { + category: 'aws', + description: + 'Shows the event category that is used in LookupEvents calls. - For management events, the value is management. - For data events, the value is data. - For Insights events, the value is insight.', + name: 'aws.cloudtrail.event_category', + type: 'keyword', + }, 'aws.cloudtrail.console_login.additional_eventdata.mobile_version': { category: 'aws', description: 'Identifies whether ConsoleLogin was from mobile version', @@ -9580,6 +11065,86 @@ export const fieldsBeat: BeatFields = { name: 'aws.cloudtrail.flattened.service_event_details', type: 'flattened', }, + 'aws.cloudtrail.digest.log_files': { + category: 'aws', + description: 'A list of Logfiles contained in the digest.', + name: 'aws.cloudtrail.digest.log_files', + type: 'nested', + }, + 'aws.cloudtrail.digest.start_time': { + category: 'aws', + description: + 'The starting UTC time range that the digest file covers, taking as a reference the time in which log files have been delivered by CloudTrail.', + name: 'aws.cloudtrail.digest.start_time', + type: 'date', + }, + 'aws.cloudtrail.digest.end_time': { + category: 'aws', + description: + 'The ending UTC time range that the digest file covers, taking as a reference the time in which log files have been delivered by CloudTrail.', + name: 'aws.cloudtrail.digest.end_time', + type: 'date', + }, + 'aws.cloudtrail.digest.s3_bucket': { + category: 'aws', + description: + 'The name of the Amazon S3 bucket to which the current digest file has been delivered.', + name: 'aws.cloudtrail.digest.s3_bucket', + type: 'keyword', + }, + 'aws.cloudtrail.digest.s3_object': { + category: 'aws', + description: + 'The Amazon S3 object key (that is, the Amazon S3 bucket location) of the current digest file.', + name: 'aws.cloudtrail.digest.s3_object', + type: 'keyword', + }, + 'aws.cloudtrail.digest.newest_event_time': { + category: 'aws', + description: + 'The UTC time of the most recent event among all of the events in the log files in the digest.', + name: 'aws.cloudtrail.digest.newest_event_time', + type: 'date', + }, + 'aws.cloudtrail.digest.oldest_event_time': { + category: 'aws', + description: + 'The UTC time of the oldest event among all of the events in the log files in the digest.', + name: 'aws.cloudtrail.digest.oldest_event_time', + type: 'date', + }, + 'aws.cloudtrail.digest.previous_s3_bucket': { + category: 'aws', + description: 'The Amazon S3 bucket to which the previous digest file was delivered.', + name: 'aws.cloudtrail.digest.previous_s3_bucket', + type: 'keyword', + }, + 'aws.cloudtrail.digest.previous_hash_algorithm': { + category: 'aws', + description: 'The name of the hash algorithm that was used to hash the previous digest file.', + name: 'aws.cloudtrail.digest.previous_hash_algorithm', + type: 'keyword', + }, + 'aws.cloudtrail.digest.public_key_fingerprint': { + category: 'aws', + description: + 'The hexadecimal encoded fingerprint of the public key that matches the private key used to sign this digest file.', + name: 'aws.cloudtrail.digest.public_key_fingerprint', + type: 'keyword', + }, + 'aws.cloudtrail.digest.signature_algorithm': { + category: 'aws', + description: 'The algorithm used to sign the digest file.', + name: 'aws.cloudtrail.digest.signature_algorithm', + type: 'keyword', + }, + 'aws.cloudtrail.insight_details': { + category: 'aws', + description: + 'Shows information about the underlying triggers of an Insights event, such as event source, user agent, statistics, API name, and whether the event is the start or end of the Insights event.', + name: 'aws.cloudtrail.insight_details', + type: 'flattened', + }, 'aws.cloudwatch.message': { category: 'aws', description: 'CloudWatch log message. ', @@ -9746,6 +11311,30 @@ export const fieldsBeat: BeatFields = { name: 'aws.elb.error.reason', type: 'keyword', }, + 'aws.elb.target_port': { + category: 'aws', + description: 'List of IP addresses and ports for the targets that processed this request. ', + name: 'aws.elb.target_port', + type: 'keyword', + }, + 'aws.elb.target_status_code': { + category: 'aws', + description: 'List of status codes from the responses of the targets. ', + name: 'aws.elb.target_status_code', + type: 'keyword', + }, + 'aws.elb.classification': { + category: 'aws', + description: 'The classification for desync mitigation. ', + name: 'aws.elb.classification', + type: 'keyword', + }, + 'aws.elb.classification_reason': { + category: 'aws', + description: 'The classification reason code. ', + name: 'aws.elb.classification_reason', + type: 'keyword', + }, 'aws.s3access.bucket_owner': { category: 'aws', description: 'The canonical user ID of the owner of the source bucket. ', @@ -9962,6 +11551,12 @@ export const fieldsBeat: BeatFields = { name: 'aws.vpcflow.tcp_flags', type: 'keyword', }, + 'aws.vpcflow.tcp_flags_array': { + category: 'aws', + description: "List of TCP flags: 'fin, syn, rst, psh, ack, urg' ", + name: 'aws.vpcflow.tcp_flags_array', + type: 'keyword', + }, 'aws.vpcflow.type': { category: 'aws', description: 'The type of traffic: IPv4, IPv6, or EFA. ', @@ -10334,6 +11929,90 @@ export const fieldsBeat: BeatFields = { name: 'azure.auditlogs.properties.initiated_by.user.ipAddress', type: 'keyword', }, + 'azure.platformlogs.operation_name': { + category: 'azure', + description: 'Operation name ', + name: 'azure.platformlogs.operation_name', + type: 'keyword', + }, + 'azure.platformlogs.result_type': { + category: 'azure', + description: 'Result type ', + name: 'azure.platformlogs.result_type', + type: 'keyword', + }, + 'azure.platformlogs.result_signature': { + category: 'azure', + description: 'Result signature ', + name: 'azure.platformlogs.result_signature', + type: 'keyword', + }, + 'azure.platformlogs.category': { + category: 'azure', + description: 'Category ', + name: 'azure.platformlogs.category', + type: 'keyword', + }, + 'azure.platformlogs.event_category': { + category: 'azure', + description: 'Event Category ', + name: 'azure.platformlogs.event_category', + type: 'keyword', + }, + 'azure.platformlogs.status': { + category: 'azure', + description: 'Status ', + name: 'azure.platformlogs.status', + type: 'keyword', + }, + 'azure.platformlogs.ccpNamespace': { + category: 'azure', + description: 'ccpNamespace ', + name: 'azure.platformlogs.ccpNamespace', + type: 'keyword', + }, + 'azure.platformlogs.Cloud': { + category: 'azure', + description: 'Cloud ', + name: 'azure.platformlogs.Cloud', + type: 'keyword', + }, + 'azure.platformlogs.Environment': { + category: 'azure', + description: 'Environment ', + name: 'azure.platformlogs.Environment', + type: 'keyword', + }, + 'azure.platformlogs.EventTimeString': { + category: 'azure', + description: 'EventTimeString ', + name: 'azure.platformlogs.EventTimeString', + type: 'keyword', + }, + 'azure.platformlogs.Caller': { + category: 'azure', + description: 'Caller ', + name: 'azure.platformlogs.Caller', + type: 'keyword', + }, + 'azure.platformlogs.ScaleUnit': { + category: 'azure', + description: 'ScaleUnit ', + name: 'azure.platformlogs.ScaleUnit', + type: 'keyword', + }, + 'azure.platformlogs.ActivityId': { + category: 'azure', + description: 'ActivityId ', + name: 'azure.platformlogs.ActivityId', + type: 'keyword', + }, + 'azure.platformlogs.properties.*': { + category: 'azure', + description: 'Properties ', + name: 'azure.platformlogs.properties.*', + type: 'object', + }, 'azure.signinlogs.operation_name': { category: 'azure', description: 'The operation name ', @@ -17047,6 +18726,331 @@ export const fieldsBeat: BeatFields = { name: 'checkpoint.trusted_domain', type: 'keyword', }, + 'cisco.amp.timestamp_nanoseconds': { + category: 'cisco', + description: 'The timestamp in Epoch nanoseconds. ', + name: 'cisco.amp.timestamp_nanoseconds', + type: 'date', + }, + 'cisco.amp.event_type_id': { + category: 'cisco', + description: 'A sub ID of the event, depending on event type. ', + name: 'cisco.amp.event_type_id', + type: 'keyword', + }, + 'cisco.amp.detection': { + category: 'cisco', + description: 'The name of the malware detected. ', + name: 'cisco.amp.detection', + type: 'keyword', + }, + 'cisco.amp.detection_id': { + category: 'cisco', + description: 'The ID of the detection. ', + name: 'cisco.amp.detection_id', + type: 'keyword', + }, + 'cisco.amp.connector_guid': { + category: 'cisco', + description: 'The GUID of the connector sending information to AMP. ', + name: 'cisco.amp.connector_guid', + type: 'keyword', + }, + 'cisco.amp.group_guids': { + category: 'cisco', + description: 'An array of group GUIDS related to the connector sending information to AMP. ', + name: 'cisco.amp.group_guids', + type: 'keyword', + }, + 'cisco.amp.vulnerabilities': { + category: 'cisco', + description: 'An array of related vulnerabilities to the malicious event. ', + name: 'cisco.amp.vulnerabilities', + type: 'flattened', + }, + 'cisco.amp.scan.description': { + category: 'cisco', + description: + 'Description of an event related to a scan being initiated, for example the specific directory name. ', + name: 'cisco.amp.scan.description', + type: 'keyword', + }, + 'cisco.amp.scan.clean': { + category: 'cisco', + description: 'Boolean value if a scanned file was clean or not. ', + name: 'cisco.amp.scan.clean', + type: 'boolean', + }, + 'cisco.amp.scan.scanned_files': { + category: 'cisco', + description: 'Count of files scanned in a directory. ', + name: 'cisco.amp.scan.scanned_files', + type: 'long', + }, + 'cisco.amp.scan.scanned_processes': { + category: 'cisco', + description: 'Count of processes scanned related to a single scan event. ', + name: 'cisco.amp.scan.scanned_processes', + type: 'long', + }, + 'cisco.amp.scan.scanned_paths': { + category: 'cisco', + description: 'Count of different directories scanned related to a single scan event. ', + name: 'cisco.amp.scan.scanned_paths', + type: 'long', + }, + 'cisco.amp.scan.malicious_detections': { + category: 'cisco', + description: 'Count of malicious files or documents detected related to a single scan event. ', + name: 'cisco.amp.scan.malicious_detections', + type: 'long', + }, + 'cisco.amp.computer.connector_guid': { + category: 'cisco', + description: + 'The GUID of the connector, similar to top level connector_guid, but unique if multiple connectors are involved. ', + name: 'cisco.amp.computer.connector_guid', + type: 'keyword', + }, + 'cisco.amp.computer.external_ip': { + category: 'cisco', + description: 'The external IP of the related host. ', + name: 'cisco.amp.computer.external_ip', + type: 'ip', + }, + 'cisco.amp.computer.active': { + category: 'cisco', + description: 'If the current endpoint is active or not. ', + name: 'cisco.amp.computer.active', + type: 'boolean', + }, + 'cisco.amp.computer.network_addresses': { + category: 'cisco', + description: 'All network interface information on the related host. ', + name: 'cisco.amp.computer.network_addresses', + type: 'flattened', + }, + 'cisco.amp.file.disposition': { + category: 'cisco', + description: 'Categorization of file, for example "Malicious" or "Clean". ', + name: 'cisco.amp.file.disposition', + type: 'keyword', + }, + 'cisco.amp.network_info.disposition': { + category: 'cisco', + description: + 'Categorization of a network event related to a file, for example "Malicious" or "Clean". ', + name: 'cisco.amp.network_info.disposition', + type: 'keyword', + }, + 'cisco.amp.network_info.nfm.direction': { + category: 'cisco', + description: 'The current direction based on source and destination IP. ', + name: 'cisco.amp.network_info.nfm.direction', + type: 'keyword', + }, + 'cisco.amp.related.mac': { + category: 'cisco', + description: 'An array of all related MAC addresses. ', + name: 'cisco.amp.related.mac', + type: 'keyword', + }, + 'cisco.amp.related.cve': { + category: 'cisco', + description: 'An array of all related MAC addresses. ', + name: 'cisco.amp.related.cve', + type: 'keyword', + }, + 'cisco.amp.cloud_ioc.description': { + category: 'cisco', + description: 'Description of the related IOC for specific IOC events from AMP. ', + name: 'cisco.amp.cloud_ioc.description', + type: 'keyword', + }, + 'cisco.amp.cloud_ioc.short_description': { + category: 'cisco', + description: 'Short description of the related IOC for specific IOC events from AMP. ', + name: 'cisco.amp.cloud_ioc.short_description', + type: 'keyword', + }, + 'cisco.amp.network_info.parent.disposition': { + category: 'cisco', + description: 'Categorization of a IOC for example "Malicious" or "Clean". ', + name: 'cisco.amp.network_info.parent.disposition', + type: 'keyword', + }, + 'cisco.amp.network_info.parent.identity.md5': { + category: 'cisco', + description: 'MD5 hash of the related IOC. ', + name: 'cisco.amp.network_info.parent.identity.md5', + type: 'keyword', + }, + 'cisco.amp.network_info.parent.identity.sha1': { + category: 'cisco', + description: 'SHA1 hash of the related IOC. ', + name: 'cisco.amp.network_info.parent.identity.sha1', + type: 'keyword', + }, + 'cisco.amp.network_info.parent.identify.sha256': { + category: 'cisco', + description: 'SHA256 hash of the related IOC. ', + name: 'cisco.amp.network_info.parent.identify.sha256', + type: 'keyword', + }, + 'cisco.amp.file.archived_file.disposition': { + category: 'cisco', + description: + 'Categorization of a file archive related to a file, for example "Malicious" or "Clean". ', + name: 'cisco.amp.file.archived_file.disposition', + type: 'keyword', + }, + 'cisco.amp.file.archived_file.identity.md5': { + category: 'cisco', + description: 'MD5 hash of the archived file related to the malicious event. ', + name: 'cisco.amp.file.archived_file.identity.md5', + type: 'keyword', + }, + 'cisco.amp.file.archived_file.identity.sha1': { + category: 'cisco', + description: 'SHA1 hash of the archived file related to the malicious event. ', + name: 'cisco.amp.file.archived_file.identity.sha1', + type: 'keyword', + }, + 'cisco.amp.file.archived_file.identify.sha256': { + category: 'cisco', + description: 'SHA256 hash of the archived file related to the malicious event. ', + name: 'cisco.amp.file.archived_file.identify.sha256', + type: 'keyword', + }, + 'cisco.amp.file.attack_details.application': { + category: 'cisco', + description: 'The application name related to Exploit Prevention events. ', + name: 'cisco.amp.file.attack_details.application', + type: 'keyword', + }, + 'cisco.amp.file.attack_details.attacked_module': { + category: 'cisco', + description: + 'Path to the executable or dll that was attacked and detected by Exploit Prevention. ', + name: 'cisco.amp.file.attack_details.attacked_module', + type: 'keyword', + }, + 'cisco.amp.file.attack_details.base_address': { + category: 'cisco', + description: 'The base memory address related to the exploit detected. ', + name: 'cisco.amp.file.attack_details.base_address', + type: 'keyword', + }, + 'cisco.amp.file.attack_details.suspicious_files': { + category: 'cisco', + description: 'An array of related files when an attack is detected by Exploit Prevention. ', + name: 'cisco.amp.file.attack_details.suspicious_files', + type: 'keyword', + }, + 'cisco.amp.file.parent.disposition': { + category: 'cisco', + description: 'Categorization of parrent, for example "Malicious" or "Clean". ', + name: 'cisco.amp.file.parent.disposition', + type: 'keyword', + }, + 'cisco.amp.error.description': { + category: 'cisco', + description: 'Description of an endpoint error event. ', + name: 'cisco.amp.error.description', + type: 'keyword', + }, + 'cisco.amp.error.error_code': { + category: 'cisco', + description: 'The error code describing the related error event. ', + name: 'cisco.amp.error.error_code', + type: 'keyword', + }, + 'cisco.amp.threat_hunting.severity': { + category: 'cisco', + description: + 'Severity result of the threat hunt registered to the malicious event. Can be Low-Critical. ', + name: 'cisco.amp.threat_hunting.severity', + type: 'keyword', + }, + 'cisco.amp.threat_hunting.incident_report_guid': { + category: 'cisco', + description: 'The GUID of the related threat hunting report. ', + name: 'cisco.amp.threat_hunting.incident_report_guid', + type: 'keyword', + }, + 'cisco.amp.threat_hunting.incident_hunt_guid': { + category: 'cisco', + description: 'The GUID of the related investigation tracking issue. ', + name: 'cisco.amp.threat_hunting.incident_hunt_guid', + type: 'keyword', + }, + 'cisco.amp.threat_hunting.incident_title': { + category: 'cisco', + description: 'Title of the incident related to the threat hunting activity. ', + name: 'cisco.amp.threat_hunting.incident_title', + type: 'keyword', + }, + 'cisco.amp.threat_hunting.incident_summary': { + category: 'cisco', + description: 'Summary of the outcome on the threat hunting activity. ', + name: 'cisco.amp.threat_hunting.incident_summary', + type: 'keyword', + }, + 'cisco.amp.threat_hunting.incident_remediation': { + category: 'cisco', + description: 'Recommendations to resolve the vulnerability or exploited host. ', + name: 'cisco.amp.threat_hunting.incident_remediation', + type: 'keyword', + }, + 'cisco.amp.threat_hunting.incident_id': { + category: 'cisco', + description: 'The id of the related incident for the threat hunting activity. ', + name: 'cisco.amp.threat_hunting.incident_id', + type: 'keyword', + }, + 'cisco.amp.threat_hunting.incident_end_time': { + category: 'cisco', + description: 'When the threat hunt finalized or closed. ', + name: 'cisco.amp.threat_hunting.incident_end_time', + type: 'date', + }, + 'cisco.amp.threat_hunting.incident_start_time': { + category: 'cisco', + description: 'When the threat hunt was initiated. ', + name: 'cisco.amp.threat_hunting.incident_start_time', + type: 'date', + }, + 'cisco.amp.file.attack_details.indicators': { + category: 'cisco', + description: + 'Different indicator types that matches the exploit detected, for example different MITRE tactics. ', + name: 'cisco.amp.file.attack_details.indicators', + type: 'flattened', + }, + 'cisco.amp.threat_hunting.tactics': { + category: 'cisco', + description: 'List of all MITRE tactics related to the incident found. ', + name: 'cisco.amp.threat_hunting.tactics', + type: 'flattened', + }, + 'cisco.amp.threat_hunting.techniques': { + category: 'cisco', + description: 'List of all MITRE techniques related to the incident found. ', + name: 'cisco.amp.threat_hunting.techniques', + type: 'flattened', + }, + 'cisco.amp.tactics': { + category: 'cisco', + description: 'List of all MITRE tactics related to the incident found. ', + name: 'cisco.amp.tactics', + type: 'flattened', + }, + 'cisco.amp.techniques': { + category: 'cisco', + description: 'List of all MITRE techniques related to the incident found. ', + name: 'cisco.amp.techniques', + type: 'flattened', + }, 'cisco.asa.message_id': { category: 'cisco', description: 'The Cisco ASA message identifier. ', @@ -17170,6 +19174,72 @@ export const fieldsBeat: BeatFields = { name: 'cisco.asa.dap_records', type: 'keyword', }, + 'cisco.asa.command_line_arguments': { + category: 'cisco', + description: 'The command line arguments logged by the local audit log ', + name: 'cisco.asa.command_line_arguments', + type: 'keyword', + }, + 'cisco.asa.assigned_ip': { + category: 'cisco', + description: 'The IP address assigned to a VPN client successfully connecting ', + name: 'cisco.asa.assigned_ip', + type: 'ip', + }, + 'cisco.asa.privilege.old': { + category: 'cisco', + description: 'When a users privilege is changed this is the old value ', + name: 'cisco.asa.privilege.old', + type: 'keyword', + }, + 'cisco.asa.privilege.new': { + category: 'cisco', + description: 'When a users privilege is changed this is the new value ', + name: 'cisco.asa.privilege.new', + type: 'keyword', + }, + 'cisco.asa.burst.object': { + category: 'cisco', + description: 'The related object for burst warnings ', + name: 'cisco.asa.burst.object', + type: 'keyword', + }, + 'cisco.asa.burst.id': { + category: 'cisco', + description: 'The related rate ID for burst warnings ', + name: 'cisco.asa.burst.id', + type: 'keyword', + }, + 'cisco.asa.burst.current_rate': { + category: 'cisco', + description: 'The current burst rate seen ', + name: 'cisco.asa.burst.current_rate', + type: 'keyword', + }, + 'cisco.asa.burst.configured_rate': { + category: 'cisco', + description: 'The current configured burst rate ', + name: 'cisco.asa.burst.configured_rate', + type: 'keyword', + }, + 'cisco.asa.burst.avg_rate': { + category: 'cisco', + description: 'The current average burst rate seen ', + name: 'cisco.asa.burst.avg_rate', + type: 'keyword', + }, + 'cisco.asa.burst.configured_avg_rate': { + category: 'cisco', + description: 'The current configured average burst rate allowed ', + name: 'cisco.asa.burst.configured_avg_rate', + type: 'keyword', + }, + 'cisco.asa.burst.cumulative_count': { + category: 'cisco', + description: 'The total count of burst rate hits since the object was created or cleared ', + name: 'cisco.asa.burst.cumulative_count', + type: 'keyword', + }, 'cisco.ftd.message_id': { category: 'cisco', description: 'The Cisco FTD message identifier. ', @@ -17313,6 +19383,96 @@ export const fieldsBeat: BeatFields = { name: 'cisco.ios.facility', type: 'keyword', }, + 'cisco.umbrella.identities': { + category: 'cisco', + description: 'An array of the different identities related to the event. ', + name: 'cisco.umbrella.identities', + type: 'keyword', + }, + 'cisco.umbrella.categories': { + category: 'cisco', + description: 'The security or content categories that the destination matches. ', + name: 'cisco.umbrella.categories', + type: 'keyword', + }, + 'cisco.umbrella.policy_identity_type': { + category: 'cisco', + description: + 'The first identity type matched with this request. Available in version 3 and above. ', + name: 'cisco.umbrella.policy_identity_type', + type: 'keyword', + }, + 'cisco.umbrella.identity_types': { + category: 'cisco', + description: + 'The type of identity that made the request. For example, Roaming Computer or Network. ', + name: 'cisco.umbrella.identity_types', + type: 'keyword', + }, + 'cisco.umbrella.blocked_categories': { + category: 'cisco', + description: + 'The categories that resulted in the destination being blocked. Available in version 4 and above. ', + name: 'cisco.umbrella.blocked_categories', + type: 'keyword', + }, + 'cisco.umbrella.content_type': { + category: 'cisco', + description: 'The type of web content, typically text/html. ', + name: 'cisco.umbrella.content_type', + type: 'keyword', + }, + 'cisco.umbrella.sha_sha256': { + category: 'cisco', + description: 'Hex digest of the response content. ', + name: 'cisco.umbrella.sha_sha256', + type: 'keyword', + }, + 'cisco.umbrella.av_detections': { + category: 'cisco', + description: 'The detection name according to the antivirus engine used in file inspection. ', + name: 'cisco.umbrella.av_detections', + type: 'keyword', + }, + 'cisco.umbrella.puas': { + category: 'cisco', + description: + 'A list of all potentially unwanted application (PUA) results for the proxied file as returned by the antivirus scanner. ', + name: 'cisco.umbrella.puas', + type: 'keyword', + }, + 'cisco.umbrella.amp_disposition': { + category: 'cisco', + description: + 'The status of the files proxied and scanned by Cisco Advanced Malware Protection (AMP) as part of the Umbrella File Inspection feature; can be Clean, Malicious or Unknown. ', + name: 'cisco.umbrella.amp_disposition', + type: 'keyword', + }, + 'cisco.umbrella.amp_malware_name': { + category: 'cisco', + description: 'If Malicious, the name of the malware according to AMP. ', + name: 'cisco.umbrella.amp_malware_name', + type: 'keyword', + }, + 'cisco.umbrella.amp_score': { + category: 'cisco', + description: + 'The score of the malware from AMP. This field is not currently used and will be blank. ', + name: 'cisco.umbrella.amp_score', + type: 'keyword', + }, + 'cisco.umbrella.datacenter': { + category: 'cisco', + description: 'The name of the Umbrella Data Center that processed the user-generated traffic. ', + name: 'cisco.umbrella.datacenter', + type: 'keyword', + }, + 'cisco.umbrella.origin_id': { + category: 'cisco', + description: 'The unique identity of the network tunnel. ', + name: 'cisco.umbrella.origin_id', + type: 'keyword', + }, 'coredns.id': { category: 'coredns', description: 'id of the DNS transaction ', @@ -19284,7 +21444,7 @@ export const fieldsBeat: BeatFields = { category: 'fortinet', description: 'Memory usage system statistics ', name: 'fortinet.firewall.mem', - type: 'keyword', + type: 'integer', }, 'fortinet.firewall.meshmode': { category: 'fortinet', @@ -20504,341 +22664,1174 @@ export const fieldsBeat: BeatFields = { name: 'fortinet.firewall.weakwepiv', type: 'keyword', }, - 'fortinet.firewall.xauthgroup': { - category: 'fortinet', - description: 'XAuth Group Name ', - name: 'fortinet.firewall.xauthgroup', + 'fortinet.firewall.xauthgroup': { + category: 'fortinet', + description: 'XAuth Group Name ', + name: 'fortinet.firewall.xauthgroup', + type: 'keyword', + }, + 'fortinet.firewall.xauthuser': { + category: 'fortinet', + description: 'XAuth User Name ', + name: 'fortinet.firewall.xauthuser', + type: 'keyword', + }, + 'fortinet.firewall.xid': { + category: 'fortinet', + description: 'Wireless X ID ', + name: 'fortinet.firewall.xid', + type: 'integer', + }, + 'googlecloud.destination.instance.project_id': { + category: 'googlecloud', + description: 'ID of the project containing the VM. ', + name: 'googlecloud.destination.instance.project_id', + type: 'keyword', + }, + 'googlecloud.destination.instance.region': { + category: 'googlecloud', + description: 'Region of the VM. ', + name: 'googlecloud.destination.instance.region', + type: 'keyword', + }, + 'googlecloud.destination.instance.zone': { + category: 'googlecloud', + description: 'Zone of the VM. ', + name: 'googlecloud.destination.instance.zone', + type: 'keyword', + }, + 'googlecloud.destination.vpc.project_id': { + category: 'googlecloud', + description: 'ID of the project containing the VM. ', + name: 'googlecloud.destination.vpc.project_id', + type: 'keyword', + }, + 'googlecloud.destination.vpc.vpc_name': { + category: 'googlecloud', + description: 'VPC on which the VM is operating. ', + name: 'googlecloud.destination.vpc.vpc_name', + type: 'keyword', + }, + 'googlecloud.destination.vpc.subnetwork_name': { + category: 'googlecloud', + description: 'Subnetwork on which the VM is operating. ', + name: 'googlecloud.destination.vpc.subnetwork_name', + type: 'keyword', + }, + 'googlecloud.source.instance.project_id': { + category: 'googlecloud', + description: 'ID of the project containing the VM. ', + name: 'googlecloud.source.instance.project_id', + type: 'keyword', + }, + 'googlecloud.source.instance.region': { + category: 'googlecloud', + description: 'Region of the VM. ', + name: 'googlecloud.source.instance.region', + type: 'keyword', + }, + 'googlecloud.source.instance.zone': { + category: 'googlecloud', + description: 'Zone of the VM. ', + name: 'googlecloud.source.instance.zone', + type: 'keyword', + }, + 'googlecloud.source.vpc.project_id': { + category: 'googlecloud', + description: 'ID of the project containing the VM. ', + name: 'googlecloud.source.vpc.project_id', + type: 'keyword', + }, + 'googlecloud.source.vpc.vpc_name': { + category: 'googlecloud', + description: 'VPC on which the VM is operating. ', + name: 'googlecloud.source.vpc.vpc_name', + type: 'keyword', + }, + 'googlecloud.source.vpc.subnetwork_name': { + category: 'googlecloud', + description: 'Subnetwork on which the VM is operating. ', + name: 'googlecloud.source.vpc.subnetwork_name', + type: 'keyword', + }, + 'googlecloud.audit.type': { + category: 'googlecloud', + description: 'Type property. ', + name: 'googlecloud.audit.type', + type: 'keyword', + }, + 'googlecloud.audit.authentication_info.principal_email': { + category: 'googlecloud', + description: 'The email address of the authenticated user making the request. ', + name: 'googlecloud.audit.authentication_info.principal_email', + type: 'keyword', + }, + 'googlecloud.audit.authentication_info.authority_selector': { + category: 'googlecloud', + description: + 'The authority selector specified by the requestor, if any. It is not guaranteed that the principal was allowed to use this authority. ', + name: 'googlecloud.audit.authentication_info.authority_selector', + type: 'keyword', + }, + 'googlecloud.audit.authorization_info.permission': { + category: 'googlecloud', + description: 'The required IAM permission. ', + name: 'googlecloud.audit.authorization_info.permission', + type: 'keyword', + }, + 'googlecloud.audit.authorization_info.granted': { + category: 'googlecloud', + description: 'Whether or not authorization for resource and permission was granted. ', + name: 'googlecloud.audit.authorization_info.granted', + type: 'boolean', + }, + 'googlecloud.audit.authorization_info.resource_attributes.service': { + category: 'googlecloud', + description: 'The name of the service. ', + name: 'googlecloud.audit.authorization_info.resource_attributes.service', + type: 'keyword', + }, + 'googlecloud.audit.authorization_info.resource_attributes.name': { + category: 'googlecloud', + description: 'The name of the resource. ', + name: 'googlecloud.audit.authorization_info.resource_attributes.name', + type: 'keyword', + }, + 'googlecloud.audit.authorization_info.resource_attributes.type': { + category: 'googlecloud', + description: 'The type of the resource. ', + name: 'googlecloud.audit.authorization_info.resource_attributes.type', + type: 'keyword', + }, + 'googlecloud.audit.method_name': { + category: 'googlecloud', + description: + "The name of the service method or operation. For API calls, this should be the name of the API method. For example, 'google.datastore.v1.Datastore.RunQuery'. ", + name: 'googlecloud.audit.method_name', + type: 'keyword', + }, + 'googlecloud.audit.num_response_items': { + category: 'googlecloud', + description: 'The number of items returned from a List or Query API method, if applicable. ', + name: 'googlecloud.audit.num_response_items', + type: 'long', + }, + 'googlecloud.audit.request.proto_name': { + category: 'googlecloud', + description: 'Type property of the request. ', + name: 'googlecloud.audit.request.proto_name', + type: 'keyword', + }, + 'googlecloud.audit.request.filter': { + category: 'googlecloud', + description: 'Filter of the request. ', + name: 'googlecloud.audit.request.filter', + type: 'keyword', + }, + 'googlecloud.audit.request.name': { + category: 'googlecloud', + description: 'Name of the request. ', + name: 'googlecloud.audit.request.name', + type: 'keyword', + }, + 'googlecloud.audit.request.resource_name': { + category: 'googlecloud', + description: 'Name of the request resource. ', + name: 'googlecloud.audit.request.resource_name', + type: 'keyword', + }, + 'googlecloud.audit.request_metadata.caller_ip': { + category: 'googlecloud', + description: 'The IP address of the caller. ', + name: 'googlecloud.audit.request_metadata.caller_ip', + type: 'ip', + }, + 'googlecloud.audit.request_metadata.caller_supplied_user_agent': { + category: 'googlecloud', + description: + 'The user agent of the caller. This information is not authenticated and should be treated accordingly. ', + name: 'googlecloud.audit.request_metadata.caller_supplied_user_agent', + type: 'keyword', + }, + 'googlecloud.audit.response.proto_name': { + category: 'googlecloud', + description: 'Type property of the response. ', + name: 'googlecloud.audit.response.proto_name', + type: 'keyword', + }, + 'googlecloud.audit.response.details.group': { + category: 'googlecloud', + description: 'The name of the group. ', + name: 'googlecloud.audit.response.details.group', + type: 'keyword', + }, + 'googlecloud.audit.response.details.kind': { + category: 'googlecloud', + description: 'The kind of the response details. ', + name: 'googlecloud.audit.response.details.kind', + type: 'keyword', + }, + 'googlecloud.audit.response.details.name': { + category: 'googlecloud', + description: 'The name of the response details. ', + name: 'googlecloud.audit.response.details.name', + type: 'keyword', + }, + 'googlecloud.audit.response.details.uid': { + category: 'googlecloud', + description: 'The uid of the response details. ', + name: 'googlecloud.audit.response.details.uid', + type: 'keyword', + }, + 'googlecloud.audit.response.status': { + category: 'googlecloud', + description: 'Status of the response. ', + name: 'googlecloud.audit.response.status', + type: 'keyword', + }, + 'googlecloud.audit.resource_name': { + category: 'googlecloud', + description: + "The resource or collection that is the target of the operation. The name is a scheme-less URI, not including the API service name. For example, 'shelves/SHELF_ID/books'. ", + name: 'googlecloud.audit.resource_name', + type: 'keyword', + }, + 'googlecloud.audit.resource_location.current_locations': { + category: 'googlecloud', + description: 'Current locations of the resource. ', + name: 'googlecloud.audit.resource_location.current_locations', + type: 'keyword', + }, + 'googlecloud.audit.service_name': { + category: 'googlecloud', + description: + 'The name of the API service performing the operation. For example, datastore.googleapis.com. ', + name: 'googlecloud.audit.service_name', + type: 'keyword', + }, + 'googlecloud.audit.status.code': { + category: 'googlecloud', + description: 'The status code, which should be an enum value of google.rpc.Code. ', + name: 'googlecloud.audit.status.code', + type: 'integer', + }, + 'googlecloud.audit.status.message': { + category: 'googlecloud', + description: + 'A developer-facing error message, which should be in English. Any user-facing error message should be localized and sent in the google.rpc.Status.details field, or localized by the client. ', + name: 'googlecloud.audit.status.message', + type: 'keyword', + }, + 'googlecloud.firewall.rule_details.priority': { + category: 'googlecloud', + description: 'The priority for the firewall rule.', + name: 'googlecloud.firewall.rule_details.priority', + type: 'long', + }, + 'googlecloud.firewall.rule_details.action': { + category: 'googlecloud', + description: 'Action that the rule performs on match.', + name: 'googlecloud.firewall.rule_details.action', + type: 'keyword', + }, + 'googlecloud.firewall.rule_details.direction': { + category: 'googlecloud', + description: 'Direction of traffic that matches this rule.', + name: 'googlecloud.firewall.rule_details.direction', + type: 'keyword', + }, + 'googlecloud.firewall.rule_details.reference': { + category: 'googlecloud', + description: 'Reference to the firewall rule.', + name: 'googlecloud.firewall.rule_details.reference', + type: 'keyword', + }, + 'googlecloud.firewall.rule_details.source_range': { + category: 'googlecloud', + description: 'List of source ranges that the firewall rule applies to.', + name: 'googlecloud.firewall.rule_details.source_range', + type: 'keyword', + }, + 'googlecloud.firewall.rule_details.destination_range': { + category: 'googlecloud', + description: 'List of destination ranges that the firewall applies to.', + name: 'googlecloud.firewall.rule_details.destination_range', + type: 'keyword', + }, + 'googlecloud.firewall.rule_details.source_tag': { + category: 'googlecloud', + description: 'List of all the source tags that the firewall rule applies to. ', + name: 'googlecloud.firewall.rule_details.source_tag', + type: 'keyword', + }, + 'googlecloud.firewall.rule_details.target_tag': { + category: 'googlecloud', + description: 'List of all the target tags that the firewall rule applies to. ', + name: 'googlecloud.firewall.rule_details.target_tag', + type: 'keyword', + }, + 'googlecloud.firewall.rule_details.ip_port_info': { + category: 'googlecloud', + description: 'List of ip protocols and applicable port ranges for rules. ', + name: 'googlecloud.firewall.rule_details.ip_port_info', + type: 'array', + }, + 'googlecloud.firewall.rule_details.source_service_account': { + category: 'googlecloud', + description: 'List of all the source service accounts that the firewall rule applies to. ', + name: 'googlecloud.firewall.rule_details.source_service_account', + type: 'keyword', + }, + 'googlecloud.firewall.rule_details.target_service_account': { + category: 'googlecloud', + description: 'List of all the target service accounts that the firewall rule applies to. ', + name: 'googlecloud.firewall.rule_details.target_service_account', + type: 'keyword', + }, + 'googlecloud.vpcflow.reporter': { + category: 'googlecloud', + description: "The side which reported the flow. Can be either 'SRC' or 'DEST'. ", + name: 'googlecloud.vpcflow.reporter', + type: 'keyword', + }, + 'googlecloud.vpcflow.rtt.ms': { + category: 'googlecloud', + description: + 'Latency as measured (for TCP flows only) during the time interval. This is the time elapsed between sending a SEQ and receiving a corresponding ACK and it contains the network RTT as well as the application related delay. ', + name: 'googlecloud.vpcflow.rtt.ms', + type: 'long', + }, + 'google_workspace.actor.type': { + category: 'google_workspace', + description: + 'The type of actor. Values can be: *USER*: Another user in the same domain. *EXTERNAL_USER*: A user outside the domain. *KEY*: A non-human actor. ', + name: 'google_workspace.actor.type', + type: 'keyword', + }, + 'google_workspace.actor.key': { + category: 'google_workspace', + description: + 'Only present when `actor.type` is `KEY`. Can be the `consumer_key` of the requestor for OAuth 2LO API requests or an identifier for robot accounts. ', + name: 'google_workspace.actor.key', + type: 'keyword', + }, + 'google_workspace.event.type': { + category: 'google_workspace', + description: + 'The type of Google Workspace event, mapped from `items[].events[].type` in the original payload. Each fileset can have a different set of values for it, more details can be found at https://developers.google.com/admin-sdk/reports/v1/reference/activities/list ', + example: 'audit#activity', + name: 'google_workspace.event.type', + type: 'keyword', + }, + 'google_workspace.kind': { + category: 'google_workspace', + description: + 'The type of API resource, mapped from `kind` in the original payload. More details can be found at https://developers.google.com/admin-sdk/reports/v1/reference/activities/list ', + example: 'audit#activity', + name: 'google_workspace.kind', + type: 'keyword', + }, + 'google_workspace.organization.domain': { + category: 'google_workspace', + description: "The domain that is affected by the report's event. ", + name: 'google_workspace.organization.domain', + type: 'keyword', + }, + 'google_workspace.admin.application.edition': { + category: 'google_workspace', + description: 'The Google Workspace edition.', + name: 'google_workspace.admin.application.edition', + type: 'keyword', + }, + 'google_workspace.admin.application.name': { + category: 'google_workspace', + description: "The application's name.", + name: 'google_workspace.admin.application.name', + type: 'keyword', + }, + 'google_workspace.admin.application.enabled': { + category: 'google_workspace', + description: 'The enabled application.', + name: 'google_workspace.admin.application.enabled', + type: 'keyword', + }, + 'google_workspace.admin.application.licences_order_number': { + category: 'google_workspace', + description: 'Order number used to redeem licenses.', + name: 'google_workspace.admin.application.licences_order_number', + type: 'keyword', + }, + 'google_workspace.admin.application.licences_purchased': { + category: 'google_workspace', + description: 'Number of licences purchased.', + name: 'google_workspace.admin.application.licences_purchased', + type: 'keyword', + }, + 'google_workspace.admin.application.id': { + category: 'google_workspace', + description: 'The application ID.', + name: 'google_workspace.admin.application.id', + type: 'keyword', + }, + 'google_workspace.admin.application.asp_id': { + category: 'google_workspace', + description: 'The application specific password ID.', + name: 'google_workspace.admin.application.asp_id', + type: 'keyword', + }, + 'google_workspace.admin.application.package_id': { + category: 'google_workspace', + description: 'The mobile application package ID.', + name: 'google_workspace.admin.application.package_id', + type: 'keyword', + }, + 'google_workspace.admin.group.email': { + category: 'google_workspace', + description: "The group's primary email address.", + name: 'google_workspace.admin.group.email', + type: 'keyword', + }, + 'google_workspace.admin.new_value': { + category: 'google_workspace', + description: 'The new value for the setting.', + name: 'google_workspace.admin.new_value', + type: 'keyword', + }, + 'google_workspace.admin.old_value': { + category: 'google_workspace', + description: 'The old value for the setting.', + name: 'google_workspace.admin.old_value', + type: 'keyword', + }, + 'google_workspace.admin.org_unit.name': { + category: 'google_workspace', + description: 'The organizational unit name.', + name: 'google_workspace.admin.org_unit.name', + type: 'keyword', + }, + 'google_workspace.admin.org_unit.full': { + category: 'google_workspace', + description: 'The org unit full path including the root org unit name.', + name: 'google_workspace.admin.org_unit.full', + type: 'keyword', + }, + 'google_workspace.admin.setting.name': { + category: 'google_workspace', + description: 'The setting name.', + name: 'google_workspace.admin.setting.name', + type: 'keyword', + }, + 'google_workspace.admin.user_defined_setting.name': { + category: 'google_workspace', + description: 'The name of the user-defined setting.', + name: 'google_workspace.admin.user_defined_setting.name', + type: 'keyword', + }, + 'google_workspace.admin.setting.description': { + category: 'google_workspace', + description: 'The setting name.', + name: 'google_workspace.admin.setting.description', + type: 'keyword', + }, + 'google_workspace.admin.group.priorities': { + category: 'google_workspace', + description: 'Group priorities.', + name: 'google_workspace.admin.group.priorities', + type: 'keyword', + }, + 'google_workspace.admin.domain.alias': { + category: 'google_workspace', + description: 'The domain alias.', + name: 'google_workspace.admin.domain.alias', + type: 'keyword', + }, + 'google_workspace.admin.domain.name': { + category: 'google_workspace', + description: 'The primary domain name.', + name: 'google_workspace.admin.domain.name', + type: 'keyword', + }, + 'google_workspace.admin.domain.secondary_name': { + category: 'google_workspace', + description: 'The secondary domain name.', + name: 'google_workspace.admin.domain.secondary_name', + type: 'keyword', + }, + 'google_workspace.admin.managed_configuration': { + category: 'google_workspace', + description: 'The name of the managed configuration.', + name: 'google_workspace.admin.managed_configuration', + type: 'keyword', + }, + 'google_workspace.admin.non_featured_services_selection': { + category: 'google_workspace', + description: + 'Non-featured services selection. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/admin-application-settings#FLASHLIGHT_EDU_NON_FEATURED_SERVICES_SELECTED ', + name: 'google_workspace.admin.non_featured_services_selection', + type: 'keyword', + }, + 'google_workspace.admin.field': { + category: 'google_workspace', + description: 'The name of the field.', + name: 'google_workspace.admin.field', + type: 'keyword', + }, + 'google_workspace.admin.resource.id': { + category: 'google_workspace', + description: 'The name of the resource identifier.', + name: 'google_workspace.admin.resource.id', + type: 'keyword', + }, + 'google_workspace.admin.user.email': { + category: 'google_workspace', + description: "The user's primary email address.", + name: 'google_workspace.admin.user.email', + type: 'keyword', + }, + 'google_workspace.admin.user.nickname': { + category: 'google_workspace', + description: "The user's nickname.", + name: 'google_workspace.admin.user.nickname', + type: 'keyword', + }, + 'google_workspace.admin.user.birthdate': { + category: 'google_workspace', + description: "The user's birth date.", + name: 'google_workspace.admin.user.birthdate', + type: 'date', + }, + 'google_workspace.admin.gateway.name': { + category: 'google_workspace', + description: 'Gateway name. Present on some chat settings.', + name: 'google_workspace.admin.gateway.name', + type: 'keyword', + }, + 'google_workspace.admin.chrome_os.session_type': { + category: 'google_workspace', + description: 'Chrome OS session type.', + name: 'google_workspace.admin.chrome_os.session_type', + type: 'keyword', + }, + 'google_workspace.admin.device.serial_number': { + category: 'google_workspace', + description: 'Device serial number.', + name: 'google_workspace.admin.device.serial_number', + type: 'keyword', + }, + 'google_workspace.admin.device.id': { + category: 'google_workspace', + name: 'google_workspace.admin.device.id', + type: 'keyword', + }, + 'google_workspace.admin.device.type': { + category: 'google_workspace', + description: 'Device type.', + name: 'google_workspace.admin.device.type', + type: 'keyword', + }, + 'google_workspace.admin.print_server.name': { + category: 'google_workspace', + description: 'The name of the print server.', + name: 'google_workspace.admin.print_server.name', + type: 'keyword', + }, + 'google_workspace.admin.printer.name': { + category: 'google_workspace', + description: 'The name of the printer.', + name: 'google_workspace.admin.printer.name', + type: 'keyword', + }, + 'google_workspace.admin.device.command_details': { + category: 'google_workspace', + description: 'Command details.', + name: 'google_workspace.admin.device.command_details', + type: 'keyword', + }, + 'google_workspace.admin.role.id': { + category: 'google_workspace', + description: 'Unique identifier for this role privilege.', + name: 'google_workspace.admin.role.id', + type: 'keyword', + }, + 'google_workspace.admin.role.name': { + category: 'google_workspace', + description: + 'The role name. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/admin-delegated-admin-settings ', + name: 'google_workspace.admin.role.name', + type: 'keyword', + }, + 'google_workspace.admin.privilege.name': { + category: 'google_workspace', + description: 'Privilege name.', + name: 'google_workspace.admin.privilege.name', + type: 'keyword', + }, + 'google_workspace.admin.service.name': { + category: 'google_workspace', + description: 'The service name.', + name: 'google_workspace.admin.service.name', + type: 'keyword', + }, + 'google_workspace.admin.url.name': { + category: 'google_workspace', + description: 'The website name.', + name: 'google_workspace.admin.url.name', + type: 'keyword', + }, + 'google_workspace.admin.product.name': { + category: 'google_workspace', + description: 'The product name.', + name: 'google_workspace.admin.product.name', + type: 'keyword', + }, + 'google_workspace.admin.product.sku': { + category: 'google_workspace', + description: 'The product SKU.', + name: 'google_workspace.admin.product.sku', + type: 'keyword', + }, + 'google_workspace.admin.bulk_upload.failed': { + category: 'google_workspace', + description: 'Number of failed records in bulk upload operation.', + name: 'google_workspace.admin.bulk_upload.failed', + type: 'long', + }, + 'google_workspace.admin.bulk_upload.total': { + category: 'google_workspace', + description: 'Number of total records in bulk upload operation.', + name: 'google_workspace.admin.bulk_upload.total', + type: 'long', + }, + 'google_workspace.admin.group.allowed_list': { + category: 'google_workspace', + description: 'Names of allow-listed groups.', + name: 'google_workspace.admin.group.allowed_list', + type: 'keyword', + }, + 'google_workspace.admin.email.quarantine_name': { + category: 'google_workspace', + description: 'The name of the quarantine.', + name: 'google_workspace.admin.email.quarantine_name', + type: 'keyword', + }, + 'google_workspace.admin.email.log_search_filter.message_id': { + category: 'google_workspace', + description: "The log search filter's email message ID.", + name: 'google_workspace.admin.email.log_search_filter.message_id', + type: 'keyword', + }, + 'google_workspace.admin.email.log_search_filter.start_date': { + category: 'google_workspace', + description: "The log search filter's start date.", + name: 'google_workspace.admin.email.log_search_filter.start_date', + type: 'date', + }, + 'google_workspace.admin.email.log_search_filter.end_date': { + category: 'google_workspace', + description: "The log search filter's ending date.", + name: 'google_workspace.admin.email.log_search_filter.end_date', + type: 'date', + }, + 'google_workspace.admin.email.log_search_filter.recipient.value': { + category: 'google_workspace', + description: "The log search filter's email recipient.", + name: 'google_workspace.admin.email.log_search_filter.recipient.value', + type: 'keyword', + }, + 'google_workspace.admin.email.log_search_filter.sender.value': { + category: 'google_workspace', + description: "The log search filter's email sender.", + name: 'google_workspace.admin.email.log_search_filter.sender.value', + type: 'keyword', + }, + 'google_workspace.admin.email.log_search_filter.recipient.ip': { + category: 'google_workspace', + description: "The log search filter's email recipient's IP address.", + name: 'google_workspace.admin.email.log_search_filter.recipient.ip', + type: 'ip', + }, + 'google_workspace.admin.email.log_search_filter.sender.ip': { + category: 'google_workspace', + description: "The log search filter's email sender's IP address.", + name: 'google_workspace.admin.email.log_search_filter.sender.ip', + type: 'ip', + }, + 'google_workspace.admin.chrome_licenses.enabled': { + category: 'google_workspace', + description: + 'Licences enabled. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/admin-org-settings ', + name: 'google_workspace.admin.chrome_licenses.enabled', + type: 'keyword', + }, + 'google_workspace.admin.chrome_licenses.allowed': { + category: 'google_workspace', + description: + 'Licences enabled. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/admin-org-settings ', + name: 'google_workspace.admin.chrome_licenses.allowed', + type: 'keyword', + }, + 'google_workspace.admin.oauth2.service.name': { + category: 'google_workspace', + description: + 'OAuth2 service name. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/admin-security-settings ', + name: 'google_workspace.admin.oauth2.service.name', + type: 'keyword', + }, + 'google_workspace.admin.oauth2.application.id': { + category: 'google_workspace', + description: 'OAuth2 application ID.', + name: 'google_workspace.admin.oauth2.application.id', + type: 'keyword', + }, + 'google_workspace.admin.oauth2.application.name': { + category: 'google_workspace', + description: 'OAuth2 application name.', + name: 'google_workspace.admin.oauth2.application.name', + type: 'keyword', + }, + 'google_workspace.admin.oauth2.application.type': { + category: 'google_workspace', + description: + 'OAuth2 application type. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/admin-security-settings ', + name: 'google_workspace.admin.oauth2.application.type', + type: 'keyword', + }, + 'google_workspace.admin.verification_method': { + category: 'google_workspace', + description: + 'Related verification method. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/admin-security-settings and https://developers.google.com/admin-sdk/reports/v1/appendix/activity/admin-domain-settings ', + name: 'google_workspace.admin.verification_method', + type: 'keyword', + }, + 'google_workspace.admin.alert.name': { + category: 'google_workspace', + description: 'The alert name.', + name: 'google_workspace.admin.alert.name', + type: 'keyword', + }, + 'google_workspace.admin.rule.name': { + category: 'google_workspace', + description: 'The rule name.', + name: 'google_workspace.admin.rule.name', + type: 'keyword', + }, + 'google_workspace.admin.api.client.name': { + category: 'google_workspace', + description: 'The API client name.', + name: 'google_workspace.admin.api.client.name', + type: 'keyword', + }, + 'google_workspace.admin.api.scopes': { + category: 'google_workspace', + description: 'The API scopes.', + name: 'google_workspace.admin.api.scopes', + type: 'keyword', + }, + 'google_workspace.admin.mdm.token': { + category: 'google_workspace', + description: 'The MDM vendor enrollment token.', + name: 'google_workspace.admin.mdm.token', + type: 'keyword', + }, + 'google_workspace.admin.mdm.vendor': { + category: 'google_workspace', + description: "The MDM vendor's name.", + name: 'google_workspace.admin.mdm.vendor', + type: 'keyword', + }, + 'google_workspace.admin.info_type': { + category: 'google_workspace', + description: + 'This will be used to state what kind of information was changed. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/admin-domain-settings ', + name: 'google_workspace.admin.info_type', + type: 'keyword', + }, + 'google_workspace.admin.email_monitor.dest_email': { + category: 'google_workspace', + description: 'The destination address of the email monitor.', + name: 'google_workspace.admin.email_monitor.dest_email', + type: 'keyword', + }, + 'google_workspace.admin.email_monitor.level.chat': { + category: 'google_workspace', + description: 'The chat email monitor level.', + name: 'google_workspace.admin.email_monitor.level.chat', + type: 'keyword', + }, + 'google_workspace.admin.email_monitor.level.draft': { + category: 'google_workspace', + description: 'The draft email monitor level.', + name: 'google_workspace.admin.email_monitor.level.draft', type: 'keyword', }, - 'fortinet.firewall.xauthuser': { - category: 'fortinet', - description: 'XAuth User Name ', - name: 'fortinet.firewall.xauthuser', + 'google_workspace.admin.email_monitor.level.incoming': { + category: 'google_workspace', + description: 'The incoming email monitor level.', + name: 'google_workspace.admin.email_monitor.level.incoming', type: 'keyword', }, - 'fortinet.firewall.xid': { - category: 'fortinet', - description: 'Wireless X ID ', - name: 'fortinet.firewall.xid', - type: 'integer', - }, - 'googlecloud.destination.instance.project_id': { - category: 'googlecloud', - description: 'ID of the project containing the VM. ', - name: 'googlecloud.destination.instance.project_id', + 'google_workspace.admin.email_monitor.level.outgoing': { + category: 'google_workspace', + description: 'The outgoing email monitor level.', + name: 'google_workspace.admin.email_monitor.level.outgoing', type: 'keyword', }, - 'googlecloud.destination.instance.region': { - category: 'googlecloud', - description: 'Region of the VM. ', - name: 'googlecloud.destination.instance.region', + 'google_workspace.admin.email_dump.include_deleted': { + category: 'google_workspace', + description: 'Indicates if deleted emails are included in the export.', + name: 'google_workspace.admin.email_dump.include_deleted', + type: 'boolean', + }, + 'google_workspace.admin.email_dump.package_content': { + category: 'google_workspace', + description: 'The contents of the mailbox package.', + name: 'google_workspace.admin.email_dump.package_content', type: 'keyword', }, - 'googlecloud.destination.instance.zone': { - category: 'googlecloud', - description: 'Zone of the VM. ', - name: 'googlecloud.destination.instance.zone', + 'google_workspace.admin.email_dump.query': { + category: 'google_workspace', + description: 'The search query used for the dump.', + name: 'google_workspace.admin.email_dump.query', type: 'keyword', }, - 'googlecloud.destination.vpc.project_id': { - category: 'googlecloud', - description: 'ID of the project containing the VM. ', - name: 'googlecloud.destination.vpc.project_id', + 'google_workspace.admin.request.id': { + category: 'google_workspace', + description: 'The request ID.', + name: 'google_workspace.admin.request.id', type: 'keyword', }, - 'googlecloud.destination.vpc.vpc_name': { - category: 'googlecloud', - description: 'VPC on which the VM is operating. ', - name: 'googlecloud.destination.vpc.vpc_name', + 'google_workspace.admin.mobile.action.id': { + category: 'google_workspace', + description: "The mobile device action's ID.", + name: 'google_workspace.admin.mobile.action.id', type: 'keyword', }, - 'googlecloud.destination.vpc.subnetwork_name': { - category: 'googlecloud', - description: 'Subnetwork on which the VM is operating. ', - name: 'googlecloud.destination.vpc.subnetwork_name', + 'google_workspace.admin.mobile.action.type': { + category: 'google_workspace', + description: + "The mobile device action's type. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/admin-mobile-settings ", + name: 'google_workspace.admin.mobile.action.type', type: 'keyword', }, - 'googlecloud.source.instance.project_id': { - category: 'googlecloud', - description: 'ID of the project containing the VM. ', - name: 'googlecloud.source.instance.project_id', + 'google_workspace.admin.mobile.certificate.name': { + category: 'google_workspace', + description: 'The mobile certificate common name.', + name: 'google_workspace.admin.mobile.certificate.name', type: 'keyword', }, - 'googlecloud.source.instance.region': { - category: 'googlecloud', - description: 'Region of the VM. ', - name: 'googlecloud.source.instance.region', + 'google_workspace.admin.mobile.company_owned_devices': { + category: 'google_workspace', + description: 'The number of devices a company owns.', + name: 'google_workspace.admin.mobile.company_owned_devices', + type: 'long', + }, + 'google_workspace.admin.distribution.entity.name': { + category: 'google_workspace', + description: + 'The distribution entity value, which can be a group name or an org-unit name. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/admin-mobile-settings ', + name: 'google_workspace.admin.distribution.entity.name', type: 'keyword', }, - 'googlecloud.source.instance.zone': { - category: 'googlecloud', - description: 'Zone of the VM. ', - name: 'googlecloud.source.instance.zone', + 'google_workspace.admin.distribution.entity.type': { + category: 'google_workspace', + description: + 'The distribution entity type, which can be a group or an org-unit. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/admin-mobile-settings ', + name: 'google_workspace.admin.distribution.entity.type', type: 'keyword', }, - 'googlecloud.source.vpc.project_id': { - category: 'googlecloud', - description: 'ID of the project containing the VM. ', - name: 'googlecloud.source.vpc.project_id', + 'google_workspace.drive.billable': { + category: 'google_workspace', + description: 'Whether this activity is billable.', + name: 'google_workspace.drive.billable', + type: 'boolean', + }, + 'google_workspace.drive.source_folder_id': { + category: 'google_workspace', + name: 'google_workspace.drive.source_folder_id', type: 'keyword', }, - 'googlecloud.source.vpc.vpc_name': { - category: 'googlecloud', - description: 'VPC on which the VM is operating. ', - name: 'googlecloud.source.vpc.vpc_name', + 'google_workspace.drive.source_folder_title': { + category: 'google_workspace', + name: 'google_workspace.drive.source_folder_title', type: 'keyword', }, - 'googlecloud.source.vpc.subnetwork_name': { - category: 'googlecloud', - description: 'Subnetwork on which the VM is operating. ', - name: 'googlecloud.source.vpc.subnetwork_name', + 'google_workspace.drive.destination_folder_id': { + category: 'google_workspace', + name: 'google_workspace.drive.destination_folder_id', type: 'keyword', }, - 'googlecloud.audit.type': { - category: 'googlecloud', - description: 'Type property. ', - name: 'googlecloud.audit.type', + 'google_workspace.drive.destination_folder_title': { + category: 'google_workspace', + name: 'google_workspace.drive.destination_folder_title', type: 'keyword', }, - 'googlecloud.audit.authentication_info.principal_email': { - category: 'googlecloud', - description: 'The email address of the authenticated user making the request. ', - name: 'googlecloud.audit.authentication_info.principal_email', + 'google_workspace.drive.file.id': { + category: 'google_workspace', + name: 'google_workspace.drive.file.id', type: 'keyword', }, - 'googlecloud.audit.authentication_info.authority_selector': { - category: 'googlecloud', + 'google_workspace.drive.file.type': { + category: 'google_workspace', description: - 'The authority selector specified by the requestor, if any. It is not guaranteed that the principal was allowed to use this authority. ', - name: 'googlecloud.audit.authentication_info.authority_selector', + 'Document Drive type. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/drive ', + name: 'google_workspace.drive.file.type', type: 'keyword', }, - 'googlecloud.audit.authorization_info.permission': { - category: 'googlecloud', - description: 'The required IAM permission. ', - name: 'googlecloud.audit.authorization_info.permission', + 'google_workspace.drive.originating_app_id': { + category: 'google_workspace', + description: 'The Google Cloud Project ID of the application that performed the action. ', + name: 'google_workspace.drive.originating_app_id', type: 'keyword', }, - 'googlecloud.audit.authorization_info.granted': { - category: 'googlecloud', - description: 'Whether or not authorization for resource and permission was granted. ', - name: 'googlecloud.audit.authorization_info.granted', + 'google_workspace.drive.file.owner.email': { + category: 'google_workspace', + name: 'google_workspace.drive.file.owner.email', + type: 'keyword', + }, + 'google_workspace.drive.file.owner.is_shared_drive': { + category: 'google_workspace', + description: 'Boolean flag denoting whether owner is a shared drive. ', + name: 'google_workspace.drive.file.owner.is_shared_drive', type: 'boolean', }, - 'googlecloud.audit.authorization_info.resource_attributes.service': { - category: 'googlecloud', - description: 'The name of the service. ', - name: 'googlecloud.audit.authorization_info.resource_attributes.service', - type: 'keyword', + 'google_workspace.drive.primary_event': { + category: 'google_workspace', + description: + 'Whether this is a primary event. A single user action in Drive may generate several events. ', + name: 'google_workspace.drive.primary_event', + type: 'boolean', }, - 'googlecloud.audit.authorization_info.resource_attributes.name': { - category: 'googlecloud', - description: 'The name of the resource. ', - name: 'googlecloud.audit.authorization_info.resource_attributes.name', + 'google_workspace.drive.shared_drive_id': { + category: 'google_workspace', + description: + 'The unique identifier of the Team Drive. Only populated for for events relating to a Team Drive or item contained inside a Team Drive. ', + name: 'google_workspace.drive.shared_drive_id', type: 'keyword', }, - 'googlecloud.audit.authorization_info.resource_attributes.type': { - category: 'googlecloud', - description: 'The type of the resource. ', - name: 'googlecloud.audit.authorization_info.resource_attributes.type', + 'google_workspace.drive.visibility': { + category: 'google_workspace', + description: + 'Visibility of target file. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/drive ', + name: 'google_workspace.drive.visibility', type: 'keyword', }, - 'googlecloud.audit.method_name': { - category: 'googlecloud', + 'google_workspace.drive.new_value': { + category: 'google_workspace', description: - "The name of the service method or operation. For API calls, this should be the name of the API method. For example, 'google.datastore.v1.Datastore.RunQuery'. ", - name: 'googlecloud.audit.method_name', + 'When a setting or property of the file changes, the new value for it will appear here. ', + name: 'google_workspace.drive.new_value', type: 'keyword', }, - 'googlecloud.audit.num_response_items': { - category: 'googlecloud', - description: 'The number of items returned from a List or Query API method, if applicable. ', - name: 'googlecloud.audit.num_response_items', - type: 'long', + 'google_workspace.drive.old_value': { + category: 'google_workspace', + description: + 'When a setting or property of the file changes, the old value for it will appear here. ', + name: 'google_workspace.drive.old_value', + type: 'keyword', }, - 'googlecloud.audit.request.proto_name': { - category: 'googlecloud', - description: 'Type property of the request. ', - name: 'googlecloud.audit.request.proto_name', + 'google_workspace.drive.sheets_import_range_recipient_doc': { + category: 'google_workspace', + description: 'Doc ID of the recipient of a sheets import range.', + name: 'google_workspace.drive.sheets_import_range_recipient_doc', type: 'keyword', }, - 'googlecloud.audit.request.filter': { - category: 'googlecloud', - description: 'Filter of the request. ', - name: 'googlecloud.audit.request.filter', + 'google_workspace.drive.old_visibility': { + category: 'google_workspace', + description: 'When visibility changes, this holds the old value. ', + name: 'google_workspace.drive.old_visibility', type: 'keyword', }, - 'googlecloud.audit.request.name': { - category: 'googlecloud', - description: 'Name of the request. ', - name: 'googlecloud.audit.request.name', + 'google_workspace.drive.visibility_change': { + category: 'google_workspace', + description: 'When visibility changes, this holds the new overall visibility of the file. ', + name: 'google_workspace.drive.visibility_change', type: 'keyword', }, - 'googlecloud.audit.request.resource_name': { - category: 'googlecloud', - description: 'Name of the request resource. ', - name: 'googlecloud.audit.request.resource_name', + 'google_workspace.drive.target_domain': { + category: 'google_workspace', + description: + 'The domain for which the acccess scope was changed. This can also be the alias all to indicate the access scope was changed for all domains that have visibility for this document. ', + name: 'google_workspace.drive.target_domain', type: 'keyword', }, - 'googlecloud.audit.request_metadata.caller_ip': { - category: 'googlecloud', - description: 'The IP address of the caller. ', - name: 'googlecloud.audit.request_metadata.caller_ip', - type: 'ip', + 'google_workspace.drive.added_role': { + category: 'google_workspace', + description: + 'Added membership role of a user/group in a Team Drive. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/drive ', + name: 'google_workspace.drive.added_role', + type: 'keyword', }, - 'googlecloud.audit.request_metadata.caller_supplied_user_agent': { - category: 'googlecloud', + 'google_workspace.drive.membership_change_type': { + category: 'google_workspace', description: - 'The user agent of the caller. This information is not authenticated and should be treated accordingly. ', - name: 'googlecloud.audit.request_metadata.caller_supplied_user_agent', + 'Type of change in Team Drive membership of a user/group. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/drive ', + name: 'google_workspace.drive.membership_change_type', type: 'keyword', }, - 'googlecloud.audit.response.proto_name': { - category: 'googlecloud', - description: 'Type property of the response. ', - name: 'googlecloud.audit.response.proto_name', + 'google_workspace.drive.shared_drive_settings_change_type': { + category: 'google_workspace', + description: + 'Type of change in Team Drive settings. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/drive ', + name: 'google_workspace.drive.shared_drive_settings_change_type', type: 'keyword', }, - 'googlecloud.audit.response.details.group': { - category: 'googlecloud', - description: 'The name of the group. ', - name: 'googlecloud.audit.response.details.group', + 'google_workspace.drive.removed_role': { + category: 'google_workspace', + description: + 'Removed membership role of a user/group in a Team Drive. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/drive ', + name: 'google_workspace.drive.removed_role', type: 'keyword', }, - 'googlecloud.audit.response.details.kind': { - category: 'googlecloud', - description: 'The kind of the response details. ', - name: 'googlecloud.audit.response.details.kind', + 'google_workspace.drive.target': { + category: 'google_workspace', + description: 'Target user or group.', + name: 'google_workspace.drive.target', type: 'keyword', }, - 'googlecloud.audit.response.details.name': { - category: 'googlecloud', - description: 'The name of the response details. ', - name: 'googlecloud.audit.response.details.name', + 'google_workspace.groups.acl_permission': { + category: 'google_workspace', + description: + 'Group permission setting updated. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/groups ', + name: 'google_workspace.groups.acl_permission', type: 'keyword', }, - 'googlecloud.audit.response.details.uid': { - category: 'googlecloud', - description: 'The uid of the response details. ', - name: 'googlecloud.audit.response.details.uid', + 'google_workspace.groups.email': { + category: 'google_workspace', + description: 'Group email. ', + name: 'google_workspace.groups.email', type: 'keyword', }, - 'googlecloud.audit.response.status': { - category: 'googlecloud', - description: 'Status of the response. ', - name: 'googlecloud.audit.response.status', + 'google_workspace.groups.member.email': { + category: 'google_workspace', + description: 'Member email. ', + name: 'google_workspace.groups.member.email', type: 'keyword', }, - 'googlecloud.audit.resource_name': { - category: 'googlecloud', + 'google_workspace.groups.member.role': { + category: 'google_workspace', description: - "The resource or collection that is the target of the operation. The name is a scheme-less URI, not including the API service name. For example, 'shelves/SHELF_ID/books'. ", - name: 'googlecloud.audit.resource_name', + 'Member role. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/groups ', + name: 'google_workspace.groups.member.role', type: 'keyword', }, - 'googlecloud.audit.resource_location.current_locations': { - category: 'googlecloud', - description: 'Current locations of the resource. ', - name: 'googlecloud.audit.resource_location.current_locations', + 'google_workspace.groups.setting': { + category: 'google_workspace', + description: + 'Group setting updated. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/groups ', + name: 'google_workspace.groups.setting', type: 'keyword', }, - 'googlecloud.audit.service_name': { - category: 'googlecloud', + 'google_workspace.groups.new_value': { + category: 'google_workspace', description: - 'The name of the API service performing the operation. For example, datastore.googleapis.com. ', - name: 'googlecloud.audit.service_name', + 'New value(s) of the group setting. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/groups ', + name: 'google_workspace.groups.new_value', type: 'keyword', }, - 'googlecloud.audit.status.code': { - category: 'googlecloud', - description: 'The status code, which should be an enum value of google.rpc.Code. ', - name: 'googlecloud.audit.status.code', - type: 'integer', - }, - 'googlecloud.audit.status.message': { - category: 'googlecloud', + 'google_workspace.groups.old_value': { + category: 'google_workspace', description: - 'A developer-facing error message, which should be in English. Any user-facing error message should be localized and sent in the google.rpc.Status.details field, or localized by the client. ', - name: 'googlecloud.audit.status.message', + 'Old value(s) of the group setting. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/groups', + name: 'google_workspace.groups.old_value', type: 'keyword', }, - 'googlecloud.firewall.rule_details.priority': { - category: 'googlecloud', - description: 'The priority for the firewall rule.', - name: 'googlecloud.firewall.rule_details.priority', - type: 'long', + 'google_workspace.groups.value': { + category: 'google_workspace', + description: + 'Value of the group setting. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/groups ', + name: 'google_workspace.groups.value', + type: 'keyword', }, - 'googlecloud.firewall.rule_details.action': { - category: 'googlecloud', - description: 'Action that the rule performs on match.', - name: 'googlecloud.firewall.rule_details.action', + 'google_workspace.groups.message.id': { + category: 'google_workspace', + description: 'SMTP message Id of an email message. Present for moderation events. ', + name: 'google_workspace.groups.message.id', type: 'keyword', }, - 'googlecloud.firewall.rule_details.direction': { - category: 'googlecloud', - description: 'Direction of traffic that matches this rule.', - name: 'googlecloud.firewall.rule_details.direction', + 'google_workspace.groups.message.moderation_action': { + category: 'google_workspace', + description: 'Message moderation action. Possible values are `approved` and `rejected`. ', + name: 'google_workspace.groups.message.moderation_action', type: 'keyword', }, - 'googlecloud.firewall.rule_details.reference': { - category: 'googlecloud', - description: 'Reference to the firewall rule.', - name: 'googlecloud.firewall.rule_details.reference', + 'google_workspace.groups.status': { + category: 'google_workspace', + description: + 'A status describing the output of an operation. Possible values are `failed` and `succeeded`. ', + name: 'google_workspace.groups.status', type: 'keyword', }, - 'googlecloud.firewall.rule_details.source_range': { - category: 'googlecloud', - description: 'List of source ranges that the firewall rule applies to.', - name: 'googlecloud.firewall.rule_details.source_range', + 'google_workspace.login.affected_email_address': { + category: 'google_workspace', + name: 'google_workspace.login.affected_email_address', type: 'keyword', }, - 'googlecloud.firewall.rule_details.destination_range': { - category: 'googlecloud', - description: 'List of destination ranges that the firewall applies to.', - name: 'googlecloud.firewall.rule_details.destination_range', + 'google_workspace.login.challenge_method': { + category: 'google_workspace', + description: + 'Login challenge method. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/login. ', + name: 'google_workspace.login.challenge_method', type: 'keyword', }, - 'googlecloud.firewall.rule_details.source_tag': { - category: 'googlecloud', - description: 'List of all the source tags that the firewall rule applies to. ', - name: 'googlecloud.firewall.rule_details.source_tag', + 'google_workspace.login.failure_type': { + category: 'google_workspace', + description: + 'Login failure type. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/login. ', + name: 'google_workspace.login.failure_type', type: 'keyword', }, - 'googlecloud.firewall.rule_details.target_tag': { - category: 'googlecloud', - description: 'List of all the target tags that the firewall rule applies to. ', - name: 'googlecloud.firewall.rule_details.target_tag', + 'google_workspace.login.type': { + category: 'google_workspace', + description: + 'Login credentials type. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/login. ', + name: 'google_workspace.login.type', type: 'keyword', }, - 'googlecloud.firewall.rule_details.ip_port_info': { - category: 'googlecloud', - description: 'List of ip protocols and applicable port ranges for rules. ', - name: 'googlecloud.firewall.rule_details.ip_port_info', - type: 'array', + 'google_workspace.login.is_second_factor': { + category: 'google_workspace', + name: 'google_workspace.login.is_second_factor', + type: 'boolean', }, - 'googlecloud.firewall.rule_details.source_service_account': { - category: 'googlecloud', - description: 'List of all the source service accounts that the firewall rule applies to. ', - name: 'googlecloud.firewall.rule_details.source_service_account', + 'google_workspace.login.is_suspicious': { + category: 'google_workspace', + name: 'google_workspace.login.is_suspicious', + type: 'boolean', + }, + 'google_workspace.saml.application_name': { + category: 'google_workspace', + description: 'Saml SP application name. ', + name: 'google_workspace.saml.application_name', type: 'keyword', }, - 'googlecloud.firewall.rule_details.target_service_account': { - category: 'googlecloud', - description: 'List of all the target service accounts that the firewall rule applies to. ', - name: 'googlecloud.firewall.rule_details.target_service_account', + 'google_workspace.saml.failure_type': { + category: 'google_workspace', + description: + 'Login failure type. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/saml. ', + name: 'google_workspace.saml.failure_type', type: 'keyword', }, - 'googlecloud.vpcflow.reporter': { - category: 'googlecloud', - description: "The side which reported the flow. Can be either 'SRC' or 'DEST'. ", - name: 'googlecloud.vpcflow.reporter', + 'google_workspace.saml.initiated_by': { + category: 'google_workspace', + description: 'Requester of SAML authentication. ', + name: 'google_workspace.saml.initiated_by', type: 'keyword', }, - 'googlecloud.vpcflow.rtt.ms': { - category: 'googlecloud', - description: - 'Latency as measured (for TCP flows only) during the time interval. This is the time elapsed between sending a SEQ and receiving a corresponding ACK and it contains the network RTT as well as the application related delay. ', - name: 'googlecloud.vpcflow.rtt.ms', + 'google_workspace.saml.orgunit_path': { + category: 'google_workspace', + description: 'User orgunit. ', + name: 'google_workspace.saml.orgunit_path', + type: 'keyword', + }, + 'google_workspace.saml.status_code': { + category: 'google_workspace', + description: 'SAML status code. ', + name: 'google_workspace.saml.status_code', + type: 'long', + }, + 'google_workspace.saml.second_level_status_code': { + category: 'google_workspace', + description: 'SAML second level status code. ', + name: 'google_workspace.saml.second_level_status_code', type: 'long', }, 'gsuite.actor.type': { @@ -21893,6 +24886,582 @@ export const fieldsBeat: BeatFields = { name: 'iptables.ubiquiti.rule_set', type: 'keyword', }, + 'juniper.srx.reason': { + category: 'juniper', + description: 'reason ', + name: 'juniper.srx.reason', + type: 'keyword', + }, + 'juniper.srx.connection_tag': { + category: 'juniper', + description: 'connection tag ', + name: 'juniper.srx.connection_tag', + type: 'keyword', + }, + 'juniper.srx.service_name': { + category: 'juniper', + description: 'service name ', + name: 'juniper.srx.service_name', + type: 'keyword', + }, + 'juniper.srx.nat_connection_tag': { + category: 'juniper', + description: 'nat connection tag ', + name: 'juniper.srx.nat_connection_tag', + type: 'keyword', + }, + 'juniper.srx.src_nat_rule_type': { + category: 'juniper', + description: 'src nat rule type ', + name: 'juniper.srx.src_nat_rule_type', + type: 'keyword', + }, + 'juniper.srx.src_nat_rule_name': { + category: 'juniper', + description: 'src nat rule name ', + name: 'juniper.srx.src_nat_rule_name', + type: 'keyword', + }, + 'juniper.srx.dst_nat_rule_type': { + category: 'juniper', + description: 'dst nat rule type ', + name: 'juniper.srx.dst_nat_rule_type', + type: 'keyword', + }, + 'juniper.srx.dst_nat_rule_name': { + category: 'juniper', + description: 'dst nat rule name ', + name: 'juniper.srx.dst_nat_rule_name', + type: 'keyword', + }, + 'juniper.srx.protocol_id': { + category: 'juniper', + description: 'protocol id ', + name: 'juniper.srx.protocol_id', + type: 'keyword', + }, + 'juniper.srx.policy_name': { + category: 'juniper', + description: 'policy name ', + name: 'juniper.srx.policy_name', + type: 'keyword', + }, + 'juniper.srx.session_id_32': { + category: 'juniper', + description: 'session id 32 ', + name: 'juniper.srx.session_id_32', + type: 'keyword', + }, + 'juniper.srx.session_id': { + category: 'juniper', + description: 'session id ', + name: 'juniper.srx.session_id', + type: 'keyword', + }, + 'juniper.srx.outbound_packets': { + category: 'juniper', + description: 'packets from client ', + name: 'juniper.srx.outbound_packets', + type: 'integer', + }, + 'juniper.srx.outbound_bytes': { + category: 'juniper', + description: 'bytes from client ', + name: 'juniper.srx.outbound_bytes', + type: 'integer', + }, + 'juniper.srx.inbound_packets': { + category: 'juniper', + description: 'packets from server ', + name: 'juniper.srx.inbound_packets', + type: 'integer', + }, + 'juniper.srx.inbound_bytes': { + category: 'juniper', + description: 'bytes from server ', + name: 'juniper.srx.inbound_bytes', + type: 'integer', + }, + 'juniper.srx.elapsed_time': { + category: 'juniper', + description: 'elapsed time ', + name: 'juniper.srx.elapsed_time', + type: 'date', + }, + 'juniper.srx.application': { + category: 'juniper', + description: 'application ', + name: 'juniper.srx.application', + type: 'keyword', + }, + 'juniper.srx.nested_application': { + category: 'juniper', + description: 'nested application ', + name: 'juniper.srx.nested_application', + type: 'keyword', + }, + 'juniper.srx.username': { + category: 'juniper', + description: 'username ', + name: 'juniper.srx.username', + type: 'keyword', + }, + 'juniper.srx.roles': { + category: 'juniper', + description: 'roles ', + name: 'juniper.srx.roles', + type: 'keyword', + }, + 'juniper.srx.encrypted': { + category: 'juniper', + description: 'encrypted ', + name: 'juniper.srx.encrypted', + type: 'keyword', + }, + 'juniper.srx.application_category': { + category: 'juniper', + description: 'application category ', + name: 'juniper.srx.application_category', + type: 'keyword', + }, + 'juniper.srx.application_sub_category': { + category: 'juniper', + description: 'application sub category ', + name: 'juniper.srx.application_sub_category', + type: 'keyword', + }, + 'juniper.srx.application_characteristics': { + category: 'juniper', + description: 'application characteristics ', + name: 'juniper.srx.application_characteristics', + type: 'keyword', + }, + 'juniper.srx.secure_web_proxy_session_type': { + category: 'juniper', + description: 'secure web proxy session type ', + name: 'juniper.srx.secure_web_proxy_session_type', + type: 'keyword', + }, + 'juniper.srx.peer_session_id': { + category: 'juniper', + description: 'peer session id ', + name: 'juniper.srx.peer_session_id', + type: 'keyword', + }, + 'juniper.srx.peer_source_address': { + category: 'juniper', + description: 'peer source address ', + name: 'juniper.srx.peer_source_address', + type: 'ip', + }, + 'juniper.srx.peer_source_port': { + category: 'juniper', + description: 'peer source port ', + name: 'juniper.srx.peer_source_port', + type: 'integer', + }, + 'juniper.srx.peer_destination_address': { + category: 'juniper', + description: 'peer destination address ', + name: 'juniper.srx.peer_destination_address', + type: 'ip', + }, + 'juniper.srx.peer_destination_port': { + category: 'juniper', + description: 'peer destination port ', + name: 'juniper.srx.peer_destination_port', + type: 'integer', + }, + 'juniper.srx.hostname': { + category: 'juniper', + description: 'hostname ', + name: 'juniper.srx.hostname', + type: 'keyword', + }, + 'juniper.srx.src_vrf_grp': { + category: 'juniper', + description: 'src_vrf_grp ', + name: 'juniper.srx.src_vrf_grp', + type: 'keyword', + }, + 'juniper.srx.dst_vrf_grp': { + category: 'juniper', + description: 'dst_vrf_grp ', + name: 'juniper.srx.dst_vrf_grp', + type: 'keyword', + }, + 'juniper.srx.icmp_type': { + category: 'juniper', + description: 'icmp type ', + name: 'juniper.srx.icmp_type', + type: 'integer', + }, + 'juniper.srx.process': { + category: 'juniper', + description: 'process that generated the message ', + name: 'juniper.srx.process', + type: 'keyword', + }, + 'juniper.srx.apbr_rule_type': { + category: 'juniper', + description: 'apbr rule type ', + name: 'juniper.srx.apbr_rule_type', + type: 'keyword', + }, + 'juniper.srx.dscp_value': { + category: 'juniper', + description: 'apbr rule type ', + name: 'juniper.srx.dscp_value', + type: 'integer', + }, + 'juniper.srx.logical_system_name': { + category: 'juniper', + description: 'logical system name ', + name: 'juniper.srx.logical_system_name', + type: 'keyword', + }, + 'juniper.srx.profile_name': { + category: 'juniper', + description: 'profile name ', + name: 'juniper.srx.profile_name', + type: 'keyword', + }, + 'juniper.srx.routing_instance': { + category: 'juniper', + description: 'routing instance ', + name: 'juniper.srx.routing_instance', + type: 'keyword', + }, + 'juniper.srx.rule_name': { + category: 'juniper', + description: 'rule name ', + name: 'juniper.srx.rule_name', + type: 'keyword', + }, + 'juniper.srx.uplink_tx_bytes': { + category: 'juniper', + description: 'uplink tx bytes ', + name: 'juniper.srx.uplink_tx_bytes', + type: 'integer', + }, + 'juniper.srx.uplink_rx_bytes': { + category: 'juniper', + description: 'uplink rx bytes ', + name: 'juniper.srx.uplink_rx_bytes', + type: 'integer', + }, + 'juniper.srx.obj': { + category: 'juniper', + description: 'url path ', + name: 'juniper.srx.obj', + type: 'keyword', + }, + 'juniper.srx.url': { + category: 'juniper', + description: 'url domain ', + name: 'juniper.srx.url', + type: 'keyword', + }, + 'juniper.srx.profile': { + category: 'juniper', + description: 'filter profile ', + name: 'juniper.srx.profile', + type: 'keyword', + }, + 'juniper.srx.category': { + category: 'juniper', + description: 'filter category ', + name: 'juniper.srx.category', + type: 'keyword', + }, + 'juniper.srx.filename': { + category: 'juniper', + description: 'filename ', + name: 'juniper.srx.filename', + type: 'keyword', + }, + 'juniper.srx.temporary_filename': { + category: 'juniper', + description: 'temporary_filename ', + name: 'juniper.srx.temporary_filename', + type: 'keyword', + }, + 'juniper.srx.name': { + category: 'juniper', + description: 'name ', + name: 'juniper.srx.name', + type: 'keyword', + }, + 'juniper.srx.error_message': { + category: 'juniper', + description: 'error_message ', + name: 'juniper.srx.error_message', + type: 'keyword', + }, + 'juniper.srx.error_code': { + category: 'juniper', + description: 'error_code ', + name: 'juniper.srx.error_code', + type: 'keyword', + }, + 'juniper.srx.action': { + category: 'juniper', + description: 'action ', + name: 'juniper.srx.action', + type: 'keyword', + }, + 'juniper.srx.protocol': { + category: 'juniper', + description: 'protocol ', + name: 'juniper.srx.protocol', + type: 'keyword', + }, + 'juniper.srx.protocol_name': { + category: 'juniper', + description: 'protocol name ', + name: 'juniper.srx.protocol_name', + type: 'keyword', + }, + 'juniper.srx.type': { + category: 'juniper', + description: 'type ', + name: 'juniper.srx.type', + type: 'keyword', + }, + 'juniper.srx.repeat_count': { + category: 'juniper', + description: 'repeat count ', + name: 'juniper.srx.repeat_count', + type: 'integer', + }, + 'juniper.srx.alert': { + category: 'juniper', + description: 'repeat alert ', + name: 'juniper.srx.alert', + type: 'keyword', + }, + 'juniper.srx.message_type': { + category: 'juniper', + description: 'message type ', + name: 'juniper.srx.message_type', + type: 'keyword', + }, + 'juniper.srx.threat_severity': { + category: 'juniper', + description: 'threat severity ', + name: 'juniper.srx.threat_severity', + type: 'keyword', + }, + 'juniper.srx.application_name': { + category: 'juniper', + description: 'application name ', + name: 'juniper.srx.application_name', + type: 'keyword', + }, + 'juniper.srx.attack_name': { + category: 'juniper', + description: 'attack name ', + name: 'juniper.srx.attack_name', + type: 'keyword', + }, + 'juniper.srx.index': { + category: 'juniper', + description: 'index ', + name: 'juniper.srx.index', + type: 'keyword', + }, + 'juniper.srx.message': { + category: 'juniper', + description: 'mesagge ', + name: 'juniper.srx.message', + type: 'keyword', + }, + 'juniper.srx.epoch_time': { + category: 'juniper', + description: 'epoch time ', + name: 'juniper.srx.epoch_time', + type: 'date', + }, + 'juniper.srx.packet_log_id': { + category: 'juniper', + description: 'packet log id ', + name: 'juniper.srx.packet_log_id', + type: 'integer', + }, + 'juniper.srx.export_id': { + category: 'juniper', + description: 'packet log id ', + name: 'juniper.srx.export_id', + type: 'integer', + }, + 'juniper.srx.ddos_application_name': { + category: 'juniper', + description: 'ddos application name ', + name: 'juniper.srx.ddos_application_name', + type: 'keyword', + }, + 'juniper.srx.connection_hit_rate': { + category: 'juniper', + description: 'connection hit rate ', + name: 'juniper.srx.connection_hit_rate', + type: 'integer', + }, + 'juniper.srx.time_scope': { + category: 'juniper', + description: 'time scope ', + name: 'juniper.srx.time_scope', + type: 'keyword', + }, + 'juniper.srx.context_hit_rate': { + category: 'juniper', + description: 'context hit rate ', + name: 'juniper.srx.context_hit_rate', + type: 'integer', + }, + 'juniper.srx.context_value_hit_rate': { + category: 'juniper', + description: 'context value hit rate ', + name: 'juniper.srx.context_value_hit_rate', + type: 'integer', + }, + 'juniper.srx.time_count': { + category: 'juniper', + description: 'time count ', + name: 'juniper.srx.time_count', + type: 'integer', + }, + 'juniper.srx.time_period': { + category: 'juniper', + description: 'time period ', + name: 'juniper.srx.time_period', + type: 'integer', + }, + 'juniper.srx.context_value': { + category: 'juniper', + description: 'context value ', + name: 'juniper.srx.context_value', + type: 'keyword', + }, + 'juniper.srx.context_name': { + category: 'juniper', + description: 'context name ', + name: 'juniper.srx.context_name', + type: 'keyword', + }, + 'juniper.srx.ruleebase_name': { + category: 'juniper', + description: 'ruleebase name ', + name: 'juniper.srx.ruleebase_name', + type: 'keyword', + }, + 'juniper.srx.verdict_source': { + category: 'juniper', + description: 'verdict source ', + name: 'juniper.srx.verdict_source', + type: 'keyword', + }, + 'juniper.srx.verdict_number': { + category: 'juniper', + description: 'verdict number ', + name: 'juniper.srx.verdict_number', + type: 'integer', + }, + 'juniper.srx.file_category': { + category: 'juniper', + description: 'file category ', + name: 'juniper.srx.file_category', + type: 'keyword', + }, + 'juniper.srx.sample_sha256': { + category: 'juniper', + description: 'sample sha256 ', + name: 'juniper.srx.sample_sha256', + type: 'keyword', + }, + 'juniper.srx.malware_info': { + category: 'juniper', + description: 'malware info ', + name: 'juniper.srx.malware_info', + type: 'keyword', + }, + 'juniper.srx.client_ip': { + category: 'juniper', + description: 'client ip ', + name: 'juniper.srx.client_ip', + type: 'ip', + }, + 'juniper.srx.tenant_id': { + category: 'juniper', + description: 'tenant id ', + name: 'juniper.srx.tenant_id', + type: 'keyword', + }, + 'juniper.srx.timestamp': { + category: 'juniper', + description: 'timestamp ', + name: 'juniper.srx.timestamp', + type: 'date', + }, + 'juniper.srx.th': { + category: 'juniper', + description: 'th ', + name: 'juniper.srx.th', + type: 'keyword', + }, + 'juniper.srx.status': { + category: 'juniper', + description: 'status ', + name: 'juniper.srx.status', + type: 'keyword', + }, + 'juniper.srx.state': { + category: 'juniper', + description: 'state ', + name: 'juniper.srx.state', + type: 'keyword', + }, + 'juniper.srx.file_hash_lookup': { + category: 'juniper', + description: 'file hash lookup ', + name: 'juniper.srx.file_hash_lookup', + type: 'keyword', + }, + 'juniper.srx.file_name': { + category: 'juniper', + description: 'file name ', + name: 'juniper.srx.file_name', + type: 'keyword', + }, + 'juniper.srx.action_detail': { + category: 'juniper', + description: 'action detail ', + name: 'juniper.srx.action_detail', + type: 'keyword', + }, + 'juniper.srx.sub_category': { + category: 'juniper', + description: 'sub category ', + name: 'juniper.srx.sub_category', + type: 'keyword', + }, + 'juniper.srx.feed_name': { + category: 'juniper', + description: 'feed name ', + name: 'juniper.srx.feed_name', + type: 'keyword', + }, + 'juniper.srx.occur_count': { + category: 'juniper', + description: 'occur count ', + name: 'juniper.srx.occur_count', + type: 'integer', + }, + 'juniper.srx.tag': { + category: 'juniper', + description: 'system log message tag, which uniquely identifies the message. ', + name: 'juniper.srx.tag', + type: 'keyword', + }, 'microsoft.defender_atp.lastUpdateTime': { category: 'microsoft', description: 'The date and time (in UTC) the alert was last updated. ', @@ -21998,6 +25567,267 @@ export const fieldsBeat: BeatFields = { name: 'microsoft.defender_atp.evidence.userPrincipalName', type: 'keyword', }, + 'microsoft.m365_defender.incidentId': { + category: 'microsoft', + description: 'Unique identifier to represent the incident. ', + name: 'microsoft.m365_defender.incidentId', + type: 'keyword', + }, + 'microsoft.m365_defender.redirectIncidentId': { + category: 'microsoft', + description: + 'Only populated in case an incident is being grouped together with another incident, as part of the incident processing logic. ', + name: 'microsoft.m365_defender.redirectIncidentId', + type: 'keyword', + }, + 'microsoft.m365_defender.incidentName': { + category: 'microsoft', + description: 'Name of the Incident. ', + name: 'microsoft.m365_defender.incidentName', + type: 'keyword', + }, + 'microsoft.m365_defender.determination': { + category: 'microsoft', + description: + 'Specifies the determination of the incident. The property values are: NotAvailable, Apt, Malware, SecurityPersonnel, SecurityTesting, UnwantedSoftware, Other. ', + name: 'microsoft.m365_defender.determination', + type: 'keyword', + }, + 'microsoft.m365_defender.investigationState': { + category: 'microsoft', + description: 'The current state of the Investigation. ', + name: 'microsoft.m365_defender.investigationState', + type: 'keyword', + }, + 'microsoft.m365_defender.assignedTo': { + category: 'microsoft', + description: 'Owner of the alert. ', + name: 'microsoft.m365_defender.assignedTo', + type: 'keyword', + }, + 'microsoft.m365_defender.tags': { + category: 'microsoft', + description: + 'Array of custom tags associated with an incident, for example to flag a group of incidents with a common characteristic. ', + name: 'microsoft.m365_defender.tags', + type: 'keyword', + }, + 'microsoft.m365_defender.status': { + category: 'microsoft', + description: + "Specifies the current status of the alert. Possible values are: 'Unknown', 'New', 'InProgress' and 'Resolved'. ", + name: 'microsoft.m365_defender.status', + type: 'keyword', + }, + 'microsoft.m365_defender.classification': { + category: 'microsoft', + description: + "Specification of the alert. Possible values are: 'Unknown', 'FalsePositive', 'TruePositive'. ", + name: 'microsoft.m365_defender.classification', + type: 'keyword', + }, + 'microsoft.m365_defender.alerts.incidentId': { + category: 'microsoft', + description: 'Unique identifier to represent the incident this alert is associated with. ', + name: 'microsoft.m365_defender.alerts.incidentId', + type: 'keyword', + }, + 'microsoft.m365_defender.alerts.resolvedTime': { + category: 'microsoft', + description: 'Time when alert was resolved. ', + name: 'microsoft.m365_defender.alerts.resolvedTime', + type: 'date', + }, + 'microsoft.m365_defender.alerts.status': { + category: 'microsoft', + description: 'Categorize alerts (as New, Active, or Resolved). ', + name: 'microsoft.m365_defender.alerts.status', + type: 'keyword', + }, + 'microsoft.m365_defender.alerts.severity': { + category: 'microsoft', + description: 'The severity of the related alert. ', + name: 'microsoft.m365_defender.alerts.severity', + type: 'keyword', + }, + 'microsoft.m365_defender.alerts.creationTime': { + category: 'microsoft', + description: 'Time when alert was first created. ', + name: 'microsoft.m365_defender.alerts.creationTime', + type: 'date', + }, + 'microsoft.m365_defender.alerts.lastUpdatedTime': { + category: 'microsoft', + description: 'Time when alert was last updated. ', + name: 'microsoft.m365_defender.alerts.lastUpdatedTime', + type: 'date', + }, + 'microsoft.m365_defender.alerts.investigationId': { + category: 'microsoft', + description: 'The automated investigation id triggered by this alert. ', + name: 'microsoft.m365_defender.alerts.investigationId', + type: 'keyword', + }, + 'microsoft.m365_defender.alerts.userSid': { + category: 'microsoft', + description: 'The SID of the related user ', + name: 'microsoft.m365_defender.alerts.userSid', + type: 'keyword', + }, + 'microsoft.m365_defender.alerts.detectionSource': { + category: 'microsoft', + description: 'The service that initially detected the threat. ', + name: 'microsoft.m365_defender.alerts.detectionSource', + type: 'keyword', + }, + 'microsoft.m365_defender.alerts.classification': { + category: 'microsoft', + description: + 'The specification for the incident. The property values are: Unknown, FalsePositive, TruePositive or null. ', + name: 'microsoft.m365_defender.alerts.classification', + type: 'keyword', + }, + 'microsoft.m365_defender.alerts.investigationState': { + category: 'microsoft', + description: "Information on the investigation's current status. ", + name: 'microsoft.m365_defender.alerts.investigationState', + type: 'keyword', + }, + 'microsoft.m365_defender.alerts.determination': { + category: 'microsoft', + description: + 'Specifies the determination of the incident. The property values are: NotAvailable, Apt, Malware, SecurityPersonnel, SecurityTesting, UnwantedSoftware, Other or null ', + name: 'microsoft.m365_defender.alerts.determination', + type: 'keyword', + }, + 'microsoft.m365_defender.alerts.assignedTo': { + category: 'microsoft', + description: 'Owner of the incident, or null if no owner is assigned. ', + name: 'microsoft.m365_defender.alerts.assignedTo', + type: 'keyword', + }, + 'microsoft.m365_defender.alerts.actorName': { + category: 'microsoft', + description: 'The activity group, if any, the associated with this alert. ', + name: 'microsoft.m365_defender.alerts.actorName', + type: 'keyword', + }, + 'microsoft.m365_defender.alerts.threatFamilyName': { + category: 'microsoft', + description: 'Threat family associated with this alert. ', + name: 'microsoft.m365_defender.alerts.threatFamilyName', + type: 'keyword', + }, + 'microsoft.m365_defender.alerts.mitreTechniques': { + category: 'microsoft', + description: 'The attack techniques, as aligned with the MITRE ATT&CK™ framework. ', + name: 'microsoft.m365_defender.alerts.mitreTechniques', + type: 'keyword', + }, + 'microsoft.m365_defender.alerts.entities.entityType': { + category: 'microsoft', + description: + 'Entities that have been identified to be part of, or related to, a given alert. The properties values are: User, Ip, Url, File, Process, MailBox, MailMessage, MailCluster, Registry. ', + name: 'microsoft.m365_defender.alerts.entities.entityType', + type: 'keyword', + }, + 'microsoft.m365_defender.alerts.entities.accountName': { + category: 'microsoft', + description: 'Account name of the related user. ', + name: 'microsoft.m365_defender.alerts.entities.accountName', + type: 'keyword', + }, + 'microsoft.m365_defender.alerts.entities.mailboxDisplayName': { + category: 'microsoft', + description: 'The display name of the related mailbox. ', + name: 'microsoft.m365_defender.alerts.entities.mailboxDisplayName', + type: 'keyword', + }, + 'microsoft.m365_defender.alerts.entities.mailboxAddress': { + category: 'microsoft', + description: 'The mail address of the related mailbox. ', + name: 'microsoft.m365_defender.alerts.entities.mailboxAddress', + type: 'keyword', + }, + 'microsoft.m365_defender.alerts.entities.clusterBy': { + category: 'microsoft', + description: 'A list of metadata if the entityType is MailCluster. ', + name: 'microsoft.m365_defender.alerts.entities.clusterBy', + type: 'keyword', + }, + 'microsoft.m365_defender.alerts.entities.sender': { + category: 'microsoft', + description: 'The sender for the related email message. ', + name: 'microsoft.m365_defender.alerts.entities.sender', + type: 'keyword', + }, + 'microsoft.m365_defender.alerts.entities.recipient': { + category: 'microsoft', + description: 'The recipient for the related email message. ', + name: 'microsoft.m365_defender.alerts.entities.recipient', + type: 'keyword', + }, + 'microsoft.m365_defender.alerts.entities.subject': { + category: 'microsoft', + description: 'The subject for the related email message. ', + name: 'microsoft.m365_defender.alerts.entities.subject', + type: 'keyword', + }, + 'microsoft.m365_defender.alerts.entities.deliveryAction': { + category: 'microsoft', + description: 'The delivery status for the related email message. ', + name: 'microsoft.m365_defender.alerts.entities.deliveryAction', + type: 'keyword', + }, + 'microsoft.m365_defender.alerts.entities.securityGroupId': { + category: 'microsoft', + description: 'The Security Group ID for the user related to the email message. ', + name: 'microsoft.m365_defender.alerts.entities.securityGroupId', + type: 'keyword', + }, + 'microsoft.m365_defender.alerts.entities.securityGroupName': { + category: 'microsoft', + description: 'The Security Group Name for the user related to the email message. ', + name: 'microsoft.m365_defender.alerts.entities.securityGroupName', + type: 'keyword', + }, + 'microsoft.m365_defender.alerts.entities.registryHive': { + category: 'microsoft', + description: + 'Reference to which Hive in registry the event is related to, if eventType is registry. Example: HKEY_LOCAL_MACHINE. ', + name: 'microsoft.m365_defender.alerts.entities.registryHive', + type: 'keyword', + }, + 'microsoft.m365_defender.alerts.entities.registryKey': { + category: 'microsoft', + description: 'Reference to the related registry key to the event. ', + name: 'microsoft.m365_defender.alerts.entities.registryKey', + type: 'keyword', + }, + 'microsoft.m365_defender.alerts.entities.registryValueType': { + category: 'microsoft', + description: 'Value type of the registry key/value pair related to the event. ', + name: 'microsoft.m365_defender.alerts.entities.registryValueType', + type: 'keyword', + }, + 'microsoft.m365_defender.alerts.entities.deviceId': { + category: 'microsoft', + description: 'The unique ID of the device related to the event. ', + name: 'microsoft.m365_defender.alerts.entities.deviceId', + type: 'keyword', + }, + 'microsoft.m365_defender.alerts.entities.ipAddress': { + category: 'microsoft', + description: 'The related IP address to the event. ', + name: 'microsoft.m365_defender.alerts.entities.ipAddress', + type: 'keyword', + }, + 'microsoft.m365_defender.alerts.devices': { + category: 'microsoft', + description: 'The devices related to the investigation. ', + name: 'microsoft.m365_defender.alerts.devices', + type: 'flattened', + }, 'misp.attack_pattern.id': { category: 'misp', description: 'Identifier of the threat indicator. ', @@ -22592,6 +26422,166 @@ export const fieldsBeat: BeatFields = { name: 'mssql.log.origin', type: 'keyword', }, + 'mysqlenterprise.audit.class': { + category: 'mysqlenterprise', + description: + 'A string representing the event class. The class defines the type of event, when taken together with the event item that specifies the event subclass. ', + name: 'mysqlenterprise.audit.class', + type: 'keyword', + }, + 'mysqlenterprise.audit.connection_id': { + category: 'mysqlenterprise', + description: + 'An integer representing the client connection identifier. This is the same as the value returned by the CONNECTION_ID() function within the session. ', + name: 'mysqlenterprise.audit.connection_id', + type: 'keyword', + }, + 'mysqlenterprise.audit.id': { + category: 'mysqlenterprise', + description: 'An unsigned integer representing an event ID. ', + name: 'mysqlenterprise.audit.id', + type: 'keyword', + }, + 'mysqlenterprise.audit.connection_data.connection_type': { + category: 'mysqlenterprise', + description: + 'The security state of the connection to the server. Permitted values are tcp/ip (TCP/IP connection established without encryption), ssl (TCP/IP connection established with encryption), socket (Unix socket file connection), named_pipe (Windows named pipe connection), and shared_memory (Windows shared memory connection). ', + name: 'mysqlenterprise.audit.connection_data.connection_type', + type: 'keyword', + }, + 'mysqlenterprise.audit.connection_data.status': { + category: 'mysqlenterprise', + description: + 'An integer representing the command status: 0 for success, nonzero if an error occurred. ', + name: 'mysqlenterprise.audit.connection_data.status', + type: 'long', + }, + 'mysqlenterprise.audit.connection_data.db': { + category: 'mysqlenterprise', + description: + 'A string representing a database name. For connection_data, it is the default database. For table_access_data, it is the table database. ', + name: 'mysqlenterprise.audit.connection_data.db', + type: 'keyword', + }, + 'mysqlenterprise.audit.connection_data.connection_attributes': { + category: 'mysqlenterprise', + description: 'Connection attributes that might be passed by different MySQL Clients. ', + name: 'mysqlenterprise.audit.connection_data.connection_attributes', + type: 'flattened', + }, + 'mysqlenterprise.audit.general_data.command': { + category: 'mysqlenterprise', + description: + 'A string representing the type of instruction that generated the audit event, such as a command that the server received from a client. ', + name: 'mysqlenterprise.audit.general_data.command', + type: 'keyword', + }, + 'mysqlenterprise.audit.general_data.sql_command': { + category: 'mysqlenterprise', + description: 'A string that indicates the SQL statement type. ', + name: 'mysqlenterprise.audit.general_data.sql_command', + type: 'keyword', + }, + 'mysqlenterprise.audit.general_data.query': { + category: 'mysqlenterprise', + description: + 'A string representing the text of an SQL statement. The value can be empty. Long values may be truncated. The string, like the audit log file itself, is written using UTF-8 (up to 4 bytes per character), so the value may be the result of conversion. ', + name: 'mysqlenterprise.audit.general_data.query', + type: 'keyword', + }, + 'mysqlenterprise.audit.general_data.status': { + category: 'mysqlenterprise', + description: + 'An integer representing the command status: 0 for success, nonzero if an error occurred. This is the same as the value of the mysql_errno() C API function. ', + name: 'mysqlenterprise.audit.general_data.status', + type: 'long', + }, + 'mysqlenterprise.audit.login.user': { + category: 'mysqlenterprise', + description: + 'A string representing the information indicating how a client connected to the server. ', + name: 'mysqlenterprise.audit.login.user', + type: 'keyword', + }, + 'mysqlenterprise.audit.login.proxy': { + category: 'mysqlenterprise', + description: + 'A string representing the proxy user. The value is empty if user proxying is not in effect. ', + name: 'mysqlenterprise.audit.login.proxy', + type: 'keyword', + }, + 'mysqlenterprise.audit.shutdown_data.server_id': { + category: 'mysqlenterprise', + description: + 'An integer representing the server ID. This is the same as the value of the server_id system variable. ', + name: 'mysqlenterprise.audit.shutdown_data.server_id', + type: 'keyword', + }, + 'mysqlenterprise.audit.startup_data.server_id': { + category: 'mysqlenterprise', + description: + 'An integer representing the server ID. This is the same as the value of the server_id system variable. ', + name: 'mysqlenterprise.audit.startup_data.server_id', + type: 'keyword', + }, + 'mysqlenterprise.audit.startup_data.mysql_version': { + category: 'mysqlenterprise', + description: + 'An integer representing the server ID. This is the same as the value of the server_id system variable. ', + name: 'mysqlenterprise.audit.startup_data.mysql_version', + type: 'keyword', + }, + 'mysqlenterprise.audit.table_access_data.db': { + category: 'mysqlenterprise', + description: + 'A string representing a database name. For connection_data, it is the default database. For table_access_data, it is the table database. ', + name: 'mysqlenterprise.audit.table_access_data.db', + type: 'keyword', + }, + 'mysqlenterprise.audit.table_access_data.table': { + category: 'mysqlenterprise', + description: 'A string representing a table name. ', + name: 'mysqlenterprise.audit.table_access_data.table', + type: 'keyword', + }, + 'mysqlenterprise.audit.table_access_data.query': { + category: 'mysqlenterprise', + description: + 'A string representing the text of an SQL statement. The value can be empty. Long values may be truncated. The string, like the audit log file itself, is written using UTF-8 (up to 4 bytes per character), so the value may be the result of conversion. ', + name: 'mysqlenterprise.audit.table_access_data.query', + type: 'keyword', + }, + 'mysqlenterprise.audit.table_access_data.sql_command': { + category: 'mysqlenterprise', + description: 'A string that indicates the SQL statement type. ', + name: 'mysqlenterprise.audit.table_access_data.sql_command', + type: 'keyword', + }, + 'mysqlenterprise.audit.account.user': { + category: 'mysqlenterprise', + description: + 'A string representing the user that the server authenticated the client as. This is the user name that the server uses for privilege checking. ', + name: 'mysqlenterprise.audit.account.user', + type: 'keyword', + }, + 'mysqlenterprise.audit.account.host': { + category: 'mysqlenterprise', + description: 'A string representing the client host name. ', + name: 'mysqlenterprise.audit.account.host', + type: 'keyword', + }, + 'mysqlenterprise.audit.login.os': { + category: 'mysqlenterprise', + description: + 'A string representing the external user name used during the authentication process, as set by the plugin used to authenticate the client. ', + name: 'mysqlenterprise.audit.login.os', + type: 'keyword', + }, + 'o365.audit.AADGroupId': { + category: 'o365', + name: 'o365.audit.AADGroupId', + type: 'keyword', + }, 'o365.audit.Actor.ID': { category: 'o365', name: 'o365.audit.Actor.ID', @@ -22697,6 +26687,11 @@ export const fieldsBeat: BeatFields = { name: 'o365.audit.Comments', type: 'text', }, + 'o365.audit.CommunicationType': { + category: 'o365', + name: 'o365.audit.CommunicationType', + type: 'keyword', + }, 'o365.audit.CorrelationId': { category: 'o365', name: 'o365.audit.CorrelationId', @@ -22722,11 +26717,21 @@ export const fieldsBeat: BeatFields = { name: 'o365.audit.DataType', type: 'keyword', }, + 'o365.audit.DoNotDistributeEvent': { + category: 'o365', + name: 'o365.audit.DoNotDistributeEvent', + type: 'boolean', + }, 'o365.audit.EntityType': { category: 'o365', name: 'o365.audit.EntityType', type: 'keyword', }, + 'o365.audit.ErrorNumber': { + category: 'o365', + name: 'o365.audit.ErrorNumber', + type: 'keyword', + }, 'o365.audit.EventData': { category: 'o365', name: 'o365.audit.EventData', @@ -22752,6 +26757,11 @@ export const fieldsBeat: BeatFields = { name: 'o365.audit.ExternalAccess', type: 'keyword', }, + 'o365.audit.FromApp': { + category: 'o365', + name: 'o365.audit.FromApp', + type: 'boolean', + }, 'o365.audit.GroupName': { category: 'o365', name: 'o365.audit.GroupName', @@ -22787,6 +26797,11 @@ export const fieldsBeat: BeatFields = { name: 'o365.audit.IntraSystemId', type: 'keyword', }, + 'o365.audit.IsDocLib': { + category: 'o365', + name: 'o365.audit.IsDocLib', + type: 'boolean', + }, 'o365.audit.Item.*': { category: 'o365', name: 'o365.audit.Item.*', @@ -22797,6 +26812,11 @@ export const fieldsBeat: BeatFields = { name: 'o365.audit.Item.*.*', type: 'object', }, + 'o365.audit.ItemCount': { + category: 'o365', + name: 'o365.audit.ItemCount', + type: 'long', + }, 'o365.audit.ItemName': { category: 'o365', name: 'o365.audit.ItemName', @@ -22807,11 +26827,36 @@ export const fieldsBeat: BeatFields = { name: 'o365.audit.ItemType', type: 'keyword', }, + 'o365.audit.ListBaseTemplateType': { + category: 'o365', + name: 'o365.audit.ListBaseTemplateType', + type: 'keyword', + }, + 'o365.audit.ListBaseType': { + category: 'o365', + name: 'o365.audit.ListBaseType', + type: 'keyword', + }, + 'o365.audit.ListColor': { + category: 'o365', + name: 'o365.audit.ListColor', + type: 'keyword', + }, + 'o365.audit.ListIcon': { + category: 'o365', + name: 'o365.audit.ListIcon', + type: 'keyword', + }, 'o365.audit.ListId': { category: 'o365', name: 'o365.audit.ListId', type: 'keyword', }, + 'o365.audit.ListTitle': { + category: 'o365', + name: 'o365.audit.ListTitle', + type: 'keyword', + }, 'o365.audit.ListItemUniqueId': { category: 'o365', name: 'o365.audit.ListItemUniqueId', @@ -23017,6 +27062,11 @@ export const fieldsBeat: BeatFields = { name: 'o365.audit.TeamGuid', type: 'keyword', }, + 'o365.audit.TemplateTypeId': { + category: 'o365', + name: 'o365.audit.TemplateTypeId', + type: 'keyword', + }, 'o365.audit.UniqueSharingId': { category: 'o365', name: 'o365.audit.UniqueSharingId', @@ -23366,6 +27416,89 @@ export const fieldsBeat: BeatFields = { name: 'okta.request.ip_chain.geographical_context.geolocation', type: 'geo_point', }, + 'oracle.database_audit.status': { + category: 'oracle', + description: 'Database Audit Status. ', + name: 'oracle.database_audit.status', + type: 'keyword', + }, + 'oracle.database_audit.session_id': { + category: 'oracle', + description: 'Indicates the audit session ID number. ', + name: 'oracle.database_audit.session_id', + type: 'keyword', + }, + 'oracle.database_audit.client.terminal': { + category: 'oracle', + description: 'If available, the client terminal type, for example "pty". ', + name: 'oracle.database_audit.client.terminal', + type: 'keyword', + }, + 'oracle.database_audit.client.address': { + category: 'oracle', + description: 'The IP Address or Domain used by the client. ', + name: 'oracle.database_audit.client.address', + type: 'keyword', + }, + 'oracle.database_audit.client.user': { + category: 'oracle', + description: 'The user running the client or connection to the database. ', + name: 'oracle.database_audit.client.user', + type: 'keyword', + }, + 'oracle.database_audit.database.user': { + category: 'oracle', + description: 'The database user used to authenticate. ', + name: 'oracle.database_audit.database.user', + type: 'keyword', + }, + 'oracle.database_audit.privilege': { + category: 'oracle', + description: 'The privilege group related to the database user. ', + name: 'oracle.database_audit.privilege', + type: 'keyword', + }, + 'oracle.database_audit.entry.id': { + category: 'oracle', + description: + 'Indicates the current audit entry number, assigned to each audit trail record. The audit entry.id sequence number is shared between fine-grained audit records and regular audit records. ', + name: 'oracle.database_audit.entry.id', + type: 'keyword', + }, + 'oracle.database_audit.database.host': { + category: 'oracle', + description: 'Client host machine name. ', + name: 'oracle.database_audit.database.host', + type: 'keyword', + }, + 'oracle.database_audit.action': { + category: 'oracle', + description: + 'The action performed during the audit event. This could for example be the raw query. ', + name: 'oracle.database_audit.action', + type: 'keyword', + }, + 'oracle.database_audit.action_number': { + category: 'oracle', + description: + 'Action is a numeric value representing the action the user performed. The corresponding name of the action type is in the AUDIT_ACTIONS table. For example, action 100 refers to LOGON. ', + name: 'oracle.database_audit.action_number', + type: 'keyword', + }, + 'oracle.database_audit.database.id': { + category: 'oracle', + description: + 'Database identifier calculated when the database is created. It corresponds to the DBID column of the V$DATABASE data dictionary view. ', + name: 'oracle.database_audit.database.id', + type: 'keyword', + }, + 'oracle.database_audit.length': { + category: 'oracle', + description: + 'Refers to the total number of bytes used in this audit record. This number includes the trailing newline bytes (\\n), if any, at the end of the audit record. ', + name: 'oracle.database_audit.length', + type: 'long', + }, 'panw.panos.ruleset': { category: 'panw', description: 'Name of the rule that matched this session. ', @@ -23420,6 +27553,12 @@ export const fieldsBeat: BeatFields = { name: 'panw.panos.destination.nat.port', type: 'long', }, + 'panw.panos.endreason': { + category: 'panw', + description: 'The reason a session terminated. ', + name: 'panw.panos.endreason', + type: 'keyword', + }, 'panw.panos.network.pcap_id': { category: 'panw', description: 'Packet capture ID for a threat. ', @@ -23482,6 +27621,16 @@ export const fieldsBeat: BeatFields = { name: 'panw.panos.action', type: 'keyword', }, + 'panw.panos.type': { + category: 'panw', + description: 'Specifies the type of the log', + name: 'panw.panos.type', + }, + 'panw.panos.sub_type': { + category: 'panw', + description: 'Specifies the sub type of the log', + name: 'panw.panos.sub_type', + }, 'rabbitmq.log.pid': { category: 'rabbitmq', description: 'The Erlang process id', @@ -23489,6 +27638,207 @@ export const fieldsBeat: BeatFields = { name: 'rabbitmq.log.pid', type: 'keyword', }, + 'snyk.projects': { + category: 'snyk', + description: 'Array with all related projects objects. ', + name: 'snyk.projects', + type: 'flattened', + }, + 'snyk.related.projects': { + category: 'snyk', + description: "Array of all the related project ID's. ", + name: 'snyk.related.projects', + type: 'keyword', + }, + 'snyk.audit.org_id': { + category: 'snyk', + description: 'ID of the related Organization related to the event. ', + name: 'snyk.audit.org_id', + type: 'keyword', + }, + 'snyk.audit.project_id': { + category: 'snyk', + description: 'ID of the project related to the event. ', + name: 'snyk.audit.project_id', + type: 'keyword', + }, + 'snyk.audit.content': { + category: 'snyk', + description: 'Overview of the content that was changed, both old and new values. ', + name: 'snyk.audit.content', + type: 'flattened', + }, + 'snyk.vulnerabilities.cvss3': { + category: 'snyk', + description: 'CSSv3 scores. ', + name: 'snyk.vulnerabilities.cvss3', + type: 'keyword', + }, + 'snyk.vulnerabilities.disclosure_time': { + category: 'snyk', + description: + 'The time this vulnerability was originally disclosed to the package maintainers. ', + name: 'snyk.vulnerabilities.disclosure_time', + type: 'date', + }, + 'snyk.vulnerabilities.exploit_maturity': { + category: 'snyk', + description: 'The Snyk exploit maturity level. ', + name: 'snyk.vulnerabilities.exploit_maturity', + type: 'keyword', + }, + 'snyk.vulnerabilities.id': { + category: 'snyk', + description: 'The vulnerability reference ID. ', + name: 'snyk.vulnerabilities.id', + type: 'keyword', + }, + 'snyk.vulnerabilities.is_ignored': { + category: 'snyk', + description: 'If the vulnerability report has been ignored. ', + name: 'snyk.vulnerabilities.is_ignored', + type: 'boolean', + }, + 'snyk.vulnerabilities.is_patchable': { + category: 'snyk', + description: 'If vulnerability is fixable by using a Snyk supplied patch. ', + name: 'snyk.vulnerabilities.is_patchable', + type: 'boolean', + }, + 'snyk.vulnerabilities.is_patched': { + category: 'snyk', + description: 'If the vulnerability has been patched. ', + name: 'snyk.vulnerabilities.is_patched', + type: 'boolean', + }, + 'snyk.vulnerabilities.is_pinnable': { + category: 'snyk', + description: 'If the vulnerability is fixable by pinning a transitive dependency. ', + name: 'snyk.vulnerabilities.is_pinnable', + type: 'boolean', + }, + 'snyk.vulnerabilities.is_upgradable': { + category: 'snyk', + description: 'If the vulnerability fixable by upgrading a dependency. ', + name: 'snyk.vulnerabilities.is_upgradable', + type: 'boolean', + }, + 'snyk.vulnerabilities.language': { + category: 'snyk', + description: "The package's programming language. ", + name: 'snyk.vulnerabilities.language', + type: 'keyword', + }, + 'snyk.vulnerabilities.package': { + category: 'snyk', + description: 'The package identifier according to its package manager. ', + name: 'snyk.vulnerabilities.package', + type: 'keyword', + }, + 'snyk.vulnerabilities.package_manager': { + category: 'snyk', + description: 'The package manager. ', + name: 'snyk.vulnerabilities.package_manager', + type: 'keyword', + }, + 'snyk.vulnerabilities.patches': { + category: 'snyk', + description: 'Patches required to resolve the issue created by Snyk. ', + name: 'snyk.vulnerabilities.patches', + type: 'flattened', + }, + 'snyk.vulnerabilities.priority_score': { + category: 'snyk', + description: 'The CVS priority score. ', + name: 'snyk.vulnerabilities.priority_score', + type: 'long', + }, + 'snyk.vulnerabilities.publication_time': { + category: 'snyk', + description: 'The vulnerability publication time. ', + name: 'snyk.vulnerabilities.publication_time', + type: 'date', + }, + 'snyk.vulnerabilities.jira_issue_url': { + category: 'snyk', + description: 'Link to the related Jira issue. ', + name: 'snyk.vulnerabilities.jira_issue_url', + type: 'keyword', + }, + 'snyk.vulnerabilities.original_severity': { + category: 'snyk', + description: 'The original severity of the vulnerability. ', + name: 'snyk.vulnerabilities.original_severity', + type: 'long', + }, + 'snyk.vulnerabilities.reachability': { + category: 'snyk', + description: + 'If the vulnerable function from the library is used in the code scanned. Can either be No Info, Potentially reachable and Reachable. ', + name: 'snyk.vulnerabilities.reachability', + type: 'keyword', + }, + 'snyk.vulnerabilities.title': { + category: 'snyk', + description: 'The issue title. ', + name: 'snyk.vulnerabilities.title', + type: 'keyword', + }, + 'snyk.vulnerabilities.type': { + category: 'snyk', + description: 'The issue type. Can be either "license" or "vulnerability". ', + name: 'snyk.vulnerabilities.type', + type: 'keyword', + }, + 'snyk.vulnerabilities.unique_severities_list': { + category: 'snyk', + description: 'A list of related unique severities. ', + name: 'snyk.vulnerabilities.unique_severities_list', + type: 'keyword', + }, + 'snyk.vulnerabilities.version': { + category: 'snyk', + description: 'The package version this issue is applicable to. ', + name: 'snyk.vulnerabilities.version', + type: 'keyword', + }, + 'snyk.vulnerabilities.introduced_date': { + category: 'snyk', + description: 'The date the vulnerability was initially found. ', + name: 'snyk.vulnerabilities.introduced_date', + type: 'date', + }, + 'snyk.vulnerabilities.is_fixed': { + category: 'snyk', + description: 'If the related vulnerability has been resolved. ', + name: 'snyk.vulnerabilities.is_fixed', + type: 'boolean', + }, + 'snyk.vulnerabilities.credit': { + category: 'snyk', + description: 'Reference to the person that original found the vulnerability. ', + name: 'snyk.vulnerabilities.credit', + type: 'keyword', + }, + 'snyk.vulnerabilities.semver': { + category: 'snyk', + description: + 'One or more semver ranges this issue is applicable to. The format varies according to package manager. ', + name: 'snyk.vulnerabilities.semver', + type: 'flattened', + }, + 'snyk.vulnerabilities.identifiers.alternative': { + category: 'snyk', + description: 'Additional vulnerability identifiers. ', + name: 'snyk.vulnerabilities.identifiers.alternative', + type: 'keyword', + }, + 'snyk.vulnerabilities.identifiers.cwe': { + category: 'snyk', + description: 'CWE vulnerability identifiers. ', + name: 'snyk.vulnerabilities.identifiers.cwe', + type: 'keyword', + }, 'sophos.xg.device': { category: 'sophos', description: 'device ', @@ -24845,16 +29195,17 @@ export const fieldsBeat: BeatFields = { name: 'suricata.eve.http.http_content_type', type: 'keyword', }, - 'suricata.eve.timestamp': { - category: 'suricata', - name: 'suricata.eve.timestamp', - type: 'alias', - }, 'suricata.eve.in_iface': { category: 'suricata', name: 'suricata.eve.in_iface', type: 'keyword', }, + 'suricata.eve.alert.metadata': { + category: 'suricata', + description: 'Metadata about the alert.', + name: 'suricata.eve.alert.metadata', + type: 'flattened', + }, 'suricata.eve.alert.category': { category: 'suricata', name: 'suricata.eve.alert.category', @@ -25600,11 +29951,6 @@ export const fieldsBeat: BeatFields = { name: 'suricata.eve.flow.pkts_toserver', type: 'alias', }, - 'suricata.eve.flow.end': { - category: 'suricata', - name: 'suricata.eve.flow.end', - type: 'date', - }, 'suricata.eve.flow.alerted': { category: 'suricata', name: 'suricata.eve.flow.alerted', @@ -25650,6 +29996,838 @@ export const fieldsBeat: BeatFields = { name: 'suricata.eve.flags', type: 'group', }, + 'threatintel.indicator.first_seen': { + category: 'threatintel', + description: + 'The date and time when intelligence source first reported sighting this indicator. ', + name: 'threatintel.indicator.first_seen', + type: 'keyword', + }, + 'threatintel.indicator.last_seen': { + category: 'threatintel', + description: + 'The date and time when intelligence source last reported sighting this indicator. ', + name: 'threatintel.indicator.last_seen', + type: 'date', + }, + 'threatintel.indicator.sightings': { + category: 'threatintel', + description: 'Number of times this indicator was observed conducting threat activity. ', + name: 'threatintel.indicator.sightings', + type: 'long', + }, + 'threatintel.indicator.type': { + category: 'threatintel', + description: + 'Type of indicator as represented by Cyber Observable in STIX 2.0. Expected values * autonomous-system * artifact * directory * domain-name * email-addr * file * ipv4-addr * ipv6-addr * mac-addr * mutex * process * software * url * user-account * windows-registry-key * x-509-certificate ', + name: 'threatintel.indicator.type', + type: 'keyword', + }, + 'threatintel.indicator.description': { + category: 'threatintel', + description: 'Describes the type of action conducted by the threat. ', + name: 'threatintel.indicator.description', + type: 'keyword', + }, + 'threatintel.indicator.scanner_stats': { + category: 'threatintel', + description: 'Count of AV/EDR vendors that successfully detected malicious file or URL. ', + name: 'threatintel.indicator.scanner_stats', + type: 'long', + }, + 'threatintel.indicator.provider': { + category: 'threatintel', + description: 'Identifies the name of the intelligence provider. ', + name: 'threatintel.indicator.provider', + type: 'keyword', + }, + 'threatintel.indicator.confidence': { + category: 'threatintel', + description: + 'Identifies the confidence rating assigned by the provider using STIX confidence scales. Expected values * Not Specified, None, Low, Medium, High * 0-10 * Admirality Scale (1-6) * DNI Scale (5-95) * WEP Scale (Impossible - Certain) ', + name: 'threatintel.indicator.confidence', + type: 'keyword', + }, + 'threatintel.indicator.module': { + category: 'threatintel', + description: 'Identifies the name of specific module this data is coming from. ', + name: 'threatintel.indicator.module', + type: 'keyword', + }, + 'threatintel.indicator.dataset': { + category: 'threatintel', + description: 'Identifies the name of specific dataset from the intelligence source. ', + name: 'threatintel.indicator.dataset', + type: 'keyword', + }, + 'threatintel.indicator.ip': { + category: 'threatintel', + description: 'Identifies a threat indicator as an IP address (irrespective of direction). ', + name: 'threatintel.indicator.ip', + type: 'ip', + }, + 'threatintel.indicator.domain': { + category: 'threatintel', + description: 'Identifies a threat indicator as a domain (irrespective of direction). ', + name: 'threatintel.indicator.domain', + type: 'keyword', + }, + 'threatintel.indicator.port': { + category: 'threatintel', + description: 'Identifies a threat indicator as a port number (irrespective of direction). ', + name: 'threatintel.indicator.port', + type: 'long', + }, + 'threatintel.indicator.email.address': { + category: 'threatintel', + description: 'Identifies a threat indicator as an email address (irrespective of direction). ', + name: 'threatintel.indicator.email.address', + type: 'keyword', + }, + 'threatintel.indicator.marking.tlp': { + category: 'threatintel', + description: + 'Traffic Light Protocol sharing markings. Expected values are: * White * Green * Amber * Red ', + name: 'threatintel.indicator.marking.tlp', + type: 'keyword', + }, + 'threatintel.indicator.matched.atomic': { + category: 'threatintel', + description: + 'Identifies the atomic indicator that matched a local environment endpoint or network event. ', + name: 'threatintel.indicator.matched.atomic', + type: 'keyword', + }, + 'threatintel.indicator.matched.field': { + category: 'threatintel', + description: + 'Identifies the field of the atomic indicator that matched a local environment endpoint or network event. ', + name: 'threatintel.indicator.matched.field', + type: 'keyword', + }, + 'threatintel.indicator.matched.type': { + category: 'threatintel', + description: + 'Identifies the type of the atomic indicator that matched a local environment endpoint or network event. ', + name: 'threatintel.indicator.matched.type', + type: 'keyword', + }, + 'threatintel.indicator.as.number': { + category: 'threatintel', + description: + 'Unique number allocated to the autonomous system. The autonomous system number (ASN) uniquely identifies each network on the Internet.', + example: 15169, + name: 'threatintel.indicator.as.number', + type: 'long', + }, + 'threatintel.indicator.as.organization.name': { + category: 'threatintel', + description: 'Organization name.', + example: 'Google LLC', + name: 'threatintel.indicator.as.organization.name', + type: 'keyword', + }, + 'threatintel.indicator.registry.data.strings': { + category: 'threatintel', + description: + 'Content when writing string types. Populated as an array when writing string data to the registry. For single string registry types (REG_SZ, REG_EXPAND_SZ), this should be an array with one string. For sequences of string with REG_MULTI_SZ, this array will be variable length. For numeric data, such as REG_DWORD and REG_QWORD, this should be populated with the decimal representation (e.g `"1"`). ', + example: '["C:\\rta\\red_ttp\\bin\\myapp.exe"]', + name: 'threatintel.indicator.registry.data.strings', + type: 'keyword', + }, + 'threatintel.indicator.registry.path': { + category: 'threatintel', + description: 'Full path, including hive, key and value', + example: + 'HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Image File Execution Options\\winword.exe\\Debugger', + name: 'threatintel.indicator.registry.path', + type: 'keyword', + }, + 'threatintel.indicator.registry.value': { + category: 'threatintel', + description: 'Name of the value written.', + example: 'Debugger', + name: 'threatintel.indicator.registry.value', + type: 'keyword', + }, + 'threatintel.indicator.registry.key': { + category: 'threatintel', + description: 'Registry key value', + name: 'threatintel.indicator.registry.key', + type: 'keyword', + }, + 'threatintel.indicator.geo.geo.city_name': { + category: 'threatintel', + description: 'City name.', + example: 'Montreal', + name: 'threatintel.indicator.geo.geo.city_name', + type: 'keyword', + }, + 'threatintel.indicator.geo.geo.country_iso_code': { + category: 'threatintel', + description: 'Country ISO code.', + example: 'CA', + name: 'threatintel.indicator.geo.geo.country_iso_code', + type: 'keyword', + }, + 'threatintel.indicator.geo.geo.country_name': { + category: 'threatintel', + description: 'Country name.', + example: 'Canada', + name: 'threatintel.indicator.geo.geo.country_name', + type: 'keyword', + }, + 'threatintel.indicator.geo.geo.location': { + category: 'threatintel', + description: 'Longitude and latitude.', + example: '{ "lon": -73.614830, "lat": 45.505918 }', + name: 'threatintel.indicator.geo.geo.location', + type: 'geo_point', + }, + 'threatintel.indicator.geo.geo.region_iso_code': { + category: 'threatintel', + description: 'Region ISO code.', + example: 'CA-QC', + name: 'threatintel.indicator.geo.geo.region_iso_code', + type: 'keyword', + }, + 'threatintel.indicator.geo.geo.region_name': { + category: 'threatintel', + description: 'Region name.', + example: 'Quebec', + name: 'threatintel.indicator.geo.geo.region_name', + type: 'keyword', + }, + 'threatintel.indicator.file.pe.imphash': { + category: 'threatintel', + description: + 'A hash of the imports in a PE file. An imphash -- or import hash -- can be used to fingerprint binaries even after recompilation or other code-level transformations have occurred, which would change more traditional hash values. Learn more at https://www.fireeye.com/blog/threat-research/2014/01/tracking-malware-import-hashing.html.', + example: '0c6803c4e922103c4dca5963aad36ddf', + name: 'threatintel.indicator.file.pe.imphash', + type: 'keyword', + }, + 'threatintel.indicator.file.hash.tlsh': { + category: 'threatintel', + description: "The file's import tlsh, if available. ", + name: 'threatintel.indicator.file.hash.tlsh', + type: 'keyword', + }, + 'threatintel.indicator.file.hash.ssdeep': { + category: 'threatintel', + description: "The file's ssdeep hash, if available. ", + name: 'threatintel.indicator.file.hash.ssdeep', + type: 'keyword', + }, + 'threatintel.indicator.file.hash.md5': { + category: 'threatintel', + description: "The file's md5 hash, if available. ", + name: 'threatintel.indicator.file.hash.md5', + type: 'keyword', + }, + 'threatintel.indicator.file.hash.sha1': { + category: 'threatintel', + description: "The file's sha1 hash, if available. ", + name: 'threatintel.indicator.file.hash.sha1', + type: 'keyword', + }, + 'threatintel.indicator.file.hash.sha256': { + category: 'threatintel', + description: "The file's sha256 hash, if available. ", + name: 'threatintel.indicator.file.hash.sha256', + type: 'keyword', + }, + 'threatintel.indicator.file.hash.sha512': { + category: 'threatintel', + description: "The file's sha512 hash, if available. ", + name: 'threatintel.indicator.file.hash.sha512', + type: 'keyword', + }, + 'threatintel.indicator.file.type': { + category: 'threatintel', + description: 'The file type ', + name: 'threatintel.indicator.file.type', + type: 'keyword', + }, + 'threatintel.indicator.file.size': { + category: 'threatintel', + description: "The file's total size ", + name: 'threatintel.indicator.file.size', + type: 'long', + }, + 'threatintel.indicator.file.name': { + category: 'threatintel', + description: "The file's name ", + name: 'threatintel.indicator.file.name', + type: 'keyword', + }, + 'threatintel.indicator.url.domain': { + category: 'threatintel', + description: 'Domain of the url, such as "www.elastic.co". ', + name: 'threatintel.indicator.url.domain', + type: 'keyword', + }, + 'threatintel.indicator.url.extension': { + category: 'threatintel', + description: 'The field contains the file extension from the original request ', + name: 'threatintel.indicator.url.extension', + type: 'keyword', + }, + 'threatintel.indicator.url.fragment': { + category: 'threatintel', + description: 'Portion of the url after the `#`, such as "top". ', + name: 'threatintel.indicator.url.fragment', + type: 'keyword', + }, + 'threatintel.indicator.url.full': { + category: 'threatintel', + description: + 'If full URLs are important to your use case, they should be stored in `url.full`, whether this field is reconstructed or present in the event source. ', + name: 'threatintel.indicator.url.full', + type: 'keyword', + }, + 'threatintel.indicator.url.original': { + category: 'threatintel', + description: + 'Unmodified original url as seen in the event source. Note that in network monitoring, the observed URL may be a full URL, whereas in access logs, the URL is often just represented as a path. This field is meant to represent the URL as it was observed, complete or not. ', + name: 'threatintel.indicator.url.original', + type: 'keyword', + }, + 'threatintel.indicator.url.password': { + category: 'threatintel', + description: 'Password of the request. ', + name: 'threatintel.indicator.url.password', + type: 'keyword', + }, + 'threatintel.indicator.url.path': { + category: 'threatintel', + description: 'Path of the request, such as "/search". ', + name: 'threatintel.indicator.url.path', + type: 'keyword', + }, + 'threatintel.indicator.url.port': { + category: 'threatintel', + description: 'Port of the request, such as 443. ', + name: 'threatintel.indicator.url.port', + type: 'long', + format: 'string', + }, + 'threatintel.indicator.url.query': { + category: 'threatintel', + description: + 'The query field describes the query string of the request, such as "q=elasticsearch". The `?` is excluded from the query string. If a URL contains no `?`, there is no query field. If there is a `?` but no query, the query field exists with an empty string. The `exists` query can be used to differentiate between the two cases. ', + name: 'threatintel.indicator.url.query', + type: 'keyword', + }, + 'threatintel.indicator.url.registered_domain': { + category: 'threatintel', + description: + 'The highest registered url domain, stripped of the subdomain. For example, the registered domain for "foo.example.com" is "example.com". This value can be determined precisely with a list like the public suffix list (http://publicsuffix.org). Trying to approximate this by simply taking the last two labels will not work well for TLDs such as "co.uk". ', + name: 'threatintel.indicator.url.registered_domain', + type: 'keyword', + }, + 'threatintel.indicator.url.scheme': { + category: 'threatintel', + description: 'Scheme of the request, such as "https". ', + name: 'threatintel.indicator.url.scheme', + type: 'keyword', + }, + 'threatintel.indicator.url.subdomain': { + category: 'threatintel', + description: + 'The subdomain portion of a fully qualified domain name includes all of the names except the host name under the registered_domain. In a partially qualified domain, or if the the qualification level of the full name cannot be determined, subdomain contains all of the names below the registered domain. For example the subdomain portion of "www.east.mydomain.co.uk" is "east". If the domain has multiple levels of subdomain, such as "sub2.sub1.example.com", the subdomain field should contain "sub2.sub1", with no trailing period. ', + name: 'threatintel.indicator.url.subdomain', + type: 'keyword', + }, + 'threatintel.indicator.url.top_level_domain': { + category: 'threatintel', + description: + 'The effective top level domain (eTLD), also known as the domain suffix, is the last part of the domain name. For example, the top level domain for example.com is "com". This value can be determined precisely with a list like the public suffix list (http://publicsuffix.org). Trying to approximate this by simply taking the last label will not work well for effective TLDs such as "co.uk". ', + name: 'threatintel.indicator.url.top_level_domain', + type: 'keyword', + }, + 'threatintel.indicator.url.username': { + category: 'threatintel', + description: 'Username of the request. ', + name: 'threatintel.indicator.url.username', + type: 'keyword', + }, + 'threatintel.indicator.x509.serial_number': { + category: 'threatintel', + description: + 'Unique serial number issued by the certificate authority. For consistency, if this value is alphanumeric, it should be formatted without colons and uppercase characters.', + example: '55FBB9C7DEBF09809D12CCAA', + name: 'threatintel.indicator.x509.serial_number', + type: 'keyword', + }, + 'threatintel.indicator.x509.issuer': { + category: 'threatintel', + description: + 'Name of issuing certificate authority. Could be either Distinguished Name (DN) or Common Name (CN), depending on source.', + example: 'C=US, O=Example Inc, OU=www.example.com, CN=Example SHA2 High Assurance Server CA', + name: 'threatintel.indicator.x509.issuer', + type: 'keyword', + }, + 'threatintel.indicator.x509.subject': { + category: 'threatintel', + description: + 'Name of the certificate subject entity. Could be either Distinguished Name (DN) or Common Name (CN), depending on source.', + example: 'C=US, ST=California, L=San Francisco, O=Example, Inc., CN=shared.global.example.net', + name: 'threatintel.indicator.x509.subject', + type: 'keyword', + }, + 'threatintel.indicator.x509.alternative_names': { + category: 'threatintel', + description: + 'List of subject alternative names (SAN). Name types vary by certificate authority and certificate type but commonly contain IP addresses, DNS names (and wildcards), and email addresses.', + example: '*.elastic.co', + name: 'threatintel.indicator.x509.alternative_names', + type: 'keyword', + }, + 'threatintel.abusemalware.file_type': { + category: 'threatintel', + description: 'File type guessed by URLhaus. ', + name: 'threatintel.abusemalware.file_type', + type: 'keyword', + }, + 'threatintel.abusemalware.signature': { + category: 'threatintel', + description: 'Malware familiy. ', + name: 'threatintel.abusemalware.signature', + type: 'keyword', + }, + 'threatintel.abusemalware.urlhaus_download': { + category: 'threatintel', + description: 'Location (URL) where you can download a copy of this file. ', + name: 'threatintel.abusemalware.urlhaus_download', + type: 'keyword', + }, + 'threatintel.abusemalware.virustotal.result': { + category: 'threatintel', + description: 'AV detection ration. ', + name: 'threatintel.abusemalware.virustotal.result', + type: 'keyword', + }, + 'threatintel.abusemalware.virustotal.percent': { + category: 'threatintel', + description: 'AV detection in percent. ', + name: 'threatintel.abusemalware.virustotal.percent', + type: 'float', + }, + 'threatintel.abusemalware.virustotal.link': { + category: 'threatintel', + description: 'Link to the Virustotal report. ', + name: 'threatintel.abusemalware.virustotal.link', + type: 'keyword', + }, + 'threatintel.abuseurl.id': { + category: 'threatintel', + description: 'The ID of the url. ', + name: 'threatintel.abuseurl.id', + type: 'keyword', + }, + 'threatintel.abuseurl.urlhaus_reference': { + category: 'threatintel', + description: 'Link to URLhaus entry. ', + name: 'threatintel.abuseurl.urlhaus_reference', + type: 'keyword', + }, + 'threatintel.abuseurl.url_status': { + category: 'threatintel', + description: + 'The current status of the URL. Possible values are: online, offline and unknown. ', + name: 'threatintel.abuseurl.url_status', + type: 'keyword', + }, + 'threatintel.abuseurl.threat': { + category: 'threatintel', + description: 'The threat corresponding to this malware URL. ', + name: 'threatintel.abuseurl.threat', + type: 'keyword', + }, + 'threatintel.abuseurl.blacklists.surbl': { + category: 'threatintel', + description: 'SURBL blacklist status. Possible values are: listed and not_listed ', + name: 'threatintel.abuseurl.blacklists.surbl', + type: 'keyword', + }, + 'threatintel.abuseurl.blacklists.spamhaus_dbl': { + category: 'threatintel', + description: 'Spamhaus DBL blacklist status. ', + name: 'threatintel.abuseurl.blacklists.spamhaus_dbl', + type: 'keyword', + }, + 'threatintel.abuseurl.reporter': { + category: 'threatintel', + description: + 'The Twitter handle of the reporter that has reported this malware URL (or anonymous). ', + name: 'threatintel.abuseurl.reporter', + type: 'keyword', + }, + 'threatintel.abuseurl.larted': { + category: 'threatintel', + description: + 'Indicates whether the malware URL has been reported to the hosting provider (true or false) ', + name: 'threatintel.abuseurl.larted', + type: 'boolean', + }, + 'threatintel.abuseurl.tags': { + category: 'threatintel', + description: 'A list of tags associated with the queried malware URL ', + name: 'threatintel.abuseurl.tags', + type: 'keyword', + }, + 'threatintel.anomali.id': { + category: 'threatintel', + description: 'The ID of the indicator. ', + name: 'threatintel.anomali.id', + type: 'keyword', + }, + 'threatintel.anomali.name': { + category: 'threatintel', + description: 'The name of the indicator. ', + name: 'threatintel.anomali.name', + type: 'keyword', + }, + 'threatintel.anomali.pattern': { + category: 'threatintel', + description: 'The pattern ID of the indicator. ', + name: 'threatintel.anomali.pattern', + type: 'keyword', + }, + 'threatintel.anomali.valid_from': { + category: 'threatintel', + description: 'When the indicator was first found or is considered valid. ', + name: 'threatintel.anomali.valid_from', + type: 'date', + }, + 'threatintel.anomali.modified': { + category: 'threatintel', + description: 'When the indicator was last modified ', + name: 'threatintel.anomali.modified', + type: 'date', + }, + 'threatintel.anomali.labels': { + category: 'threatintel', + description: 'The labels related to the indicator ', + name: 'threatintel.anomali.labels', + type: 'keyword', + }, + 'threatintel.anomali.indicator': { + category: 'threatintel', + description: + 'The value of the indicator, for example if the type is domain, this would be the value. ', + name: 'threatintel.anomali.indicator', + type: 'keyword', + }, + 'threatintel.anomali.description': { + category: 'threatintel', + description: 'A description of the indicator. ', + name: 'threatintel.anomali.description', + type: 'keyword', + }, + 'threatintel.anomali.title': { + category: 'threatintel', + description: 'Title describing the indicator. ', + name: 'threatintel.anomali.title', + type: 'keyword', + }, + 'threatintel.anomali.content': { + category: 'threatintel', + description: 'Extra text or descriptive content related to the indicator. ', + name: 'threatintel.anomali.content', + type: 'keyword', + }, + 'threatintel.anomali.type': { + category: 'threatintel', + description: 'The indicator type, can for example be "domain, email, FileHash-SHA256". ', + name: 'threatintel.anomali.type', + type: 'keyword', + }, + 'threatintel.anomali.object_marking_refs': { + category: 'threatintel', + description: 'The STIX reference object. ', + name: 'threatintel.anomali.object_marking_refs', + type: 'keyword', + }, + 'threatintel.misp.id': { + category: 'threatintel', + description: 'Attribute ID. ', + name: 'threatintel.misp.id', + type: 'keyword', + }, + 'threatintel.misp.orgc_id': { + category: 'threatintel', + description: 'Organization Community ID of the event. ', + name: 'threatintel.misp.orgc_id', + type: 'keyword', + }, + 'threatintel.misp.org_id': { + category: 'threatintel', + description: 'Organization ID of the event. ', + name: 'threatintel.misp.org_id', + type: 'keyword', + }, + 'threatintel.misp.threat_level_id': { + category: 'threatintel', + description: 'Threat level from 5 to 1, where 1 is the most critical. ', + name: 'threatintel.misp.threat_level_id', + type: 'long', + }, + 'threatintel.misp.info': { + category: 'threatintel', + description: 'Additional text or information related to the event. ', + name: 'threatintel.misp.info', + type: 'keyword', + }, + 'threatintel.misp.published': { + category: 'threatintel', + description: 'When the event was published. ', + name: 'threatintel.misp.published', + type: 'boolean', + }, + 'threatintel.misp.uuid': { + category: 'threatintel', + description: 'The UUID of the event object. ', + name: 'threatintel.misp.uuid', + type: 'keyword', + }, + 'threatintel.misp.date': { + category: 'threatintel', + description: 'The date of when the event object was created. ', + name: 'threatintel.misp.date', + type: 'date', + }, + 'threatintel.misp.attribute_count': { + category: 'threatintel', + description: 'How many attributes are included in a single event object. ', + name: 'threatintel.misp.attribute_count', + type: 'long', + }, + 'threatintel.misp.timestamp': { + category: 'threatintel', + description: 'The timestamp of when the event object was created. ', + name: 'threatintel.misp.timestamp', + type: 'date', + }, + 'threatintel.misp.distribution': { + category: 'threatintel', + description: 'Distribution type related to MISP. ', + name: 'threatintel.misp.distribution', + type: 'keyword', + }, + 'threatintel.misp.proposal_email_lock': { + category: 'threatintel', + description: 'Settings configured on MISP for email lock on this event object. ', + name: 'threatintel.misp.proposal_email_lock', + type: 'boolean', + }, + 'threatintel.misp.locked': { + category: 'threatintel', + description: 'If the current MISP event object is locked or not. ', + name: 'threatintel.misp.locked', + type: 'boolean', + }, + 'threatintel.misp.publish_timestamp': { + category: 'threatintel', + description: 'At what time the event object was published ', + name: 'threatintel.misp.publish_timestamp', + type: 'date', + }, + 'threatintel.misp.sharing_group_id': { + category: 'threatintel', + description: 'The ID of the grouped events or sources of the event. ', + name: 'threatintel.misp.sharing_group_id', + type: 'keyword', + }, + 'threatintel.misp.disable_correlation': { + category: 'threatintel', + description: 'If correlation is disabled on the MISP event object. ', + name: 'threatintel.misp.disable_correlation', + type: 'boolean', + }, + 'threatintel.misp.extends_uuid': { + category: 'threatintel', + description: 'The UUID of the event object it might extend. ', + name: 'threatintel.misp.extends_uuid', + type: 'keyword', + }, + 'threatintel.misp.org.id': { + category: 'threatintel', + description: 'The organization ID related to the event object. ', + name: 'threatintel.misp.org.id', + type: 'keyword', + }, + 'threatintel.misp.org.name': { + category: 'threatintel', + description: 'The organization name related to the event object. ', + name: 'threatintel.misp.org.name', + type: 'keyword', + }, + 'threatintel.misp.org.uuid': { + category: 'threatintel', + description: 'The UUID of the organization related to the event object. ', + name: 'threatintel.misp.org.uuid', + type: 'keyword', + }, + 'threatintel.misp.org.local': { + category: 'threatintel', + description: 'If the event object is local or from a remote source. ', + name: 'threatintel.misp.org.local', + type: 'boolean', + }, + 'threatintel.misp.orgc.id': { + category: 'threatintel', + description: 'The Organization Community ID in which the event object was reported from. ', + name: 'threatintel.misp.orgc.id', + type: 'keyword', + }, + 'threatintel.misp.orgc.name': { + category: 'threatintel', + description: 'The Organization Community name in which the event object was reported from. ', + name: 'threatintel.misp.orgc.name', + type: 'keyword', + }, + 'threatintel.misp.orgc.uuid': { + category: 'threatintel', + description: 'The Organization Community UUID in which the event object was reported from. ', + name: 'threatintel.misp.orgc.uuid', + type: 'keyword', + }, + 'threatintel.misp.orgc.local': { + category: 'threatintel', + description: 'If the Organization Community was local or synced from a remote source. ', + name: 'threatintel.misp.orgc.local', + type: 'boolean', + }, + 'threatintel.misp.attribute.id': { + category: 'threatintel', + description: 'The ID of the attribute related to the event object. ', + name: 'threatintel.misp.attribute.id', + type: 'keyword', + }, + 'threatintel.misp.attribute.type': { + category: 'threatintel', + description: + 'The type of the attribute related to the event object. For example email, ipv4, sha1 and such. ', + name: 'threatintel.misp.attribute.type', + type: 'keyword', + }, + 'threatintel.misp.attribute.category': { + category: 'threatintel', + description: + 'The category of the attribute related to the event object. For example "Network Activity". ', + name: 'threatintel.misp.attribute.category', + type: 'keyword', + }, + 'threatintel.misp.attribute.to_ids': { + category: 'threatintel', + description: 'If the attribute should be automatically synced with an IDS. ', + name: 'threatintel.misp.attribute.to_ids', + type: 'boolean', + }, + 'threatintel.misp.attribute.uuid': { + category: 'threatintel', + description: 'The UUID of the attribute related to the event. ', + name: 'threatintel.misp.attribute.uuid', + type: 'keyword', + }, + 'threatintel.misp.attribute.event_id': { + category: 'threatintel', + description: 'The local event ID of the attribute related to the event. ', + name: 'threatintel.misp.attribute.event_id', + type: 'keyword', + }, + 'threatintel.misp.attribute.distribution': { + category: 'threatintel', + description: 'How the attribute has been distributed, represented by integer numbers. ', + name: 'threatintel.misp.attribute.distribution', + type: 'long', + }, + 'threatintel.misp.attribute.timestamp': { + category: 'threatintel', + description: 'The timestamp in which the attribute was attached to the event object. ', + name: 'threatintel.misp.attribute.timestamp', + type: 'date', + }, + 'threatintel.misp.attribute.comment': { + category: 'threatintel', + description: 'Comments made to the attribute itself. ', + name: 'threatintel.misp.attribute.comment', + type: 'keyword', + }, + 'threatintel.misp.attribute.sharing_group_id': { + category: 'threatintel', + description: 'The group ID of the sharing group related to the specific attribute. ', + name: 'threatintel.misp.attribute.sharing_group_id', + type: 'keyword', + }, + 'threatintel.misp.attribute.deleted': { + category: 'threatintel', + description: 'If the attribute has been removed from the event object. ', + name: 'threatintel.misp.attribute.deleted', + type: 'boolean', + }, + 'threatintel.misp.attribute.disable_correlation': { + category: 'threatintel', + description: 'If correlation has been enabled on the attribute related to the event object. ', + name: 'threatintel.misp.attribute.disable_correlation', + type: 'boolean', + }, + 'threatintel.misp.attribute.object_id': { + category: 'threatintel', + description: 'The ID of the Object in which the attribute is attached. ', + name: 'threatintel.misp.attribute.object_id', + type: 'keyword', + }, + 'threatintel.misp.attribute.object_relation': { + category: 'threatintel', + description: 'The type of relation the attribute has with the event object itself. ', + name: 'threatintel.misp.attribute.object_relation', + type: 'keyword', + }, + 'threatintel.misp.attribute.value': { + category: 'threatintel', + description: 'The value of the attribute, depending on the type like "url, sha1, email-src". ', + name: 'threatintel.misp.attribute.value', + type: 'keyword', + }, + 'threatintel.otx.id': { + category: 'threatintel', + description: 'The ID of the indicator. ', + name: 'threatintel.otx.id', + type: 'keyword', + }, + 'threatintel.otx.indicator': { + category: 'threatintel', + description: + 'The value of the indicator, for example if the type is domain, this would be the value. ', + name: 'threatintel.otx.indicator', + type: 'keyword', + }, + 'threatintel.otx.description': { + category: 'threatintel', + description: 'A description of the indicator. ', + name: 'threatintel.otx.description', + type: 'keyword', + }, + 'threatintel.otx.title': { + category: 'threatintel', + description: 'Title describing the indicator. ', + name: 'threatintel.otx.title', + type: 'keyword', + }, + 'threatintel.otx.content': { + category: 'threatintel', + description: 'Extra text or descriptive content related to the indicator. ', + name: 'threatintel.otx.content', + type: 'keyword', + }, + 'threatintel.otx.type': { + category: 'threatintel', + description: 'The indicator type, can for example be "domain, email, FileHash-SHA256". ', + name: 'threatintel.otx.type', + type: 'keyword', + }, 'zeek.session_id': { category: 'zeek', description: 'A unique identifier of the session ', @@ -27358,6 +32536,42 @@ export const fieldsBeat: BeatFields = { name: 'zeek.rfb.height', type: 'integer', }, + 'zeek.signature.note': { + category: 'zeek', + description: 'Notice associated with signature event. ', + name: 'zeek.signature.note', + type: 'keyword', + }, + 'zeek.signature.sig_id': { + category: 'zeek', + description: 'The name of the signature that matched. ', + name: 'zeek.signature.sig_id', + type: 'keyword', + }, + 'zeek.signature.event_msg': { + category: 'zeek', + description: 'A more descriptive message of the signature-matching event. ', + name: 'zeek.signature.event_msg', + type: 'keyword', + }, + 'zeek.signature.sub_msg': { + category: 'zeek', + description: 'Extracted payload data or extra message. ', + name: 'zeek.signature.sub_msg', + type: 'keyword', + }, + 'zeek.signature.sig_count': { + category: 'zeek', + description: 'Number of sigs, usually from summary count. ', + name: 'zeek.signature.sig_count', + type: 'integer', + }, + 'zeek.signature.host_count': { + category: 'zeek', + description: 'Number of hosts, from a summary count. ', + name: 'zeek.signature.host_count', + type: 'integer', + }, 'zeek.sip.transaction_depth': { category: 'zeek', description: @@ -28650,22 +33864,965 @@ export const fieldsBeat: BeatFields = { name: 'zeek.x509.log_cert', type: 'boolean', }, - 'awscloudwatch.log_group': { - category: 'awscloudwatch', + 'zoom.master_account_id': { + category: 'zoom', + description: 'Master Account related to a specific Sub Account ', + name: 'zoom.master_account_id', + type: 'keyword', + }, + 'zoom.sub_account_id': { + category: 'zoom', + description: 'Related Sub Account ', + name: 'zoom.sub_account_id', + type: 'keyword', + }, + 'zoom.operator_id': { + category: 'zoom', + description: 'UserID that triggered the event ', + name: 'zoom.operator_id', + type: 'keyword', + }, + 'zoom.operator': { + category: 'zoom', + description: 'Username/Email related to the user that triggered the event ', + name: 'zoom.operator', + type: 'keyword', + }, + 'zoom.account_id': { + category: 'zoom', + description: 'Related accountID to the event ', + name: 'zoom.account_id', + type: 'keyword', + }, + 'zoom.timestamp': { + category: 'zoom', + description: 'Timestamp related to the event ', + name: 'zoom.timestamp', + type: 'date', + }, + 'zoom.creation_type': { + category: 'zoom', + description: 'Creation type ', + name: 'zoom.creation_type', + type: 'keyword', + }, + 'zoom.account.owner_id': { + category: 'zoom', + description: 'UserID of the user whose sub account was created/disassociated ', + name: 'zoom.account.owner_id', + type: 'keyword', + }, + 'zoom.account.email': { + category: 'zoom', + description: 'Email related to the user the action was performed on ', + name: 'zoom.account.email', + type: 'keyword', + }, + 'zoom.account.owner_email': { + category: 'zoom', + description: 'Email of the user whose sub account was created/disassociated ', + name: 'zoom.account.owner_email', + type: 'keyword', + }, + 'zoom.account.account_name': { + category: 'zoom', + description: 'When an account name is updated, this is the new value set ', + name: 'zoom.account.account_name', + type: 'keyword', + }, + 'zoom.account.account_alias': { + category: 'zoom', + description: 'When an account alias is updated, this is the new value set ', + name: 'zoom.account.account_alias', + type: 'keyword', + }, + 'zoom.account.account_support_name': { + category: 'zoom', + description: 'When an account support_name is updated, this is the new value set ', + name: 'zoom.account.account_support_name', + type: 'keyword', + }, + 'zoom.account.account_support_email': { + category: 'zoom', + description: 'When an account support_email is updated, this is the new value set ', + name: 'zoom.account.account_support_email', + type: 'keyword', + }, + 'zoom.chat_channel.name': { + category: 'zoom', + description: 'The name of the channel that has been added/modified/deleted ', + name: 'zoom.chat_channel.name', + type: 'keyword', + }, + 'zoom.chat_channel.id': { + category: 'zoom', + description: 'The ID of the channel that has been added/modified/deleted ', + name: 'zoom.chat_channel.id', + type: 'keyword', + }, + 'zoom.chat_channel.type': { + category: 'zoom', + description: + 'Type of channel related to the event. Can be 1(Invite-Only), 2(Private) or 3(Public) ', + name: 'zoom.chat_channel.type', + type: 'keyword', + }, + 'zoom.chat_message.id': { + category: 'zoom', + description: 'Unique ID of the related chat message ', + name: 'zoom.chat_message.id', + type: 'keyword', + }, + 'zoom.chat_message.type': { + category: 'zoom', + description: 'Type of message, can be either "to_contact" or "to_channel" ', + name: 'zoom.chat_message.type', + type: 'keyword', + }, + 'zoom.chat_message.session_id': { + category: 'zoom', + description: 'SessionID for the channel related to the message ', + name: 'zoom.chat_message.session_id', + type: 'keyword', + }, + 'zoom.chat_message.contact_email': { + category: 'zoom', + description: 'Email address related to the user sending the message ', + name: 'zoom.chat_message.contact_email', + type: 'keyword', + }, + 'zoom.chat_message.contact_id': { + category: 'zoom', + description: 'UserID belonging to the user receiving a message ', + name: 'zoom.chat_message.contact_id', + type: 'keyword', + }, + 'zoom.chat_message.channel_id': { + category: 'zoom', + description: 'ChannelID related to the message ', + name: 'zoom.chat_message.channel_id', + type: 'keyword', + }, + 'zoom.chat_message.channel_name': { + category: 'zoom', + description: 'Channel name related to the message ', + name: 'zoom.chat_message.channel_name', + type: 'keyword', + }, + 'zoom.chat_message.message': { + category: 'zoom', + description: 'A string containing the full message that was sent ', + name: 'zoom.chat_message.message', + type: 'keyword', + }, + 'zoom.meeting.id': { + category: 'zoom', + description: 'Unique ID of the related meeting ', + name: 'zoom.meeting.id', + type: 'keyword', + }, + 'zoom.meeting.uuid': { + category: 'zoom', + description: 'The UUID of the related meeting ', + name: 'zoom.meeting.uuid', + type: 'keyword', + }, + 'zoom.meeting.host_id': { + category: 'zoom', + description: 'The UserID of the configured meeting host ', + name: 'zoom.meeting.host_id', + type: 'keyword', + }, + 'zoom.meeting.topic': { + category: 'zoom', + description: 'Topic of the related meeting ', + name: 'zoom.meeting.topic', + type: 'keyword', + }, + 'zoom.meeting.type': { + category: 'zoom', + description: 'Type of meeting created ', + name: 'zoom.meeting.type', + type: 'keyword', + }, + 'zoom.meeting.start_time': { + category: 'zoom', + description: 'Date and time the meeting started ', + name: 'zoom.meeting.start_time', + type: 'date', + }, + 'zoom.meeting.timezone': { + category: 'zoom', + description: 'Which timezone is used for the meeting timestamps ', + name: 'zoom.meeting.timezone', + type: 'keyword', + }, + 'zoom.meeting.duration': { + category: 'zoom', + description: 'The duration of a meeting in minutes ', + name: 'zoom.meeting.duration', + type: 'long', + }, + 'zoom.meeting.issues': { + category: 'zoom', + description: + 'When a user reports an issue with the meeting, for example: "Unstable audio quality" ', + name: 'zoom.meeting.issues', + type: 'keyword', + }, + 'zoom.meeting.password': { + category: 'zoom', + description: 'Password related to the meeting ', + name: 'zoom.meeting.password', + type: 'keyword', + }, + 'zoom.phone.id': { + category: 'zoom', + description: 'Unique ID for the phone or conversation ', + name: 'zoom.phone.id', + type: 'keyword', + }, + 'zoom.phone.user_id': { + category: 'zoom', + description: 'UserID for the phone owner related to a Call Log being completed ', + name: 'zoom.phone.user_id', + type: 'keyword', + }, + 'zoom.phone.download_url': { + category: 'zoom', + description: 'Download URL for the voicemail ', + name: 'zoom.phone.download_url', + type: 'keyword', + }, + 'zoom.phone.ringing_start_time': { + category: 'zoom', + description: 'The timestamp when a ringtone was established to the callee ', + name: 'zoom.phone.ringing_start_time', + type: 'date', + }, + 'zoom.phone.connected_start_time': { + category: 'zoom', + description: 'The date and time when a ringtone was established to the callee ', + name: 'zoom.phone.connected_start_time', + type: 'date', + }, + 'zoom.phone.answer_start_time': { + category: 'zoom', + description: 'The date and time when the call was answered ', + name: 'zoom.phone.answer_start_time', + type: 'date', + }, + 'zoom.phone.call_end_time': { + category: 'zoom', + description: 'The date and time when the call ended ', + name: 'zoom.phone.call_end_time', + type: 'date', + }, + 'zoom.phone.call_id': { + category: 'zoom', + description: 'Unique ID of the related call ', + name: 'zoom.phone.call_id', + type: 'keyword', + }, + 'zoom.phone.duration': { + category: 'zoom', + description: 'Duration of a voicemail in minutes ', + name: 'zoom.phone.duration', + type: 'long', + }, + 'zoom.phone.caller.id': { + category: 'zoom', + description: 'UserID of the caller related to the voicemail/call ', + name: 'zoom.phone.caller.id', + type: 'keyword', + }, + 'zoom.phone.caller.user_id': { + category: 'zoom', + description: 'UserID of the person which initiated the call ', + name: 'zoom.phone.caller.user_id', + type: 'keyword', + }, + 'zoom.phone.caller.number_type': { + category: 'zoom', + description: 'The type of number, can be 1(Internal) or 2(External) ', + name: 'zoom.phone.caller.number_type', + type: 'keyword', + }, + 'zoom.phone.caller.name': { + category: 'zoom', + description: 'The name of the related callee ', + name: 'zoom.phone.caller.name', + type: 'keyword', + }, + 'zoom.phone.caller.phone_number': { + category: 'zoom', + description: 'Phone Number of the caller related to the call ', + name: 'zoom.phone.caller.phone_number', + type: 'keyword', + }, + 'zoom.phone.caller.extension_type': { + category: 'zoom', + description: + 'Extension type of the caller number, can be user, callQueue, autoReceptionist or shareLineGroup ', + name: 'zoom.phone.caller.extension_type', + type: 'keyword', + }, + 'zoom.phone.caller.extension_number': { + category: 'zoom', + description: 'Extension number of the caller ', + name: 'zoom.phone.caller.extension_number', + type: 'keyword', + }, + 'zoom.phone.caller.timezone': { + category: 'zoom', + description: 'Timezone of the caller ', + name: 'zoom.phone.caller.timezone', + type: 'keyword', + }, + 'zoom.phone.caller.device_type': { + category: 'zoom', + description: 'Device type used by the caller ', + name: 'zoom.phone.caller.device_type', + type: 'keyword', + }, + 'zoom.phone.callee.id': { + category: 'zoom', + description: 'UserID of the callee related to the voicemail/call ', + name: 'zoom.phone.callee.id', + type: 'keyword', + }, + 'zoom.phone.callee.user_id': { + category: 'zoom', + description: 'UserID of the related callee of a voicemail/call ', + name: 'zoom.phone.callee.user_id', + type: 'keyword', + }, + 'zoom.phone.callee.name': { + category: 'zoom', + description: 'The name of the related callee ', + name: 'zoom.phone.callee.name', + type: 'keyword', + }, + 'zoom.phone.callee.number_type': { + category: 'zoom', + description: 'The type of number, can be 1(Internal) or 2(External) ', + name: 'zoom.phone.callee.number_type', + type: 'keyword', + }, + 'zoom.phone.callee.phone_number': { + category: 'zoom', + description: 'Phone Number of the callee related to the call ', + name: 'zoom.phone.callee.phone_number', + type: 'keyword', + }, + 'zoom.phone.callee.extension_type': { + category: 'zoom', + description: + 'Extension type of the callee number, can be user, callQueue, autoReceptionist or shareLineGroup ', + name: 'zoom.phone.callee.extension_type', + type: 'keyword', + }, + 'zoom.phone.callee.extension_number': { + category: 'zoom', + description: 'Extension number of the callee related to the call ', + name: 'zoom.phone.callee.extension_number', + type: 'keyword', + }, + 'zoom.phone.callee.timezone': { + category: 'zoom', + description: 'Timezone of the callee related to the call ', + name: 'zoom.phone.callee.timezone', + type: 'keyword', + }, + 'zoom.phone.callee.device_type': { + category: 'zoom', + description: 'Device type used by the callee related to the call ', + name: 'zoom.phone.callee.device_type', + type: 'keyword', + }, + 'zoom.phone.date_time': { + category: 'zoom', + description: 'Date and time of the related phone event ', + name: 'zoom.phone.date_time', + type: 'date', + }, + 'zoom.recording.id': { + category: 'zoom', + description: 'Unique ID of the related recording ', + name: 'zoom.recording.id', + type: 'keyword', + }, + 'zoom.recording.uuid': { + category: 'zoom', + description: 'UUID of the related recording ', + name: 'zoom.recording.uuid', + type: 'keyword', + }, + 'zoom.recording.host_id': { + category: 'zoom', + description: 'UserID of the host of the meeting that was recorded ', + name: 'zoom.recording.host_id', + type: 'keyword', + }, + 'zoom.recording.topic': { + category: 'zoom', + description: 'Topic of the meeting related to the recording ', + name: 'zoom.recording.topic', + type: 'keyword', + }, + 'zoom.recording.type': { + category: 'zoom', + description: + 'Type of recording, can be multiple type of values, please check Zoom documentation ', + name: 'zoom.recording.type', + type: 'keyword', + }, + 'zoom.recording.start_time': { + category: 'zoom', + description: 'The date and time when the recording started ', + name: 'zoom.recording.start_time', + type: 'date', + }, + 'zoom.recording.timezone': { + category: 'zoom', + description: 'The timezone used for the recording date ', + name: 'zoom.recording.timezone', + type: 'keyword', + }, + 'zoom.recording.duration': { + category: 'zoom', + description: 'Duration of the recording in minutes ', + name: 'zoom.recording.duration', + type: 'long', + }, + 'zoom.recording.share_url': { + category: 'zoom', + description: 'The URL to access the recording ', + name: 'zoom.recording.share_url', + type: 'keyword', + }, + 'zoom.recording.total_size': { + category: 'zoom', + description: 'Total size of the recording in bytes ', + name: 'zoom.recording.total_size', + type: 'long', + }, + 'zoom.recording.recording_count': { + category: 'zoom', + description: 'Number of recording files related to the recording ', + name: 'zoom.recording.recording_count', + type: 'long', + }, + 'zoom.recording.recording_file.recording_start': { + category: 'zoom', + description: 'The date and time the recording started ', + name: 'zoom.recording.recording_file.recording_start', + type: 'date', + }, + 'zoom.recording.recording_file.recording_end': { + category: 'zoom', + description: 'The date and time the recording finished ', + name: 'zoom.recording.recording_file.recording_end', + type: 'date', + }, + 'zoom.recording.host_email': { + category: 'zoom', + description: 'Email address of the host related to the meeting that was recorded ', + name: 'zoom.recording.host_email', + type: 'keyword', + }, + 'zoom.user.id': { + category: 'zoom', + description: 'UserID related to the user event ', + name: 'zoom.user.id', + type: 'keyword', + }, + 'zoom.user.first_name': { + category: 'zoom', + description: 'User first name related to the user event ', + name: 'zoom.user.first_name', + type: 'keyword', + }, + 'zoom.user.last_name': { + category: 'zoom', + description: 'User last name related to the user event ', + name: 'zoom.user.last_name', + type: 'keyword', + }, + 'zoom.user.email': { + category: 'zoom', + description: 'User email related to the user event ', + name: 'zoom.user.email', + type: 'keyword', + }, + 'zoom.user.type': { + category: 'zoom', + description: 'User type related to the user event ', + name: 'zoom.user.type', + type: 'keyword', + }, + 'zoom.user.phone_number': { + category: 'zoom', + description: 'User phone number related to the user event ', + name: 'zoom.user.phone_number', + type: 'keyword', + }, + 'zoom.user.phone_country': { + category: 'zoom', + description: 'User country code related to the user event ', + name: 'zoom.user.phone_country', + type: 'keyword', + }, + 'zoom.user.company': { + category: 'zoom', + description: 'User company related to the user event ', + name: 'zoom.user.company', + type: 'keyword', + }, + 'zoom.user.pmi': { + category: 'zoom', + description: 'User personal meeting ID related to the user event ', + name: 'zoom.user.pmi', + type: 'keyword', + }, + 'zoom.user.use_pmi': { + category: 'zoom', + description: 'If a user has PMI enabled ', + name: 'zoom.user.use_pmi', + type: 'boolean', + }, + 'zoom.user.pic_url': { + category: 'zoom', + description: 'Full URL to the profile picture used by the user ', + name: 'zoom.user.pic_url', + type: 'keyword', + }, + 'zoom.user.vanity_name': { + category: 'zoom', + description: 'Name of the personal meeting room related to the user event ', + name: 'zoom.user.vanity_name', + type: 'keyword', + }, + 'zoom.user.timezone': { + category: 'zoom', + description: 'Timezone configured for the user ', + name: 'zoom.user.timezone', + type: 'keyword', + }, + 'zoom.user.language': { + category: 'zoom', + description: 'Language configured for the user ', + name: 'zoom.user.language', + type: 'keyword', + }, + 'zoom.user.host_key': { + category: 'zoom', + description: 'Host key set for the user ', + name: 'zoom.user.host_key', + type: 'keyword', + }, + 'zoom.user.role': { + category: 'zoom', + description: 'The configured role for the user ', + name: 'zoom.user.role', + type: 'keyword', + }, + 'zoom.user.dept': { + category: 'zoom', + description: 'The configured departement for the user ', + name: 'zoom.user.dept', + type: 'keyword', + }, + 'zoom.user.presence_status': { + category: 'zoom', + description: 'Current presence status of user ', + name: 'zoom.user.presence_status', + type: 'keyword', + }, + 'zoom.user.personal_notes': { + category: 'zoom', + description: 'Personal notes for the User ', + name: 'zoom.user.personal_notes', + type: 'keyword', + }, + 'zoom.user.client_type': { + category: 'zoom', + description: 'Type of client used by the user. Can be browser, mac, win, iphone or android ', + name: 'zoom.user.client_type', + type: 'keyword', + }, + 'zoom.user.version': { + category: 'zoom', + description: 'Version of the client used by the user ', + name: 'zoom.user.version', + type: 'keyword', + }, + 'zoom.webinar.id': { + category: 'zoom', + description: 'Unique ID for the related webinar ', + name: 'zoom.webinar.id', + type: 'keyword', + }, + 'zoom.webinar.join_url': { + category: 'zoom', + description: 'The URL configured to join the webinar ', + name: 'zoom.webinar.join_url', + type: 'keyword', + }, + 'zoom.webinar.uuid': { + category: 'zoom', + description: 'UUID for the related webinar ', + name: 'zoom.webinar.uuid', + type: 'keyword', + }, + 'zoom.webinar.host_id': { + category: 'zoom', + description: 'UserID for the configured host of the webinar ', + name: 'zoom.webinar.host_id', + type: 'keyword', + }, + 'zoom.webinar.topic': { + category: 'zoom', + description: 'Meeting topic of the related webinar ', + name: 'zoom.webinar.topic', + type: 'keyword', + }, + 'zoom.webinar.type': { + category: 'zoom', + description: + 'Type of webinar created. Can be either 5(Webinar), 6(Recurring webinar without fixed time) or 9(Recurring webinar with fixed time) ', + name: 'zoom.webinar.type', + type: 'keyword', + }, + 'zoom.webinar.start_time': { + category: 'zoom', + description: 'The date and time when the webinar started ', + name: 'zoom.webinar.start_time', + type: 'date', + }, + 'zoom.webinar.timezone': { + category: 'zoom', + description: 'Timezone used for the dates related to the webinar ', + name: 'zoom.webinar.timezone', + type: 'keyword', + }, + 'zoom.webinar.duration': { + category: 'zoom', + description: 'Duration of the webinar in minutes ', + name: 'zoom.webinar.duration', + type: 'long', + }, + 'zoom.webinar.agenda': { + category: 'zoom', + description: 'The configured agenda of the webinar ', + name: 'zoom.webinar.agenda', + type: 'keyword', + }, + 'zoom.webinar.password': { + category: 'zoom', + description: 'Password configured to access the webinar ', + name: 'zoom.webinar.password', + type: 'keyword', + }, + 'zoom.webinar.issues': { + category: 'zoom', + description: 'Any reported issues about a webinar is reported in this field ', + name: 'zoom.webinar.issues', + type: 'keyword', + }, + 'zoom.zoomroom.id': { + category: 'zoom', + description: 'Unique ID of the Zoom room ', + name: 'zoom.zoomroom.id', + type: 'keyword', + }, + 'zoom.zoomroom.room_name': { + category: 'zoom', + description: 'The configured name of the Zoom room ', + name: 'zoom.zoomroom.room_name', + type: 'keyword', + }, + 'zoom.zoomroom.calendar_name': { + category: 'zoom', + description: 'Calendar name of the Zoom room ', + name: 'zoom.zoomroom.calendar_name', + type: 'keyword', + }, + 'zoom.zoomroom.calendar_id': { + category: 'zoom', + description: 'Unique ID of the calendar used by the Zoom room ', + name: 'zoom.zoomroom.calendar_id', + type: 'keyword', + }, + 'zoom.zoomroom.event_id': { + category: 'zoom', + description: 'Unique ID of the calendar event associated with the Zoom Room ', + name: 'zoom.zoomroom.event_id', + type: 'keyword', + }, + 'zoom.zoomroom.change_key': { + category: 'zoom', + description: + 'Key used by Microsoft products integration that represents a specific version of a calendar ', + name: 'zoom.zoomroom.change_key', + type: 'keyword', + }, + 'zoom.zoomroom.resource_email': { + category: 'zoom', + description: 'Email address associated with the calendar in use by the Zoom room ', + name: 'zoom.zoomroom.resource_email', + type: 'keyword', + }, + 'zoom.zoomroom.email': { + category: 'zoom', + description: 'Email address associated with the Zoom room itself ', + name: 'zoom.zoomroom.email', + type: 'keyword', + }, + 'zoom.zoomroom.issue': { + category: 'zoom', + description: 'Any reported alerts or issues related to the Zoom room or its equipment ', + name: 'zoom.zoomroom.issue', + type: 'keyword', + }, + 'zoom.zoomroom.alert_type': { + category: 'zoom', + description: + 'An integer value representing the type of alert. The list of alert types can be found in the Zoom documentation ', + name: 'zoom.zoomroom.alert_type', + type: 'keyword', + }, + 'zoom.zoomroom.component': { + category: 'zoom', + description: + 'An integer value representing the type of equipment or component, The list of component types can be found in the Zoom documentation ', + name: 'zoom.zoomroom.component', + type: 'keyword', + }, + 'zoom.zoomroom.alert_kind': { + category: 'zoom', + description: + 'An integer value showing if the Zoom room alert has been either 1(Triggered) or 2(Cleared) ', + name: 'zoom.zoomroom.alert_kind', + type: 'keyword', + }, + 'zoom.registrant.id': { + category: 'zoom', + description: 'Unique ID of the user registering to a meeting or webinar ', + name: 'zoom.registrant.id', + type: 'keyword', + }, + 'zoom.registrant.status': { + category: 'zoom', + description: 'Status of the specific user registration ', + name: 'zoom.registrant.status', + type: 'keyword', + }, + 'zoom.registrant.email': { + category: 'zoom', + description: 'Email of the user registering to a meeting or webinar ', + name: 'zoom.registrant.email', + type: 'keyword', + }, + 'zoom.registrant.first_name': { + category: 'zoom', + description: 'First name of the user registering to a meeting or webinar ', + name: 'zoom.registrant.first_name', + type: 'keyword', + }, + 'zoom.registrant.last_name': { + category: 'zoom', + description: 'Last name of the user registering to a meeting or webinar ', + name: 'zoom.registrant.last_name', + type: 'keyword', + }, + 'zoom.registrant.address': { + category: 'zoom', + description: 'Address of the user registering to a meeting or webinar ', + name: 'zoom.registrant.address', + type: 'keyword', + }, + 'zoom.registrant.city': { + category: 'zoom', + description: 'City of the user registering to a meeting or webinar ', + name: 'zoom.registrant.city', + type: 'keyword', + }, + 'zoom.registrant.country': { + category: 'zoom', + description: 'Country of the user registering to a meeting or webinar ', + name: 'zoom.registrant.country', + type: 'keyword', + }, + 'zoom.registrant.zip': { + category: 'zoom', + description: 'Zip code of the user registering to a meeting or webinar ', + name: 'zoom.registrant.zip', + type: 'keyword', + }, + 'zoom.registrant.state': { + category: 'zoom', + description: 'State of the user registering to a meeting or webinar ', + name: 'zoom.registrant.state', + type: 'keyword', + }, + 'zoom.registrant.phone': { + category: 'zoom', + description: 'Phone number of the user registering to a meeting or webinar ', + name: 'zoom.registrant.phone', + type: 'keyword', + }, + 'zoom.registrant.industry': { + category: 'zoom', + description: 'Related industry of the user registering to a meeting or webinar ', + name: 'zoom.registrant.industry', + type: 'keyword', + }, + 'zoom.registrant.org': { + category: 'zoom', + description: 'Organization related to the user registering to a meeting or webinar ', + name: 'zoom.registrant.org', + type: 'keyword', + }, + 'zoom.registrant.job_title': { + category: 'zoom', + description: 'Job title of the user registering to a meeting or webinar ', + name: 'zoom.registrant.job_title', + type: 'keyword', + }, + 'zoom.registrant.purchasing_time_frame': { + category: 'zoom', + description: 'Choosen purchase timeframe of the user registering to a meeting or webinar ', + name: 'zoom.registrant.purchasing_time_frame', + type: 'keyword', + }, + 'zoom.registrant.role_in_purchase_process': { + category: 'zoom', + description: + 'Choosen role in a purchase process related to the user registering to a meeting or webinar ', + name: 'zoom.registrant.role_in_purchase_process', + type: 'keyword', + }, + 'zoom.registrant.no_of_employees': { + category: 'zoom', + description: 'Number of employees choosen by the user registering to a meeting or webinar ', + name: 'zoom.registrant.no_of_employees', + type: 'keyword', + }, + 'zoom.registrant.comments': { + category: 'zoom', + description: 'Comments left by the user registering to a meeting or webinar ', + name: 'zoom.registrant.comments', + type: 'keyword', + }, + 'zoom.registrant.join_url': { + category: 'zoom', + description: 'The URL that the registrant can use to join the webinar ', + name: 'zoom.registrant.join_url', + type: 'keyword', + }, + 'zoom.participant.id': { + category: 'zoom', + description: 'Unique ID of the participant related to a meeting ', + name: 'zoom.participant.id', + type: 'keyword', + }, + 'zoom.participant.user_id': { + category: 'zoom', + description: 'UserID of the participant related to a meeting ', + name: 'zoom.participant.user_id', + type: 'keyword', + }, + 'zoom.participant.user_name': { + category: 'zoom', + description: 'Username of the participant related to a meeting ', + name: 'zoom.participant.user_name', + type: 'keyword', + }, + 'zoom.participant.join_time': { + category: 'zoom', + description: 'The date and time a participant joined a meeting ', + name: 'zoom.participant.join_time', + type: 'date', + }, + 'zoom.participant.leave_time': { + category: 'zoom', + description: 'The date and time a participant left a meeting ', + name: 'zoom.participant.leave_time', + type: 'date', + }, + 'zoom.participant.sharing_details.link_source': { + category: 'zoom', + description: 'Method of sharing with dropbox integration ', + name: 'zoom.participant.sharing_details.link_source', + type: 'keyword', + }, + 'zoom.participant.sharing_details.content': { + category: 'zoom', + description: 'Type of content that was shared ', + name: 'zoom.participant.sharing_details.content', + type: 'keyword', + }, + 'zoom.participant.sharing_details.file_link': { + category: 'zoom', + description: 'The file link that was shared ', + name: 'zoom.participant.sharing_details.file_link', + type: 'keyword', + }, + 'zoom.participant.sharing_details.date_time': { + category: 'zoom', + description: 'Timestamp the sharing started ', + name: 'zoom.participant.sharing_details.date_time', + type: 'keyword', + }, + 'zoom.participant.sharing_details.source': { + category: 'zoom', + description: 'The file source that was share ', + name: 'zoom.participant.sharing_details.source', + type: 'keyword', + }, + 'zoom.old_values': { + category: 'zoom', + description: + 'Includes the old values when updating a object like user, meeting, account or webinar ', + name: 'zoom.old_values', + type: 'flattened', + }, + 'zoom.settings': { + category: 'zoom', + description: + 'The current active settings related to a object like user, meeting, account or webinar ', + name: 'zoom.settings', + type: 'flattened', + }, + 'aws-cloudwatch.log_group': { + category: 'aws-cloudwatch', description: 'The name of the log group to which this event belongs.', - name: 'awscloudwatch.log_group', + name: 'aws-cloudwatch.log_group', type: 'keyword', }, - 'awscloudwatch.log_stream': { - category: 'awscloudwatch', + 'aws-cloudwatch.log_stream': { + category: 'aws-cloudwatch', description: 'The name of the log stream to which this event belongs.', - name: 'awscloudwatch.log_stream', + name: 'aws-cloudwatch.log_stream', type: 'keyword', }, - 'awscloudwatch.ingestion_time': { - category: 'awscloudwatch', + 'aws-cloudwatch.ingestion_time': { + category: 'aws-cloudwatch', description: 'The time the event was ingested in AWS CloudWatch.', - name: 'awscloudwatch.ingestion_time', + name: 'aws-cloudwatch.ingestion_time', + type: 'keyword', + }, + bucket_name: { + category: 'base', + description: 'Name of the S3 bucket that this log retrieved from. ', + name: 'bucket_name', + type: 'keyword', + }, + object_key: { + category: 'base', + description: 'Name of the S3 object that this log retrieved from. ', + name: 'object_key', type: 'keyword', }, 'netflow.type': { @@ -30934,18 +37091,6 @@ export const fieldsBeat: BeatFields = { name: 'netflow.vpn_identifier', type: 'short', }, - bucket_name: { - category: 'base', - description: 'Name of the S3 bucket that this log retrieved from. ', - name: 'bucket_name', - type: 'keyword', - }, - object_key: { - category: 'base', - description: 'Name of the S3 object that this log retrieved from. ', - name: 'object_key', - type: 'keyword', - }, 'cef.version': { category: 'cef', description: 'Version of the CEF specification used by the message. ', @@ -33954,377 +40099,451 @@ export const fieldsBeat: BeatFields = { 'If the Redis command has resulted in an error, this field contains the error message returned by the Redis server. ', name: 'redis.error', }, - 'thrift.params': { - category: 'thrift', - description: - 'The RPC method call parameters in a human readable format. If the IDL files are available, the parameters use names whenever possible. Otherwise, the IDs from the message are used. ', - name: 'thrift.params', + 'sip.code': { + category: 'sip', + description: 'Response status code.', + name: 'sip.code', + type: 'keyword', }, - 'thrift.service': { - category: 'thrift', - description: 'The name of the Thrift-RPC service as defined in the IDL files. ', - name: 'thrift.service', + 'sip.method': { + category: 'sip', + description: 'Request method.', + name: 'sip.method', + type: 'keyword', }, - 'thrift.return_value': { - category: 'thrift', - description: - 'The value returned by the Thrift-RPC call. This is encoded in a human readable format. ', - name: 'thrift.return_value', + 'sip.status': { + category: 'sip', + description: 'Response status phrase.', + name: 'sip.status', + type: 'keyword', }, - 'thrift.exceptions': { - category: 'thrift', - description: - 'If the call resulted in exceptions, this field contains the exceptions in a human readable format. ', - name: 'thrift.exceptions', + 'sip.type': { + category: 'sip', + description: 'Either request or response.', + name: 'sip.type', + type: 'keyword', }, - 'tls.client.x509.version': { - category: 'tls', - description: 'Version of x509 format.', - example: 3, - name: 'tls.client.x509.version', + 'sip.version': { + category: 'sip', + description: 'SIP protocol version.', + name: 'sip.version', type: 'keyword', }, - 'tls.client.x509.version_number': { - category: 'tls', - description: 'Version of x509 format.', - example: 3, - name: 'tls.client.x509.version_number', + 'sip.uri.original': { + category: 'sip', + description: 'The original URI.', + name: 'sip.uri.original', type: 'keyword', }, - 'tls.client.x509.serial_number': { - category: 'tls', - description: - 'Unique serial number issued by the certificate authority. For consistency, if this value is alphanumeric, it should be formatted without colons and uppercase characters. ', - example: '55FBB9C7DEBF09809D12CCAA', - name: 'tls.client.x509.serial_number', + 'sip.uri.scheme': { + category: 'sip', + description: 'The URI scheme.', + name: 'sip.uri.scheme', type: 'keyword', }, - 'tls.client.x509.issuer.distinguished_name': { - category: 'tls', - description: 'Distinguished name (DN) of issuing certificate authority.', - example: 'C=US, O=DigiCert Inc, OU=www.digicert.com, CN=DigiCert SHA2 High Assurance Server CA', - name: 'tls.client.x509.issuer.distinguished_name', + 'sip.uri.username': { + category: 'sip', + description: 'The URI user name.', + name: 'sip.uri.username', type: 'keyword', }, - 'tls.client.x509.issuer.common_name': { - category: 'tls', - description: 'List of common name (CN) of issuing certificate authority.', - example: 'DigiCert SHA2 High Assurance Server CA', - name: 'tls.client.x509.issuer.common_name', + 'sip.uri.host': { + category: 'sip', + description: 'The URI host.', + name: 'sip.uri.host', type: 'keyword', }, - 'tls.client.x509.issuer.organizational_unit': { - category: 'tls', - description: 'List of organizational units (OU) of issuing certificate authority.', - example: 'www.digicert.com', - name: 'tls.client.x509.issuer.organizational_unit', + 'sip.uri.port': { + category: 'sip', + description: 'The URI port.', + name: 'sip.uri.port', type: 'keyword', }, - 'tls.client.x509.issuer.organization': { - category: 'tls', - description: 'List of organizations (O) of issuing certificate authority.', - example: 'DigiCert Inc', - name: 'tls.client.x509.issuer.organization', + 'sip.accept': { + category: 'sip', + description: 'Accept header value.', + name: 'sip.accept', type: 'keyword', }, - 'tls.client.x509.issuer.locality': { - category: 'tls', - description: 'List of locality names (L)', - example: 'Mountain View', - name: 'tls.client.x509.issuer.locality', + 'sip.allow': { + category: 'sip', + description: 'Allowed methods.', + name: 'sip.allow', type: 'keyword', }, - 'tls.client.x509.issuer.province': { - category: 'tls', - description: 'Province or region within country.', - name: 'tls.client.x509.issuer.province', + 'sip.call_id': { + category: 'sip', + description: 'Call ID.', + name: 'sip.call_id', type: 'keyword', }, - 'tls.client.x509.issuer.state_or_province': { - category: 'tls', - description: 'List of state or province names (ST, S, or P)', - example: 'California', - name: 'tls.client.x509.issuer.state_or_province', + 'sip.content_length': { + category: 'sip', + name: 'sip.content_length', + type: 'long', + }, + 'sip.content_type': { + category: 'sip', + name: 'sip.content_type', type: 'keyword', }, - 'tls.client.x509.issuer.country': { - category: 'tls', - description: 'List of country (C) codes', - example: 'US', - name: 'tls.client.x509.issuer.country', + 'sip.max_forwards': { + category: 'sip', + name: 'sip.max_forwards', + type: 'long', + }, + 'sip.supported': { + category: 'sip', + description: 'Supported methods.', + name: 'sip.supported', type: 'keyword', }, - 'tls.client.x509.signature_algorithm': { - category: 'tls', - description: - 'Identifier for certificate signature algorithm. Recommend using names found in Go Lang Crypto library (See https://github.com/golang/go/blob/go1.14/src/crypto/x509/x509.go#L337-L353).', - example: 'SHA256-RSA', - name: 'tls.client.x509.signature_algorithm', + 'sip.user_agent.original': { + category: 'sip', + name: 'sip.user_agent.original', type: 'keyword', }, - 'tls.client.x509.not_before': { - category: 'tls', - description: 'Time at which the certificate is first considered valid.', - example: '"2019-08-16T01:40:25.000Z"', - name: 'tls.client.x509.not_before', - type: 'date', + 'sip.private.uri.original': { + category: 'sip', + description: 'Private original URI.', + name: 'sip.private.uri.original', + type: 'keyword', }, - 'tls.client.x509.not_after': { - category: 'tls', - description: 'Time at which the certificate is no longer considered valid.', - example: '"2020-07-16T03:15:39.000Z"', - name: 'tls.client.x509.not_after', - type: 'date', + 'sip.private.uri.scheme': { + category: 'sip', + description: 'Private URI scheme.', + name: 'sip.private.uri.scheme', + type: 'keyword', }, - 'tls.client.x509.subject.distinguished_name': { - category: 'tls', - description: 'Distinguished name (DN) of the certificate subject entity.', - example: 'C=US, ST=California, L=San Francisco, O=Fastly, Inc., CN=r2.shared.global.fastly.net', - name: 'tls.client.x509.subject.distinguished_name', + 'sip.private.uri.username': { + category: 'sip', + description: 'Private URI user name.', + name: 'sip.private.uri.username', type: 'keyword', }, - 'tls.client.x509.subject.common_name': { - category: 'tls', - description: 'List of common names (CN) of subject.', - example: 'r2.shared.global.fastly.net', - name: 'tls.client.x509.subject.common_name', + 'sip.private.uri.host': { + category: 'sip', + description: 'Private URI host.', + name: 'sip.private.uri.host', type: 'keyword', }, - 'tls.client.x509.subject.organizational_unit': { - category: 'tls', - description: 'List of organizational units (OU) of subject.', - name: 'tls.client.x509.subject.organizational_unit', + 'sip.private.uri.port': { + category: 'sip', + description: 'Private URI port.', + name: 'sip.private.uri.port', type: 'keyword', }, - 'tls.client.x509.subject.organization': { - category: 'tls', - description: 'List of organizations (O) of subject.', - example: 'Fastly, Inc.', - name: 'tls.client.x509.subject.organization', + 'sip.cseq.code': { + category: 'sip', + description: 'Sequence code.', + name: 'sip.cseq.code', type: 'keyword', }, - 'tls.client.x509.subject.locality': { - category: 'tls', - description: 'List of locality names (L)', - example: 'San Francisco', - name: 'tls.client.x509.subject.locality', + 'sip.cseq.method': { + category: 'sip', + description: 'Sequence method.', + name: 'sip.cseq.method', type: 'keyword', }, - 'tls.client.x509.subject.province': { - category: 'tls', - description: 'Province or region within country.', - name: 'tls.client.x509.subject.province', + 'sip.via.original': { + category: 'sip', + description: 'The original Via value.', + name: 'sip.via.original', type: 'keyword', }, - 'tls.client.x509.subject.state_or_province': { - category: 'tls', - description: 'List of state or province names (ST, S, or P)', - example: 'California', - name: 'tls.client.x509.subject.state_or_province', + 'sip.to.display_info': { + category: 'sip', + description: 'To display info', + name: 'sip.to.display_info', type: 'keyword', }, - 'tls.client.x509.subject.country': { - category: 'tls', - description: 'List of country (C) code', - example: 'US', - name: 'tls.client.x509.subject.country', + 'sip.to.uri.original': { + category: 'sip', + description: 'To original URI', + name: 'sip.to.uri.original', type: 'keyword', }, - 'tls.client.x509.public_key_algorithm': { - category: 'tls', - description: 'Algorithm used to generate the public key.', - example: 'RSA', - name: 'tls.client.x509.public_key_algorithm', + 'sip.to.uri.scheme': { + category: 'sip', + description: 'To URI scheme', + name: 'sip.to.uri.scheme', type: 'keyword', }, - 'tls.client.x509.public_key_size': { - category: 'tls', - description: 'The size of the public key space in bits.', - example: 2048, - name: 'tls.client.x509.public_key_size', - type: 'long', + 'sip.to.uri.username': { + category: 'sip', + description: 'To URI user name', + name: 'sip.to.uri.username', + type: 'keyword', }, - 'tls.client.x509.alternative_names': { - category: 'tls', - description: - 'List of subject alternative names (SAN). Name types vary by certificate authority and certificate type but commonly contain IP addresses, DNS names (and wildcards), and email addresses.', - example: '*.elastic.co', - name: 'tls.client.x509.alternative_names', + 'sip.to.uri.host': { + category: 'sip', + description: 'To URI host', + name: 'sip.to.uri.host', type: 'keyword', }, - 'tls.server.x509.version': { - category: 'tls', - description: 'Version of x509 format.', - example: 3, - name: 'tls.server.x509.version', + 'sip.to.uri.port': { + category: 'sip', + description: 'To URI port', + name: 'sip.to.uri.port', type: 'keyword', }, - 'tls.server.x509.version_number': { - category: 'tls', - description: 'Version of x509 format.', - example: 3, - name: 'tls.server.x509.version_number', + 'sip.to.tag': { + category: 'sip', + description: 'To tag', + name: 'sip.to.tag', type: 'keyword', }, - 'tls.server.x509.serial_number': { - category: 'tls', - description: - 'Unique serial number issued by the certificate authority. For consistency, if this value is alphanumeric, it should be formatted without colons and uppercase characters. ', - example: '55FBB9C7DEBF09809D12CCAA', - name: 'tls.server.x509.serial_number', + 'sip.from.display_info': { + category: 'sip', + description: 'From display info', + name: 'sip.from.display_info', type: 'keyword', }, - 'tls.server.x509.issuer.distinguished_name': { - category: 'tls', - description: 'Distinguished name (DN) of issuing certificate authority.', - example: 'C=US, O=DigiCert Inc, OU=www.digicert.com, CN=DigiCert SHA2 High Assurance Server CA', - name: 'tls.server.x509.issuer.distinguished_name', + 'sip.from.uri.original': { + category: 'sip', + description: 'From original URI', + name: 'sip.from.uri.original', type: 'keyword', }, - 'tls.server.x509.issuer.common_name': { - category: 'tls', - description: 'List of common name (CN) of issuing certificate authority.', - example: 'DigiCert SHA2 High Assurance Server CA', - name: 'tls.server.x509.issuer.common_name', + 'sip.from.uri.scheme': { + category: 'sip', + description: 'From URI scheme', + name: 'sip.from.uri.scheme', type: 'keyword', }, - 'tls.server.x509.issuer.organizational_unit': { - category: 'tls', - description: 'List of organizational units (OU) of issuing certificate authority.', - example: 'www.digicert.com', - name: 'tls.server.x509.issuer.organizational_unit', + 'sip.from.uri.username': { + category: 'sip', + description: 'From URI user name', + name: 'sip.from.uri.username', type: 'keyword', }, - 'tls.server.x509.issuer.organization': { - category: 'tls', - description: 'List of organizations (O) of issuing certificate authority.', - example: 'DigiCert Inc', - name: 'tls.server.x509.issuer.organization', + 'sip.from.uri.host': { + category: 'sip', + description: 'From URI host', + name: 'sip.from.uri.host', type: 'keyword', }, - 'tls.server.x509.issuer.locality': { - category: 'tls', - description: 'List of locality names (L)', - example: 'Mountain View', - name: 'tls.server.x509.issuer.locality', + 'sip.from.uri.port': { + category: 'sip', + description: 'From URI port', + name: 'sip.from.uri.port', type: 'keyword', }, - 'tls.server.x509.issuer.province': { - category: 'tls', - description: 'Province or region within country.', - name: 'tls.server.x509.issuer.province', + 'sip.from.tag': { + category: 'sip', + description: 'From tag', + name: 'sip.from.tag', type: 'keyword', }, - 'tls.server.x509.issuer.state_or_province': { - category: 'tls', - description: 'List of state or province names (ST, S, or P)', - example: 'California', - name: 'tls.server.x509.issuer.state_or_province', + 'sip.contact.display_info': { + category: 'sip', + description: 'Contact display info', + name: 'sip.contact.display_info', type: 'keyword', }, - 'tls.server.x509.issuer.country': { - category: 'tls', - description: 'List of country (C) codes', - example: 'US', - name: 'tls.server.x509.issuer.country', + 'sip.contact.uri.original': { + category: 'sip', + description: 'Contact original URI', + name: 'sip.contact.uri.original', type: 'keyword', }, - 'tls.server.x509.signature_algorithm': { - category: 'tls', - description: - 'Identifier for certificate signature algorithm. Recommend using names found in Go Lang Crypto library (See https://github.com/golang/go/blob/go1.14/src/crypto/x509/x509.go#L337-L353).', - example: 'SHA256-RSA', - name: 'tls.server.x509.signature_algorithm', + 'sip.contact.uri.scheme': { + category: 'sip', + description: 'Contat URI scheme', + name: 'sip.contact.uri.scheme', type: 'keyword', }, - 'tls.server.x509.not_before': { - category: 'tls', - description: 'Time at which the certificate is first considered valid.', - example: '"2019-08-16T01:40:25.000Z"', - name: 'tls.server.x509.not_before', - type: 'date', + 'sip.contact.uri.username': { + category: 'sip', + description: 'Contact URI user name', + name: 'sip.contact.uri.username', + type: 'keyword', }, - 'tls.server.x509.not_after': { - category: 'tls', - description: 'Time at which the certificate is no longer considered valid.', - example: '"2020-07-16T03:15:39.000Z"', - name: 'tls.server.x509.not_after', - type: 'date', + 'sip.contact.uri.host': { + category: 'sip', + description: 'Contact URI host', + name: 'sip.contact.uri.host', + type: 'keyword', }, - 'tls.server.x509.subject.distinguished_name': { - category: 'tls', - description: 'Distinguished name (DN) of the certificate subject entity.', - example: 'C=US, ST=California, L=San Francisco, O=Fastly, Inc., CN=r2.shared.global.fastly.net', - name: 'tls.server.x509.subject.distinguished_name', + 'sip.contact.uri.port': { + category: 'sip', + description: 'Contact URI port', + name: 'sip.contact.uri.port', type: 'keyword', }, - 'tls.server.x509.subject.common_name': { - category: 'tls', - description: 'List of common names (CN) of subject.', - example: 'r2.shared.global.fastly.net', - name: 'tls.server.x509.subject.common_name', + 'sip.contact.transport': { + category: 'sip', + description: 'Contact transport', + name: 'sip.contact.transport', type: 'keyword', }, - 'tls.server.x509.subject.organizational_unit': { - category: 'tls', - description: 'List of organizational units (OU) of subject.', - name: 'tls.server.x509.subject.organizational_unit', + 'sip.contact.line': { + category: 'sip', + description: 'Contact line', + name: 'sip.contact.line', type: 'keyword', }, - 'tls.server.x509.subject.organization': { - category: 'tls', - description: 'List of organizations (O) of subject.', - example: 'Fastly, Inc.', - name: 'tls.server.x509.subject.organization', + 'sip.contact.expires': { + category: 'sip', + description: 'Contact expires', + name: 'sip.contact.expires', type: 'keyword', }, - 'tls.server.x509.subject.locality': { - category: 'tls', - description: 'List of locality names (L)', - example: 'San Francisco', - name: 'tls.server.x509.subject.locality', + 'sip.contact.q': { + category: 'sip', + description: 'Contact Q', + name: 'sip.contact.q', type: 'keyword', }, - 'tls.server.x509.subject.province': { + 'sip.auth.scheme': { + category: 'sip', + description: 'Auth scheme', + name: 'sip.auth.scheme', + type: 'keyword', + }, + 'sip.auth.realm': { + category: 'sip', + description: 'Auth realm', + name: 'sip.auth.realm', + type: 'keyword', + }, + 'sip.auth.uri.original': { + category: 'sip', + description: 'Auth original URI', + name: 'sip.auth.uri.original', + type: 'keyword', + }, + 'sip.auth.uri.scheme': { + category: 'sip', + description: 'Auth URI scheme', + name: 'sip.auth.uri.scheme', + type: 'keyword', + }, + 'sip.auth.uri.host': { + category: 'sip', + description: 'Auth URI host', + name: 'sip.auth.uri.host', + type: 'keyword', + }, + 'sip.auth.uri.port': { + category: 'sip', + description: 'Auth URI port', + name: 'sip.auth.uri.port', + type: 'keyword', + }, + 'sip.sdp.version': { + category: 'sip', + description: 'SDP version', + name: 'sip.sdp.version', + type: 'keyword', + }, + 'sip.sdp.owner.username': { + category: 'sip', + description: 'SDP owner user name', + name: 'sip.sdp.owner.username', + type: 'keyword', + }, + 'sip.sdp.owner.session_id': { + category: 'sip', + description: 'SDP owner session ID', + name: 'sip.sdp.owner.session_id', + type: 'keyword', + }, + 'sip.sdp.owner.version': { + category: 'sip', + description: 'SDP owner version', + name: 'sip.sdp.owner.version', + type: 'keyword', + }, + 'sip.sdp.owner.ip': { + category: 'sip', + description: 'SDP owner IP', + name: 'sip.sdp.owner.ip', + type: 'ip', + }, + 'sip.sdp.session.name': { + category: 'sip', + description: 'SDP session name', + name: 'sip.sdp.session.name', + type: 'keyword', + }, + 'sip.sdp.connection.info': { + category: 'sip', + description: 'SDP connection info', + name: 'sip.sdp.connection.info', + type: 'keyword', + }, + 'sip.sdp.connection.address': { + category: 'sip', + description: 'SDP connection address', + name: 'sip.sdp.connection.address', + type: 'keyword', + }, + 'sip.sdp.body.original': { + category: 'sip', + description: 'SDP original body', + name: 'sip.sdp.body.original', + type: 'keyword', + }, + 'thrift.params': { + category: 'thrift', + description: + 'The RPC method call parameters in a human readable format. If the IDL files are available, the parameters use names whenever possible. Otherwise, the IDs from the message are used. ', + name: 'thrift.params', + }, + 'thrift.service': { + category: 'thrift', + description: 'The name of the Thrift-RPC service as defined in the IDL files. ', + name: 'thrift.service', + }, + 'thrift.return_value': { + category: 'thrift', + description: + 'The value returned by the Thrift-RPC call. This is encoded in a human readable format. ', + name: 'thrift.return_value', + }, + 'thrift.exceptions': { + category: 'thrift', + description: + 'If the call resulted in exceptions, this field contains the exceptions in a human readable format. ', + name: 'thrift.exceptions', + }, + 'tls.client.x509.version': { category: 'tls', - description: 'Province or region within country.', - name: 'tls.server.x509.subject.province', + description: 'Version of x509 format.', + example: 3, + name: 'tls.client.x509.version', type: 'keyword', }, - 'tls.server.x509.subject.state_or_province': { + 'tls.client.x509.issuer.province': { category: 'tls', - description: 'List of state or province names (ST, S, or P)', - example: 'California', - name: 'tls.server.x509.subject.state_or_province', + description: 'Province or region within country.', + name: 'tls.client.x509.issuer.province', type: 'keyword', }, - 'tls.server.x509.subject.country': { + 'tls.client.x509.subject.province': { category: 'tls', - description: 'List of country (C) code', - example: 'US', - name: 'tls.server.x509.subject.country', + description: 'Province or region within country.', + name: 'tls.client.x509.subject.province', type: 'keyword', }, - 'tls.server.x509.public_key_algorithm': { + 'tls.server.x509.version': { category: 'tls', - description: 'Algorithm used to generate the public key.', - example: 'RSA', - name: 'tls.server.x509.public_key_algorithm', + description: 'Version of x509 format.', + example: 3, + name: 'tls.server.x509.version', type: 'keyword', }, - 'tls.server.x509.public_key_size': { + 'tls.server.x509.issuer.province': { category: 'tls', - description: 'The size of the public key space in bits.', - example: 2048, - name: 'tls.server.x509.public_key_size', - type: 'long', + description: 'Province or region within country.', + name: 'tls.server.x509.issuer.province', + type: 'keyword', }, - 'tls.server.x509.alternative_names': { + 'tls.server.x509.subject.province': { category: 'tls', - description: - 'List of subject alternative names (SAN). Name types vary by certificate authority and certificate type but commonly contain IP addresses, DNS names (and wildcards), and email addresses.', - example: '*.elastic.co', - name: 'tls.server.x509.alternative_names', + description: 'Province or region within country.', + name: 'tls.server.x509.subject.province', type: 'keyword', }, 'tls.detailed.version': { @@ -35117,7 +41336,7 @@ export const fieldsBeat: BeatFields = { 'winlog.api': { category: 'winlog', description: - 'The event log API type used to read the record. The possible values are "wineventlog" for the Windows Event Log API or "eventlogging" for the Event Logging API. The Event Logging API was designed for Windows Server 2003 or Windows 2000 operating systems. In Windows Vista, the event logging infrastructure was redesigned. On Windows Vista or later operating systems, the Windows Event Log API is used. Winlogbeat automatically detects which API to use for reading event logs. ', + 'The event log API type used to read the record. The possible values are "wineventlog" for the Windows Event Log API or "wineventlog-experimental" for its experimental implementation. ', name: 'winlog.api', }, 'winlog.activity_id': { diff --git a/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/query_builder/util_components/__snapshots__/geo_index_pattern_select.test.tsx.snap b/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/query_builder/util_components/__snapshots__/geo_index_pattern_select.test.tsx.snap new file mode 100644 index 0000000000000..84ba07b3c015b --- /dev/null +++ b/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/query_builder/util_components/__snapshots__/geo_index_pattern_select.test.tsx.snap @@ -0,0 +1,63 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render with error when index pattern does not have geo_point field 1`] = ` + + + + + +`; + +exports[`should render without error after mounting 1`] = ` + + + + + +`; diff --git a/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/query_builder/util_components/geo_index_pattern_select.test.tsx b/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/query_builder/util_components/geo_index_pattern_select.test.tsx new file mode 100644 index 0000000000000..f587e8ed35e17 --- /dev/null +++ b/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/query_builder/util_components/geo_index_pattern_select.test.tsx @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { GeoIndexPatternSelect } from './geo_index_pattern_select'; +import { IndexPatternsContract } from 'src/plugins/data/public'; +import { HttpSetup } from 'kibana/public'; + +class MockIndexPatternSelectComponent extends React.Component { + render() { + return 'MockIndexPatternSelectComponent'; + } +} + +function makeMockIndexPattern(id: string, fields: unknown) { + return { + id, + fields, + }; +} + +const mockIndexPatternService: IndexPatternsContract = ({ + get(id: string) { + if (id === 'foobar_with_geopoint') { + return makeMockIndexPattern(id, [{ type: 'geo_point' }]); + } else if (id === 'foobar_without_geopoint') { + return makeMockIndexPattern(id, [{ type: 'string' }]); + } + }, +} as unknown) as IndexPatternsContract; + +test('should render without error after mounting', async () => { + const component = shallow( + {}} + value={'foobar_with_geopoint'} + includedGeoTypes={['geo_point']} + indexPatternService={mockIndexPatternService} + IndexPatternSelectComponent={MockIndexPatternSelectComponent} + /> + ); + + // Ensure all promises resolve + await new Promise((resolve) => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + expect(component).toMatchSnapshot(); +}); + +test('should render with error when index pattern does not have geo_point field', async () => { + const component = shallow( + {}} + value={'foobar_without_geopoint'} + includedGeoTypes={['geo_point']} + indexPatternService={mockIndexPatternService} + IndexPatternSelectComponent={MockIndexPatternSelectComponent} + /> + ); + + // Ensure all promises resolve + await new Promise((resolve) => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + expect(component).toMatchSnapshot(); +}); diff --git a/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/query_builder/util_components/geo_index_pattern_select.tsx b/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/query_builder/util_components/geo_index_pattern_select.tsx index 9bb29b65e7454..f024499122ce5 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/query_builder/util_components/geo_index_pattern_select.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/geo_containment/query_builder/util_components/geo_index_pattern_select.tsx @@ -41,9 +41,12 @@ export class GeoIndexPatternSelect extends Component { componentDidMount() { this._isMounted = true; + if (this.props.value) { + this._loadIndexPattern(this.props.value); + } } - _onIndexPatternSelect = async (indexPatternId: string) => { + _loadIndexPattern = async (indexPatternId: string) => { if (!indexPatternId || indexPatternId.length === 0 || !this.props.indexPatternService) { return; } @@ -55,14 +58,22 @@ export class GeoIndexPatternSelect extends Component { return; } - // method may be called again before 'get' returns - // ignore response when fetched index pattern does not match active index pattern - if (this._isMounted && indexPattern.id === indexPatternId) { - this.setState({ - doesIndexPatternHaveGeoField: indexPattern.fields.some((field) => { - return this.props.includedGeoTypes.includes(field.type); - }), - }); + if (!this._isMounted || indexPattern.id !== indexPatternId) { + return; + } + + this.setState({ + doesIndexPatternHaveGeoField: indexPattern.fields.some((field) => { + return this.props.includedGeoTypes.includes(field.type); + }), + }); + + return indexPattern; + }; + + _onIndexPatternSelect = async (indexPatternId: string) => { + const indexPattern = await this._loadIndexPattern(indexPatternId); + if (indexPattern) { this.props.onChange(indexPattern); } }; @@ -123,9 +134,14 @@ export class GeoIndexPatternSelect extends Component { const isIndexPatternInvalid = !!this.props.value && !this.state.doesIndexPatternHaveGeoField; const error = isIndexPatternInvalid ? i18n.translate('xpack.stackAlerts.geoContainment.noGeoFieldInIndexPattern.message', { - defaultMessage: 'Index pattern does not contain any geospatial fields', + defaultMessage: + 'Index pattern does not contain any allowed geospatial fields. Must have one of type {geoFields}.', + values: { + geoFields: this.props.includedGeoTypes.join(', '), + }, }) : ''; + return ( <> {this._renderNoIndexPatternWarning()} diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json index d2fbbf147efd5..688c256d30880 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -5405,13 +5405,22 @@ "monitor_name_stats": { "properties": { "avg_length": { - "type": "float" + "type": "float", + "_meta": { + "description": "This field represents the average length of monitor names" + } }, "max_length": { - "type": "long" + "type": "long", + "_meta": { + "description": "This field represents the max length of monitor names" + } }, "min_length": { - "type": "long" + "type": "long", + "_meta": { + "description": "This field represents the min length of monitor names" + } } } }, @@ -5419,21 +5428,36 @@ "type": "long" }, "no_of_unique_monitors": { - "type": "long" + "type": "long", + "_meta": { + "description": "This field represents the number of unique configured monitors" + } }, "no_of_unique_observer_locations": { - "type": "long" + "type": "long", + "_meta": { + "description": "This field represents the number of unique monitor observer locations" + } }, "observer_location_name_stats": { "properties": { "avg_length": { - "type": "float" + "type": "float", + "_meta": { + "description": "This field represents the average length of monitor observer location names" + } }, "max_length": { - "type": "long" + "type": "long", + "_meta": { + "description": "This field represents the max length of monitor observer location names" + } }, "min_length": { - "type": "long" + "type": "long", + "_meta": { + "description": "This field represents the min length of monitor observer location names" + } } } }, @@ -5442,6 +5466,43 @@ }, "settings_page": { "type": "long" + }, + "fleet_monitor_name_stats": { + "properties": { + "avg_length": { + "type": "float", + "_meta": { + "description": "This field represents the average length of fleet managed monitor names" + } + }, + "max_length": { + "type": "long", + "_meta": { + "description": "This field represents the max length of fleet managed monitor names" + } + }, + "min_length": { + "type": "long", + "_meta": { + "description": "This field represents the min length of fleet managed monitor names" + } + } + } + }, + "fleet_monitor_frequency": { + "type": "array", + "items": { + "type": "long", + "_meta": { + "description": "This field represents the average the monitor frequency of fleet managed monitors" + } + } + }, + "fleet_no_of_unique_monitors": { + "type": "long", + "_meta": { + "description": "This field represents the number of unique configured fleet managed monitors" + } } } } diff --git a/x-pack/plugins/transform/common/utils/field_utils.test.ts b/x-pack/plugins/transform/common/utils/field_utils.test.ts new file mode 100644 index 0000000000000..bb38b3888bd75 --- /dev/null +++ b/x-pack/plugins/transform/common/utils/field_utils.test.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { hasKeywordDuplicate, isKeywordDuplicate, removeKeywordPostfix } from './field_utils'; + +const allFields = new Set([ + 'field1', + 'field2', + 'field2.keyword', + 'field3.keyword', + 'field3.keyword.keyword', + 'field4.keyword.b', + 'field4.keyword.b.keyword', +]); + +describe('field_utils: hasKeywordDuplicate()', () => { + it('returns true when a corresponding keyword field is available', () => { + expect(hasKeywordDuplicate('field2', allFields)).toBe(true); + expect(hasKeywordDuplicate('field3.keyword', allFields)).toBe(true); + expect(hasKeywordDuplicate('field4.keyword.b', allFields)).toBe(true); + }); + it('returns false when a corresponding keyword field is not available', () => { + expect(hasKeywordDuplicate('field1', allFields)).toBe(false); + expect(hasKeywordDuplicate('field2.keyword', allFields)).toBe(false); + expect(hasKeywordDuplicate('field3.keyword.keyword', allFields)).toBe(false); + expect(hasKeywordDuplicate('field4.keyword.b.keyword', allFields)).toBe(false); + }); +}); + +describe('field_utils: isKeywordDuplicate()', () => { + it('returns true when a corresponding field without keyword postfix is available', () => { + expect(isKeywordDuplicate('field2.keyword', allFields)).toBe(true); + expect(isKeywordDuplicate('field3.keyword.keyword', allFields)).toBe(true); + expect(isKeywordDuplicate('field4.keyword.b.keyword', allFields)).toBe(true); + }); + it('returns false when a corresponding field without keyword postfix is not available', () => { + expect(isKeywordDuplicate('field1', allFields)).toBe(false); + expect(isKeywordDuplicate('field2', allFields)).toBe(false); + expect(isKeywordDuplicate('field3.keyword', allFields)).toBe(false); + expect(isKeywordDuplicate('field4.keyword.b', allFields)).toBe(false); + }); +}); + +describe('field_utils: removeKeywordPostfix()', () => { + it('removes the keyword postfix', () => { + expect(removeKeywordPostfix('field2.keyword')).toBe('field2'); + expect(removeKeywordPostfix('field3.keyword.keyword')).toBe('field3.keyword'); + expect(removeKeywordPostfix('field4.keyword.b.keyword')).toBe('field4.keyword.b'); + }); + it("returns the field name as is when there's no keyword postfix", () => { + expect(removeKeywordPostfix('field1')).toBe('field1'); + expect(removeKeywordPostfix('field2')).toBe('field2'); + expect(removeKeywordPostfix('field4.keyword.b')).toBe('field4.keyword.b'); + }); +}); diff --git a/x-pack/plugins/transform/common/utils/field_utils.ts b/x-pack/plugins/transform/common/utils/field_utils.ts new file mode 100644 index 0000000000000..cde26f3e657b9 --- /dev/null +++ b/x-pack/plugins/transform/common/utils/field_utils.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const KEYWORD_POSTFIX = '.keyword'; + +// checks if fieldName has a `fieldName.keyword` equivalent in the set of all field names. +export const hasKeywordDuplicate = (fieldName: string, fieldNamesSet: Set): boolean => + fieldNamesSet.has(`${fieldName}${KEYWORD_POSTFIX}`); + +// checks if a fieldName ends with `.keyword` and has a field name equivalent without the postfix in the set of all field names. +export const isKeywordDuplicate = (fieldName: string, fieldNamesSet: Set): boolean => + fieldName.endsWith(KEYWORD_POSTFIX) && fieldNamesSet.has(removeKeywordPostfix(fieldName)); + +// removes the `.keyword` postfix form a field name if applicable +export const removeKeywordPostfix = (fieldName: string): string => + fieldName.replace(new RegExp(`${KEYWORD_POSTFIX}$`), ''); diff --git a/x-pack/plugins/transform/public/app/hooks/use_index_data.ts b/x-pack/plugins/transform/public/app/hooks/use_index_data.ts index 2dabc6ba1595d..bd1ecd79f4d12 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_index_data.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_index_data.ts @@ -14,6 +14,11 @@ import { isEsSearchResponse, isFieldHistogramsResponseSchema, } from '../../../common/api_schemas/type_guards'; +import { + hasKeywordDuplicate, + isKeywordDuplicate, + removeKeywordPostfix, +} from '../../../common/utils/field_utils'; import type { EsSorting, UseIndexDataReturnType } from '../../shared_imports'; import { getErrorMessage } from '../../../common/utils/errors'; @@ -209,14 +214,25 @@ export const useIndexData = ( }; const fetchColumnChartsData = async function () { + const allIndexPatternFieldNames = new Set(indexPattern.fields.map((f) => f.name)); const columnChartsData = await api.getHistogramsForFields( indexPattern.title, columns .filter((cT) => dataGrid.visibleColumns.includes(cT.id)) - .map((cT) => ({ - fieldName: cT.id, - type: getFieldType(cT.schema), - })), + .map((cT) => { + // If a column field name has a corresponding keyword field, + // fetch the keyword field instead to be able to do aggregations. + const fieldName = cT.id; + return hasKeywordDuplicate(fieldName, allIndexPatternFieldNames) + ? { + fieldName: `${fieldName}.keyword`, + type: getFieldType(undefined), + } + : { + fieldName, + type: getFieldType(cT.schema), + }; + }), isDefaultQuery(query) ? matchAllQuery : query, combinedRuntimeMappings ); @@ -226,7 +242,15 @@ export const useIndexData = ( return; } - setColumnCharts(columnChartsData); + setColumnCharts( + // revert field names with `.keyword` used to do aggregations to their original column name + columnChartsData.map((d) => ({ + ...d, + ...(isKeywordDuplicate(d.id, allIndexPatternFieldNames) + ? { id: removeKeywordPostfix(d.id) } + : {}), + })) + ); }; useEffect(() => { diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_pivot_dropdown_options.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_pivot_dropdown_options.ts index 957439810adc7..300626e0570ae 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_pivot_dropdown_options.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_pivot_dropdown_options.ts @@ -13,6 +13,8 @@ import { } from '../../../../../../../../../../src/plugins/data/public'; import { getNestedProperty } from '../../../../../../../common/utils/object_utils'; +import { removeKeywordPostfix } from '../../../../../../../common/utils/field_utils'; + import { isRuntimeMappings } from '../../../../../../../common/shared_imports'; import { @@ -93,41 +95,44 @@ export function getPivotDropdownOptions( const combinedFields = [...indexPatternFields, ...runtimeFields].sort(sortByLabel); combinedFields.forEach((field) => { + const rawFieldName = field.name; + const displayFieldName = removeKeywordPostfix(rawFieldName); + // Group by const availableGroupByAggs: [] = getNestedProperty(pivotGroupByFieldSupport, field.type); if (availableGroupByAggs !== undefined) { availableGroupByAggs.forEach((groupByAgg) => { // Aggregation name for the group-by is the plain field name. Illegal characters will be removed. - const aggName = field.name.replace(illegalEsAggNameChars, '').trim(); + const aggName = displayFieldName.replace(illegalEsAggNameChars, '').trim(); // Option name in the dropdown for the group-by is in the form of `sum(fieldname)`. - const dropDownName = `${groupByAgg}(${field.name})`; + const dropDownName = `${groupByAgg}(${displayFieldName})`; const groupByOption: DropDownLabel = { label: dropDownName }; groupByOptions.push(groupByOption); groupByOptionsData[dropDownName] = getDefaultGroupByConfig( aggName, dropDownName, - field.name, + rawFieldName, groupByAgg ); }); } // Aggregations - const aggOption: DropDownOption = { label: field.name, options: [] }; + const aggOption: DropDownOption = { label: displayFieldName, options: [] }; const availableAggs: [] = getNestedProperty(pivotAggsFieldSupport, field.type); if (availableAggs !== undefined) { availableAggs.forEach((agg) => { // Aggregation name is formatted like `fieldname.sum`. Illegal characters will be removed. - const aggName = `${field.name.replace(illegalEsAggNameChars, '').trim()}.${agg}`; + const aggName = `${displayFieldName.replace(illegalEsAggNameChars, '').trim()}.${agg}`; // Option name in the dropdown for the aggregation is in the form of `sum(fieldname)`. - const dropDownName = `${agg}(${field.name})`; + const dropDownName = `${agg}(${displayFieldName})`; aggOption.options.push({ label: dropDownName }); aggOptionsData[dropDownName] = getDefaultAggregationConfig( aggName, dropDownName, - field.name, + rawFieldName, agg ); }); diff --git a/x-pack/plugins/transform/server/routes/api/transforms.ts b/x-pack/plugins/transform/server/routes/api/transforms.ts index 1dd136cb484fa..80f80d6f1d71b 100644 --- a/x-pack/plugins/transform/server/routes/api/transforms.ts +++ b/x-pack/plugins/transform/server/routes/api/transforms.ts @@ -61,6 +61,7 @@ import { registerTransformsAuditMessagesRoutes } from './transforms_audit_messag import { registerTransformNodesRoutes } from './transforms_nodes'; import { IIndexPattern } from '../../../../../../src/plugins/data/common/index_patterns'; import { isLatestTransform } from '../../../common/types/transform'; +import { isKeywordDuplicate } from '../../../common/utils/field_utils'; enum TRANSFORM_ACTIONS { STOP = 'stop', @@ -562,9 +563,7 @@ const previewTransformHandler: RequestHandler< ).reduce((acc, [fieldName, fieldCaps]) => { const fieldDefinition = Object.values(fieldCaps)[0]; const isMetaField = fieldDefinition.type.startsWith('_') || fieldName === '_doc_count'; - const isKeywordDuplicate = - fieldName.endsWith('.keyword') && fieldNamesSet.has(fieldName.split('.keyword')[0]); - if (isMetaField || isKeywordDuplicate) { + if (isMetaField || isKeywordDuplicate(fieldName, fieldNamesSet)) { return acc; } acc[fieldName] = { ...fieldDefinition }; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index a20cf5a94312a..ff47ac52872cc 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -7614,10 +7614,10 @@ "xpack.enterpriseSearch.overview.productCard.launchButton": "{productName}の起動", "xpack.enterpriseSearch.overview.productCard.setupButton": "{productName} のセットアップ", "xpack.enterpriseSearch.overview.setupCta.description": "Elastic App Search および Workplace Search を使用して、アプリまたは社内組織に検索を追加できます。検索が簡単になるとどのような利点があるのかについては、動画をご覧ください。", - "xpack.enterpriseSearch.overview.setupCta.title": "あらゆる規模のチームに対応するエンタープライズ級の機能", "xpack.enterpriseSearch.overview.setupHeading": "セットアップする製品を選択し、開始してください。", "xpack.enterpriseSearch.overview.subheading": "開始する製品を選択します。", "xpack.enterpriseSearch.productName": "エンタープライズサーチ", + "xpack.enterpriseSearch.productSelectorCalloutTitle": "あらゆる規模のチームに対応するエンタープライズ級の機能", "xpack.enterpriseSearch.readOnlyMode.warning": "エンタープライズ サーチは読み取り専用モードです。作成、編集、削除などの変更を実行できません。", "xpack.enterpriseSearch.schema.addFieldModal.addFieldButtonLabel": "フィールドの追加", "xpack.enterpriseSearch.schema.addFieldModal.description": "追加すると、フィールドはスキーマから削除されます。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index c4c18a18d37e2..0f1c84d0fcaa1 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -7684,10 +7684,10 @@ "xpack.enterpriseSearch.overview.productCard.launchButton": "推出 {productName}", "xpack.enterpriseSearch.overview.productCard.setupButton": "设置 {productName}", "xpack.enterpriseSearch.overview.setupCta.description": "通过 Elastic App Search 和 Workplace Search,将搜索添加到您的应用或内部组织中。观看视频,了解方便易用的搜索功能可以帮您做些什么。", - "xpack.enterpriseSearch.overview.setupCta.title": "适用于大型和小型团队的企业级功能", "xpack.enterpriseSearch.overview.setupHeading": "选择产品进行设置并开始使用。", "xpack.enterpriseSearch.overview.subheading": "选择产品开始使用。", "xpack.enterpriseSearch.productName": "企业搜索", + "xpack.enterpriseSearch.productSelectorCalloutTitle": "适用于大型和小型团队的企业级功能", "xpack.enterpriseSearch.readOnlyMode.warning": "企业搜索处于只读模式。您将无法执行更改,例如创建、编辑或删除。", "xpack.enterpriseSearch.schema.addFieldModal.addFieldButtonLabel": "添加字段", "xpack.enterpriseSearch.schema.addFieldModal.description": "字段添加后,将无法从架构中删除。", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/connectors.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/connectors.ts index cf424ea1e7317..7011ec016c089 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/connectors.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/connectors.ts @@ -23,11 +23,13 @@ const transformConnector: RewriteRequestCase< connector_type_id: actionTypeId, is_preconfigured: isPreconfigured, referenced_by_count: referencedByCount, + is_missing_secrets: isMissingSecrets, ...res }) => ({ actionTypeId, isPreconfigured, referencedByCount, + isMissingSecrets, ...res, }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/create.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/create.ts index e6e74f3f3c059..ba5c8a214b6ae 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/create.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/create.ts @@ -23,10 +23,16 @@ const rewriteBodyRequest: RewriteResponseCase< const rewriteBodyRes: RewriteRequestCase< ActionConnectorProps, Record> -> = ({ connector_type_id: actionTypeId, is_preconfigured: isPreconfigured, ...res }) => ({ +> = ({ + connector_type_id: actionTypeId, + is_preconfigured: isPreconfigured, + is_missing_secrets: isMissingSecrets, + ...res +}) => ({ ...res, actionTypeId, isPreconfigured, + isMissingSecrets, }); export async function createActionConnector({ diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/update.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/update.ts index 1bc0cefc2723b..f2319ace29d68 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/update.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api/update.ts @@ -15,10 +15,16 @@ import type { const rewriteBodyRes: RewriteRequestCase< ActionConnectorProps, Record> -> = ({ connector_type_id: actionTypeId, is_preconfigured: isPreconfigured, ...res }) => ({ +> = ({ + connector_type_id: actionTypeId, + is_preconfigured: isPreconfigured, + is_missing_secrets: isMissingSecrets, + ...res +}) => ({ ...res, actionTypeId, isPreconfigured, + isMissingSecrets, }); export async function updateActionConnector({ diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx index 26a101cedf955..174407e7edec5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx @@ -124,6 +124,72 @@ describe('action_form', () => { actionParamsFields: mockedActionParamsFields, }; + const allActions = [ + { + secrets: {}, + isMissingSecrets: false, + id: 'test', + actionTypeId: actionType.id, + name: 'Test connector', + config: {}, + isPreconfigured: false, + }, + { + secrets: {}, + isMissingSecrets: false, + id: 'test2', + actionTypeId: actionType.id, + name: 'Test connector 2', + config: {}, + isPreconfigured: true, + }, + { + secrets: {}, + isMissingSecrets: false, + id: 'test3', + actionTypeId: preconfiguredOnly.id, + name: 'Preconfigured Only', + config: {}, + isPreconfigured: true, + }, + { + secrets: {}, + isMissingSecrets: false, + id: 'test4', + actionTypeId: preconfiguredOnly.id, + name: 'Regular connector', + config: {}, + isPreconfigured: false, + }, + { + secrets: {}, + isMissingSecrets: false, + id: '.servicenow', + actionTypeId: '.servicenow', + name: 'Non consumer connector', + config: {}, + isPreconfigured: false, + }, + { + secrets: {}, + isMissingSecrets: false, + id: '.jira', + actionTypeId: disabledByActionType.id, + name: 'Connector with disabled action group', + config: {}, + isPreconfigured: false, + }, + { + secrets: null, + isMissingSecrets: true, + id: '.jira', + actionTypeId: actionType.id, + name: 'Connector with disabled action group', + config: {}, + isPreconfigured: false, + }, + ]; + const useKibanaMock = useKibana as jest.Mocked; describe('action_form in alert', () => { @@ -131,56 +197,7 @@ describe('action_form', () => { const actionTypeRegistry = actionTypeRegistryMock.create(); const { loadAllActions } = jest.requireMock('../../lib/action_connector_api'); - loadAllActions.mockResolvedValueOnce([ - { - secrets: {}, - id: 'test', - actionTypeId: actionType.id, - name: 'Test connector', - config: {}, - isPreconfigured: false, - }, - { - secrets: {}, - id: 'test2', - actionTypeId: actionType.id, - name: 'Test connector 2', - config: {}, - isPreconfigured: true, - }, - { - secrets: {}, - id: 'test3', - actionTypeId: preconfiguredOnly.id, - name: 'Preconfigured Only', - config: {}, - isPreconfigured: true, - }, - { - secrets: {}, - id: 'test4', - actionTypeId: preconfiguredOnly.id, - name: 'Regular connector', - config: {}, - isPreconfigured: false, - }, - { - secrets: {}, - id: '.servicenow', - actionTypeId: '.servicenow', - name: 'Non consumer connector', - config: {}, - isPreconfigured: false, - }, - { - secrets: {}, - id: '.jira', - actionTypeId: disabledByActionType.id, - name: 'Connector with disabled action group', - config: {}, - isPreconfigured: false, - }, - ]); + loadAllActions.mockResolvedValueOnce(allActions); const mocks = coreMock.createSetup(); const [ { @@ -467,6 +484,14 @@ describe('action_form', () => { ); actionOption.first().simulate('click'); const combobox = wrapper.find(`[data-test-subj="selectActionConnector-${actionType.id}"]`); + const numConnectors = allActions.filter((action) => action.actionTypeId === actionType.id) + .length; + const numConnectorsWithMissingSecrets = allActions.filter( + (action) => action.actionTypeId === actionType.id && action.isMissingSecrets + ).length; + expect((combobox.first().props() as any).options.length).toEqual( + numConnectors - numConnectorsWithMissingSecrets + ); expect((combobox.first().props() as any).options).toMatchInlineSnapshot(` Array [ Object { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx index 02e96b5fd05c5..55ebbbc6f3edd 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx @@ -141,7 +141,7 @@ export const ActionForm = ({ try { setIsLoadingConnectors(true); const loadedConnectors = await loadConnectors({ http }); - setConnectors(loadedConnectors); + setConnectors(loadedConnectors.filter((connector) => !connector.isMissingSecrets)); } catch (e) { toasts.addDanger({ title: i18n.translate( diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index 1fd031cda6d96..6db5634be2221 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -141,6 +141,7 @@ export interface ActionConnectorProps { referencedByCount?: number; config: Config; isPreconfigured: boolean; + isMissingSecrets?: boolean; } export type PreConfiguredActionConnector = Omit< diff --git a/x-pack/plugins/uptime/public/state/api/utils.ts b/x-pack/plugins/uptime/public/state/api/utils.ts index f59f1939b5989..91e017292d00f 100644 --- a/x-pack/plugins/uptime/public/state/api/utils.ts +++ b/x-pack/plugins/uptime/public/state/api/utils.ts @@ -16,8 +16,9 @@ function isObject(value: unknown) { return value != null && (type === 'object' || type === 'function'); } -// TODO: Copied from https://github.com/elastic/kibana/blob/master/x-pack/plugins/security_solution/common/format_errors.ts -// We should figure out a better way to share this +/** + * @deprecated Use packages/kbn-securitysolution-io-ts-utils/src/format_errors/index.ts + */ export const formatErrors = (errors: t.Errors): string[] => { return errors.map((error) => { if (error.message != null) { diff --git a/x-pack/plugins/uptime/server/lib/adapters/telemetry/__snapshots__/kibana_telemetry_adapter.test.ts.snap b/x-pack/plugins/uptime/server/lib/adapters/telemetry/__snapshots__/kibana_telemetry_adapter.test.ts.snap index 8c55d5da54ac7..9148887ae4f83 100644 --- a/x-pack/plugins/uptime/server/lib/adapters/telemetry/__snapshots__/kibana_telemetry_adapter.test.ts.snap +++ b/x-pack/plugins/uptime/server/lib/adapters/telemetry/__snapshots__/kibana_telemetry_adapter.test.ts.snap @@ -14,6 +14,13 @@ Object { "dateRangeStart": Array [ "now-15", ], + "fleet_monitor_frequency": Array [], + "fleet_monitor_name_stats": Object { + "avg_length": 0, + "max_length": 0, + "min_length": 0, + }, + "fleet_no_of_unique_monitors": 0, "monitor_frequency": Array [], "monitor_name_stats": Object { "avg_length": 0, @@ -49,6 +56,13 @@ Object { "dateRangeStart": Array [ "now-15", ], + "fleet_monitor_frequency": Array [], + "fleet_monitor_name_stats": Object { + "avg_length": 0, + "max_length": 0, + "min_length": 0, + }, + "fleet_no_of_unique_monitors": 0, "monitor_frequency": Array [], "monitor_name_stats": Object { "avg_length": 0, diff --git a/x-pack/plugins/uptime/server/lib/adapters/telemetry/kibana_telemetry_adapter.test.ts b/x-pack/plugins/uptime/server/lib/adapters/telemetry/kibana_telemetry_adapter.test.ts index 06a14b30bdae0..70385698ca6e0 100644 --- a/x-pack/plugins/uptime/server/lib/adapters/telemetry/kibana_telemetry_adapter.test.ts +++ b/x-pack/plugins/uptime/server/lib/adapters/telemetry/kibana_telemetry_adapter.test.ts @@ -27,7 +27,9 @@ describe('KibanaTelemetryAdapter', () => { }, }; getSavedObjectsClient = () => { - return {}; + return { + get: () => {}, + }; }; collectorFetchContext = createCollectorFetchContextMock(); }); diff --git a/x-pack/plugins/uptime/server/lib/adapters/telemetry/kibana_telemetry_adapter.ts b/x-pack/plugins/uptime/server/lib/adapters/telemetry/kibana_telemetry_adapter.ts index e540909b505c7..631bcf1b245db 100644 --- a/x-pack/plugins/uptime/server/lib/adapters/telemetry/kibana_telemetry_adapter.ts +++ b/x-pack/plugins/uptime/server/lib/adapters/telemetry/kibana_telemetry_adapter.ts @@ -52,20 +52,104 @@ export class KibanaTelemetryAdapter { dateRangeStart: { type: 'array', items: { type: 'date' } }, monitor_frequency: { type: 'array', items: { type: 'long' } }, monitor_name_stats: { - avg_length: { type: 'float' }, - max_length: { type: 'long' }, - min_length: { type: 'long' }, + avg_length: { + type: 'float', + _meta: { + description: 'This field represents the average length of monitor names', + }, + }, + max_length: { + type: 'long', + _meta: { + description: 'This field represents the max length of monitor names', + }, + }, + min_length: { + type: 'long', + _meta: { + description: 'This field represents the min length of monitor names', + }, + }, }, monitor_page: { type: 'long' }, - no_of_unique_monitors: { type: 'long' }, - no_of_unique_observer_locations: { type: 'long' }, + no_of_unique_monitors: { + type: 'long', + _meta: { + description: 'This field represents the number of unique configured monitors', + }, + }, + no_of_unique_observer_locations: { + type: 'long', + _meta: { + description: + 'This field represents the number of unique monitor observer locations', + }, + }, observer_location_name_stats: { - avg_length: { type: 'float' }, - max_length: { type: 'long' }, - min_length: { type: 'long' }, + avg_length: { + type: 'float', + _meta: { + description: + 'This field represents the average length of monitor observer location names', + }, + }, + max_length: { + type: 'long', + _meta: { + description: + 'This field represents the max length of monitor observer location names', + }, + }, + min_length: { + type: 'long', + _meta: { + description: + 'This field represents the min length of monitor observer location names', + }, + }, }, overview_page: { type: 'long' }, settings_page: { type: 'long' }, + fleet_monitor_name_stats: { + avg_length: { + type: 'float', + _meta: { + description: + 'This field represents the average length of fleet managed monitor names', + }, + }, + max_length: { + type: 'long', + _meta: { + description: + 'This field represents the max length of fleet managed monitor names', + }, + }, + min_length: { + type: 'long', + _meta: { + description: + 'This field represents the min length of fleet managed monitor names', + }, + }, + }, + fleet_monitor_frequency: { + type: 'array', + items: { + type: 'long', + _meta: { + description: + 'This field represents the average the monitor frequency of fleet managed monitors', + }, + }, + }, + fleet_no_of_unique_monitors: { + type: 'long', + _meta: { + description: + 'This field represents the number of unique configured fleet managed monitors', + }, + }, }, }, }, @@ -74,6 +158,7 @@ export class KibanaTelemetryAdapter { if (savedObjectsClient) { const uptimeEsClient = createUptimeESClient({ esClient, savedObjectsClient }); await this.countNoOfUniqueMonitorAndLocations(uptimeEsClient, savedObjectsClient); + await this.countNoOfUniqueFleetManagedMonitors(uptimeEsClient); } const report = this.getReport(); return { last_24_hours: { hits: { ...report } } }; @@ -218,6 +303,79 @@ export class KibanaTelemetryAdapter { return bucket; } + public static async countNoOfUniqueFleetManagedMonitors(callCluster: UptimeESClient) { + const params = { + index: 'synthetics-*', + body: { + query: { + bool: { + must: [ + { + range: { + '@timestamp': { + gte: 'now-1d/d', + lt: 'now', + }, + }, + }, + { + term: { + 'monitor.fleet_managed': true, + }, + }, + ], + }, + }, + size: 0, + aggs: { + unique_monitors: { + cardinality: { + field: 'monitor.id', + }, + }, + monitor_name: { + string_stats: { + field: 'monitor.name', + }, + }, + monitors: { + terms: { + field: 'monitor.id', + size: 1000, + }, + aggs: { + docs: { + top_hits: { + size: 1, + _source: ['monitor.timespan'], + }, + }, + }, + }, + }, + }, + }; + + const { body: result } = await callCluster.search(params); + + const numberOfUniqueMonitors: number = result?.aggregations?.unique_monitors?.value ?? 0; + const monitorNameStats = result?.aggregations?.monitor_name; + const uniqueMonitors: any = result?.aggregations?.monitors.buckets; + + const bucketId = this.getBucketToIncrement(); + const bucket = this.collector[bucketId]; + + bucket.fleet_no_of_unique_monitors = numberOfUniqueMonitors; + bucket.fleet_monitor_name_stats = { + min_length: monitorNameStats?.min_length ?? 0, + max_length: monitorNameStats?.max_length ?? 0, + avg_length: +(monitorNameStats?.avg_length?.toFixed(2) ?? 0), + }; + + bucket.fleet_monitor_frequency = this.getMonitorsFrequency(uniqueMonitors); + return bucket; + } + private static getMonitorsFrequency(uniqueMonitors = []) { const frequencies: number[] = []; uniqueMonitors @@ -285,6 +443,14 @@ export class KibanaTelemetryAdapter { dateRangeEnd: [], autoRefreshEnabled: false, autorefreshInterval: [], + + fleet_no_of_unique_monitors: 0, + fleet_monitor_frequency: [], + fleet_monitor_name_stats: { + min_length: 0, + max_length: 0, + avg_length: 0, + }, }; } return bucketId; diff --git a/x-pack/plugins/uptime/server/lib/adapters/telemetry/types.ts b/x-pack/plugins/uptime/server/lib/adapters/telemetry/types.ts index 632544a5c2d3d..eceee3505dd7e 100644 --- a/x-pack/plugins/uptime/server/lib/adapters/telemetry/types.ts +++ b/x-pack/plugins/uptime/server/lib/adapters/telemetry/types.ts @@ -40,4 +40,8 @@ export interface UptimeTelemetry { dateRangeEnd: string[]; autorefreshInterval: number[]; autoRefreshEnabled: boolean; + + fleet_no_of_unique_monitors: number; + fleet_monitor_frequency: number[]; + fleet_monitor_name_stats: Stats; } diff --git a/x-pack/plugins/uptime/server/rest_api/telemetry/log_page_view.ts b/x-pack/plugins/uptime/server/rest_api/telemetry/log_page_view.ts index ef1e0a07c6392..088cf494efbf7 100644 --- a/x-pack/plugins/uptime/server/rest_api/telemetry/log_page_view.ts +++ b/x-pack/plugins/uptime/server/rest_api/telemetry/log_page_view.ts @@ -34,6 +34,7 @@ export const createLogPageViewRoute: UMRestApiRouteFactory = () => ({ uptimeEsClient, savedObjectsClient ); + await KibanaTelemetryAdapter.countNoOfUniqueFleetManagedMonitors(uptimeEsClient); return KibanaTelemetryAdapter.countPageView(pageView as PageViewParams); }, }); diff --git a/x-pack/test/accessibility/apps/transform.ts b/x-pack/test/accessibility/apps/transform.ts index 4c0cae4174d36..38cd8d98e8c32 100644 --- a/x-pack/test/accessibility/apps/transform.ts +++ b/x-pack/test/accessibility/apps/transform.ts @@ -45,8 +45,8 @@ export default function ({ getService }: FtrProviderContext) { const pivotGroupByEntries = [ { - identifier: 'terms(category.keyword)', - label: 'category.keyword', + identifier: 'terms(category)', + label: 'category', }, { identifier: 'date_histogram(order_date)', diff --git a/x-pack/test/api_integration/apis/uptime/rest/helper/make_checks.ts b/x-pack/test/api_integration/apis/uptime/rest/helper/make_checks.ts index c8b1434b6ffd0..829ae07eccf20 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/helper/make_checks.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/helper/make_checks.ts @@ -18,6 +18,7 @@ interface CheckProps { mogrify?: (doc: any) => any; refresh?: boolean; tls?: boolean | TlsProps; + isFleetManaged?: boolean; } const getRandomMonitorId = () => { @@ -31,6 +32,7 @@ export const makeCheck = async ({ mogrify = (d) => d, refresh = true, tls = false, + isFleetManaged = false, }: CheckProps): Promise<{ monitorId: string; docs: any }> => { const cgFields = { monitor: { @@ -52,7 +54,15 @@ export const makeCheck = async ({ if (i === numIps - 1) { pingFields.summary = summary; } - const doc = await makePing(es, monitorId, pingFields, mogrify, false, tls as any); + const doc = await makePing( + es, + monitorId, + pingFields, + mogrify, + false, + tls as any, + isFleetManaged + ); docs.push(doc); // @ts-ignore summary[doc.monitor.status]++; @@ -73,7 +83,8 @@ export const makeChecks = async ( every: number = 10000, // number of millis between checks fields: { [key: string]: any } = {}, mogrify: (doc: any) => any = (d) => d, - refresh: boolean = true + refresh: boolean = true, + isFleetManaged: boolean = false ) => { const checks = []; const oldestTime = new Date().getTime() - numChecks * every; @@ -90,7 +101,15 @@ export const makeChecks = async ( }, }, }); - const { docs } = await makeCheck({ es, monitorId, numIps, fields, mogrify, refresh: false }); + const { docs } = await makeCheck({ + es, + monitorId, + numIps, + fields, + mogrify, + refresh: false, + isFleetManaged, + }); checks.push(docs); } @@ -110,7 +129,8 @@ export const makeChecksWithStatus = async ( fields: { [key: string]: any } = {}, status: 'up' | 'down', mogrify: (doc: any) => any = (d) => d, - refresh: boolean = true + refresh: boolean = true, + isFleetManaged: boolean = false ) => { const oppositeStatus = status === 'up' ? 'down' : 'up'; @@ -130,7 +150,8 @@ export const makeChecksWithStatus = async ( return mogrify(d); }, - refresh + refresh, + isFleetManaged ); }; diff --git a/x-pack/test/api_integration/apis/uptime/rest/helper/make_ping.ts b/x-pack/test/api_integration/apis/uptime/rest/helper/make_ping.ts index f6ce4246f0a23..c45f68e4ae0ca 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/helper/make_ping.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/helper/make_ping.ts @@ -9,7 +9,8 @@ import uuid from 'uuid'; import { merge } from 'lodash'; import { makeTls, TlsProps } from './make_tls'; -const INDEX_NAME = 'heartbeat-8-generated-test'; +const DEFAULT_INDEX_NAME = 'heartbeat-8-generated-test'; +const DATA_STREAM_INDEX_NAME = 'synthetics-http-default'; export const makePing = async ( es: any, @@ -17,7 +18,8 @@ export const makePing = async ( fields: { [key: string]: any }, mogrify: (doc: any) => any, refresh: boolean = true, - tls: boolean | TlsProps = false + tls: boolean | TlsProps = false, + isFleetManaged: boolean | undefined = false ) => { const timestamp = new Date(); const baseDoc: any = { @@ -115,7 +117,7 @@ export const makePing = async ( const doc = mogrify(merge(baseDoc, fields)); await es.index({ - index: INDEX_NAME, + index: isFleetManaged ? DATA_STREAM_INDEX_NAME : DEFAULT_INDEX_NAME, refresh, body: doc, }); diff --git a/x-pack/test/api_integration/apis/uptime/rest/index.ts b/x-pack/test/api_integration/apis/uptime/rest/index.ts index a46aa653b6f2b..9f90d3739797b 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/index.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/index.ts @@ -38,14 +38,21 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { }); describe('with generated data', () => { - beforeEach('load heartbeat data', async () => await esArchiver.loadIfNeeded('uptime/blank')); - after('unload', async () => await esArchiver.unload('uptime/blank')); + beforeEach('load heartbeat data', async () => { + await esArchiver.loadIfNeeded('uptime/blank'); + await esArchiver.loadIfNeeded('uptime/blank_data_stream'); + }); + after('unload', async () => { + await esArchiver.unload('uptime/blank'); + await esArchiver.unload('uptime/blank_data_stream'); + }); loadTestFile(require.resolve('./certs')); loadTestFile(require.resolve('./dynamic_settings')); loadTestFile(require.resolve('./snapshot')); loadTestFile(require.resolve('./monitor_states_generated')); loadTestFile(require.resolve('./telemetry_collectors')); + loadTestFile(require.resolve('./telemetry_collectors_fleet')); }); describe('with real-world data', () => { diff --git a/x-pack/test/api_integration/apis/uptime/rest/telemetry_collectors.ts b/x-pack/test/api_integration/apis/uptime/rest/telemetry_collectors.ts index e528ab6719fe7..f0f7a520d098a 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/telemetry_collectors.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/telemetry_collectors.ts @@ -14,7 +14,7 @@ export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const es = getService('legacyEs'); - describe('telemetry collectors', () => { + describe('telemetry collectors heartbeat', () => { before('generating data', async () => { await getService('esArchiver').load('uptime/blank'); @@ -82,7 +82,9 @@ export default function ({ getService }: FtrProviderContext) { await es.indices.refresh(); }); - after('unload heartbeat index', () => getService('esArchiver').unload('uptime/blank')); + after('unload heartbeat index', () => { + getService('esArchiver').unload('uptime/blank'); + }); beforeEach(async () => { await es.indices.refresh(); @@ -116,6 +118,13 @@ export default function ({ getService }: FtrProviderContext) { dateRangeEnd: ['now/d'], autoRefreshEnabled: true, autorefreshInterval: [100], + fleet_monitor_frequency: [], + fleet_monitor_name_stats: { + avg_length: 0, + max_length: 0, + min_length: 0, + }, + fleet_no_of_unique_monitors: 0, }); }); diff --git a/x-pack/test/api_integration/apis/uptime/rest/telemetry_collectors_fleet.ts b/x-pack/test/api_integration/apis/uptime/rest/telemetry_collectors_fleet.ts new file mode 100644 index 0000000000000..8c462f1db431b --- /dev/null +++ b/x-pack/test/api_integration/apis/uptime/rest/telemetry_collectors_fleet.ts @@ -0,0 +1,191 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { API_URLS } from '../../../../../plugins/uptime/common/constants'; +import { makeChecksWithStatus } from './helper/make_checks'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + const client = getService('es'); + + describe('telemetry collectors fleet', () => { + before('generating data', async () => { + await getService('esArchiver').load('uptime/blank_data_stream'); + + const observer = { + geo: { + name: 'US-East', + location: '40.7128, -74.0060', + }, + }; + + const observer2 = { + geo: { + name: 'US', + location: '40.7128, -74.0060', + }, + }; + + await makeChecksWithStatus( + es, + 'upMonitorId', + 1, + 1, + 60 * 1000, + { + observer: {}, + monitor: { + name: 'Elastic', + fleet_managed: true, + }, + }, + 'up', + undefined, + undefined, + true + ); + + await makeChecksWithStatus( + es, + 'downMonitorId', + 1, + 1, + 120 * 1000, + { + observer, + monitor: { + name: 'Long Name with 22 Char', + fleet_managed: true, + }, + }, + 'down', + undefined, + undefined, + true + ); + + await makeChecksWithStatus( + es, + 'noGeoNameMonitor', + 1, + 1, + 60 * 1000, + { + observer: {}, + monitor: { + fleet_managed: true, + }, + }, + 'down', + undefined, + undefined, + true + ); + await makeChecksWithStatus( + es, + 'downMonitorId', + 1, + 1, + 1, + { + observer, + monitor: { + name: 'Elastic', + fleet_managed: true, + }, + }, + 'down', + undefined, + undefined, + true + ); + + await makeChecksWithStatus( + es, + 'mixMonitorId', + 1, + 1, + 1, + { observer: observer2, monitor: { fleet_managed: true } }, + 'down', + undefined, + undefined, + true + ); + await es.indices.refresh(); + }); + + after('unload heartbeat index', () => { + getService('esArchiver').unload('uptime/blank_data_stream'); + /** + * Data streams aren't included in the javascript elasticsearch client in kibana yet so we + * need to do raw requests here. Delete a data stream is slightly different than that of a regular index which + * is why we're using _data_stream here. + */ + client.transport.request({ + method: 'DELETE', + path: `_data_stream/synthetics-http-default`, + }); + }); + + beforeEach(async () => { + await es.indices.refresh(); + }); + + it('should receive expected results for fleet managed monitors after calling monitor logging', async () => { + // call monitor page + const { body: result } = await supertest + .post(API_URLS.LOG_PAGE_VIEW) + .set('kbn-xsrf', 'true') + .send({ + page: 'Monitor', + autorefreshInterval: 100, + dateStart: 'now/d', + dateEnd: 'now/d', + autoRefreshEnabled: true, + refreshTelemetryHistory: true, + }) + .expect(200); + + expect(result).to.eql({ + overview_page: 0, + monitor_page: 1, + no_of_unique_monitors: 4, + settings_page: 0, + monitor_frequency: [120, 0.001, 60, 60], + monitor_name_stats: { min_length: 7, max_length: 22, avg_length: 12 }, + no_of_unique_observer_locations: 3, + observer_location_name_stats: { min_length: 2, max_length: 7, avg_length: 4.8 }, + dateRangeStart: ['now/d'], + dateRangeEnd: ['now/d'], + autoRefreshEnabled: true, + autorefreshInterval: [100], + fleet_monitor_frequency: [120, 0.001, 60, 60], + fleet_monitor_name_stats: { min_length: 7, max_length: 22, avg_length: 12 }, + fleet_no_of_unique_monitors: 4, + }); + }); + + it('should receive 200 status after overview logging', async () => { + // call overview page + await supertest + .post(API_URLS.LOG_PAGE_VIEW) + .set('kbn-xsrf', 'true') + .send({ + page: 'Overview', + autorefreshInterval: 60, + dateStart: 'now/d', + dateEnd: 'now-30', + autoRefreshEnabled: true, + }) + .expect(200); + }); + }); +} diff --git a/x-pack/test/case_api_integration/basic/tests/cases/alerts/get_cases.ts b/x-pack/test/case_api_integration/basic/tests/cases/alerts/get_cases.ts new file mode 100644 index 0000000000000..140fb80949a24 --- /dev/null +++ b/x-pack/test/case_api_integration/basic/tests/cases/alerts/get_cases.ts @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; +import { postCaseReq, postCommentAlertReq } from '../../../../common/lib/mock'; +import { deleteAllCaseItems } from '../../../../common/lib/utils'; +import { CaseResponse } from '../../../../../../plugins/cases/common'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + + describe('get_cases using alertID', () => { + const createCase = async () => { + const { body } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + return body; + }; + + const createComment = async (caseID: string) => { + await supertest + .post(`${CASES_URL}/${caseID}/comments`) + .set('kbn-xsrf', 'true') + .send(postCommentAlertReq) + .expect(200); + }; + + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should return all cases with the same alert ID attached to them', async () => { + const [case1, case2, case3] = await Promise.all([createCase(), createCase(), createCase()]); + + await Promise.all([ + createComment(case1.id), + createComment(case2.id), + createComment(case3.id), + ]); + + const { body: caseIDsWithAlert } = await supertest + .get(`${CASES_URL}/alerts/test-id`) + .expect(200); + + expect(caseIDsWithAlert.length).to.eql(3); + expect(caseIDsWithAlert).to.contain(case1.id); + expect(caseIDsWithAlert).to.contain(case2.id); + expect(caseIDsWithAlert).to.contain(case3.id); + }); + + it('should return all cases with the same alert ID when more than 100 cases', async () => { + // if there are more than 100 responses, the implementation sets the aggregation size to the + // specific value + const numCases = 102; + const createCasePromises: Array> = []; + for (let i = 0; i < numCases; i++) { + createCasePromises.push(createCase()); + } + + const cases = await Promise.all(createCasePromises); + + const commentPromises: Array> = []; + for (const caseInfo of cases) { + commentPromises.push(createComment(caseInfo.id)); + } + + await Promise.all(commentPromises); + + const { body: caseIDsWithAlert } = await supertest + .get(`${CASES_URL}/alerts/test-id`) + .expect(200); + + expect(caseIDsWithAlert.length).to.eql(numCases); + + for (const caseInfo of cases) { + expect(caseIDsWithAlert).to.contain(caseInfo.id); + } + }); + + it('should return no cases when the alert ID is not found', async () => { + const [case1, case2, case3] = await Promise.all([createCase(), createCase(), createCase()]); + + await Promise.all([ + createComment(case1.id), + createComment(case2.id), + createComment(case3.id), + ]); + + const { body: caseIDsWithAlert } = await supertest + .get(`${CASES_URL}/alerts/test-id100`) + .expect(200); + + expect(caseIDsWithAlert.length).to.eql(0); + }); + + it('should return a 302 when passing an empty alertID', async () => { + // kibana returns a 302 instead of a 400 when a url param is missing + await supertest.get(`${CASES_URL}/alerts/`).expect(302); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/basic/tests/index.ts b/x-pack/test/case_api_integration/basic/tests/index.ts index 837e6503084a7..24e6b12138895 100644 --- a/x-pack/test/case_api_integration/basic/tests/index.ts +++ b/x-pack/test/case_api_integration/basic/tests/index.ts @@ -13,6 +13,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => { // Fastest ciGroup for the moment. this.tags('ciGroup5'); + loadTestFile(require.resolve('./cases/alerts/get_cases')); loadTestFile(require.resolve('./cases/comments/delete_comment')); loadTestFile(require.resolve('./cases/comments/find_comments')); loadTestFile(require.resolve('./cases/comments/get_comment')); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_ml.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_ml.ts new file mode 100644 index 0000000000000..9e7fb0ea7c84b --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_ml.ts @@ -0,0 +1,256 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; + +import { MachineLearningCreateSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + createRule, + createRuleWithExceptionEntries, + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getOpenSignals, +} from '../../utils'; +import { + createListsIndex, + deleteAllExceptions, + deleteListsIndex, + importFile, +} from '../../../lists_api_integration/utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const es = getService('es'); + const siemModule = 'siem_auditbeat'; + const mlJobId = 'linux_anomalous_network_activity_ecs'; + const testRule: MachineLearningCreateSchema = { + name: 'Test ML rule', + description: 'Test ML rule description', + risk_score: 50, + severity: 'critical', + type: 'machine_learning', + anomaly_threshold: 30, + machine_learning_job_id: mlJobId, + from: '1900-01-01T00:00:00.000Z', + }; + + async function executeSetupModuleRequest(module: string, rspCode: number) { + const { body } = await supertest + .post(`/api/ml/modules/setup/${module}`) + .set('kbn-xsrf', 'true') + .send({ + prefix: '', + groups: ['auditbeat'], + indexPatternName: 'auditbeat-*', + startDatafeed: false, + useDedicatedIndex: true, + applyToAllSpaces: true, + }) + .expect(rspCode); + + return body; + } + + async function forceStartDatafeeds(jobId: string, rspCode: number) { + const { body } = await supertest + .post(`/api/ml/jobs/force_start_datafeeds`) + .set('kbn-xsrf', 'true') + .send({ + datafeedIds: [`datafeed-${jobId}`], + start: new Date().getUTCMilliseconds(), + }) + .expect(rspCode); + + return body; + } + + describe('Generating signals from ml anomalies', () => { + before(async () => { + // Order is critical here: auditbeat data must be loaded before attempting to start the ML job, + // as the job looks for certain indices on start + await esArchiver.load('auditbeat/hosts'); + await executeSetupModuleRequest(siemModule, 200); + await forceStartDatafeeds(mlJobId, 200); + await esArchiver.load('security_solution/anomalies'); + }); + after(async () => { + await esArchiver.unload('auditbeat/hosts'); + await esArchiver.unload('security_solution/anomalies'); + }); + + beforeEach(async () => { + await createSignalsIndex(supertest); + }); + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(supertest); + }); + + it('should create 1 alert from ML rule when record meets anomaly_threshold', async () => { + const createdRule = await createRule(supertest, testRule); + const signalsOpen = await getOpenSignals(supertest, es, createdRule); + expect(signalsOpen.hits.hits.length).eql(1); + const signal = signalsOpen.hits.hits[0]; + expect(signal._source).eql({ + '@timestamp': signal._source['@timestamp'], + actual: [1], + bucket_span: 900, + by_field_name: 'process.name', + by_field_value: 'store', + detector_index: 0, + function: 'rare', + function_description: 'rare', + influencers: [ + { influencer_field_name: 'user.name', influencer_field_values: ['root'] }, + { influencer_field_name: 'process.name', influencer_field_values: ['store'] }, + { influencer_field_name: 'host.name', influencer_field_values: ['mothra'] }, + ], + initial_record_score: 33.36147565024334, + is_interim: false, + job_id: 'linux_anomalous_network_activity_ecs', + multi_bucket_impact: 0, + probability: 0.007820139656036713, + record_score: 33.36147565024334, + result_type: 'record', + timestamp: 1605567488000, + typical: [0.007820139656036711], + user: { name: ['root'] }, + process: { name: ['store'] }, + host: { name: ['mothra'] }, + event: { kind: 'signal' }, + signal: { + _meta: { version: 35 }, + parents: [ + { + id: + 'linux_anomalous_network_activity_ecs_record_1586274300000_900_0_-96106189301704594950079884115725560577_5', + type: 'event', + index: '.ml-anomalies-custom-linux_anomalous_network_activity_ecs', + depth: 0, + }, + ], + ancestors: [ + { + id: + 'linux_anomalous_network_activity_ecs_record_1586274300000_900_0_-96106189301704594950079884115725560577_5', + type: 'event', + index: '.ml-anomalies-custom-linux_anomalous_network_activity_ecs', + depth: 0, + }, + ], + status: 'open', + rule: { + id: createdRule.id, + rule_id: createdRule.rule_id, + created_at: createdRule.created_at, + updated_at: signal._source.signal.rule.updated_at, + actions: [], + interval: '5m', + name: 'Test ML rule', + tags: [], + enabled: true, + created_by: 'elastic', + updated_by: 'elastic', + throttle: null, + description: 'Test ML rule description', + risk_score: 50, + severity: 'critical', + output_index: '.siem-signals-default', + author: [], + false_positives: [], + from: '1900-01-01T00:00:00.000Z', + max_signals: 100, + risk_score_mapping: [], + severity_mapping: [], + threat: [], + to: 'now', + references: [], + version: 1, + exceptions_list: [], + immutable: false, + type: 'machine_learning', + anomaly_threshold: 30, + machine_learning_job_id: ['linux_anomalous_network_activity_ecs'], + }, + depth: 1, + parent: { + id: + 'linux_anomalous_network_activity_ecs_record_1586274300000_900_0_-96106189301704594950079884115725560577_5', + type: 'event', + index: '.ml-anomalies-custom-linux_anomalous_network_activity_ecs', + depth: 0, + }, + original_time: '2020-11-16T22:58:08.000Z', + }, + }); + }); + + it('should create 7 alerts from ML rule when records meet anomaly_threshold', async () => { + const rule: MachineLearningCreateSchema = { + ...testRule, + anomaly_threshold: 20, + }; + const createdRule = await createRule(supertest, rule); + const signalsOpen = await getOpenSignals(supertest, es, createdRule); + expect(signalsOpen.hits.hits.length).eql(7); + }); + describe('with non-value list exception', () => { + afterEach(async () => { + await deleteAllExceptions(es); + }); + it('generates no signals when an exception is added for an ML rule', async () => { + const createdRule = await createRuleWithExceptionEntries(supertest, testRule, [ + [ + { + field: 'host.name', + operator: 'included', + type: 'match', + value: 'mothra', + }, + ], + ]); + const signalsOpen = await getOpenSignals(supertest, es, createdRule); + expect(signalsOpen.hits.hits.length).equal(0); + }); + }); + + describe('with value list exception', () => { + beforeEach(async () => { + await createListsIndex(supertest); + }); + + afterEach(async () => { + await deleteListsIndex(supertest); + await deleteAllExceptions(es); + }); + + it('generates no signals when a value list exception is added for an ML rule', async () => { + const valueListId = 'value-list-id'; + await importFile(supertest, 'keyword', ['mothra'], valueListId); + const createdRule = await createRuleWithExceptionEntries(supertest, testRule, [ + [ + { + field: 'host.name', + operator: 'included', + type: 'list', + list: { + id: valueListId, + type: 'keyword', + }, + }, + ], + ]); + const signalsOpen = await getOpenSignals(supertest, es, createdRule); + expect(signalsOpen.hits.hits.length).equal(0); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts index b6d88b657f25c..57b24f6de2a48 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts @@ -19,6 +19,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => { loadTestFile(require.resolve('./create_rules')); loadTestFile(require.resolve('./create_rules_bulk')); loadTestFile(require.resolve('./create_index')); + loadTestFile(require.resolve('./create_ml')); loadTestFile(require.resolve('./create_threat_matching')); loadTestFile(require.resolve('./create_exceptions')); loadTestFile(require.resolve('./delete_rules')); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_actions.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_actions.ts index 257c6a4286982..12841b9072624 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_actions.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_actions.ts @@ -26,6 +26,7 @@ import { createNewAction, findImmutableRuleById, getPrePackagedRulesStatus, + getSimpleRuleOutput, } from '../../utils'; // eslint-disable-next-line import/no-default-export @@ -61,6 +62,21 @@ export default ({ getService }: FtrProviderContext) => { expect(bodyToCompare).to.eql(expected); }); + it('should be able to add a new webhook action and then remove the action from the rule again', async () => { + const hookAction = await createNewAction(supertest); + const rule = getSimpleRule(); + await createRule(supertest, rule); + const ruleToUpdate = getRuleWithWebHookAction(hookAction.id, false, rule); + await updateRule(supertest, ruleToUpdate); + const ruleAfterActionRemoved = await updateRule(supertest, rule); + const bodyToCompare = removeServerGeneratedProperties(ruleAfterActionRemoved); + const expected = { + ...getSimpleRuleOutput(), + version: 3, // version bump is required since this is an updated rule and this is part of the testing that we do bump the version number on update + }; + expect(bodyToCompare).to.eql(expected); + }); + it('should be able to create a new webhook action and attach it to a rule without a meta field and run it correctly', async () => { const hookAction = await createNewAction(supertest); const rule = getSimpleRule(); diff --git a/x-pack/test/detection_engine_api_integration/utils.ts b/x-pack/test/detection_engine_api_integration/utils.ts index 55011ec055190..0f57728f99d67 100644 --- a/x-pack/test/detection_engine_api_integration/utils.ts +++ b/x-pack/test/detection_engine_api_integration/utils.ts @@ -683,7 +683,7 @@ export const getWebHookAction = () => ({ export const getRuleWithWebHookAction = ( id: string, enabled = false, - rule?: QueryCreateSchema + rule?: CreateRulesSchema ): CreateRulesSchema | UpdateRulesSchema => { const finalRule = rule != null ? { ...rule, enabled } : getSimpleRule('rule-1', enabled); return { @@ -888,7 +888,7 @@ export const createNewAction = async (supertest: SuperTest { + await esArchiver.load('fleet/empty_fleet_server'); + }); + setupFleetAndAgents(providerContext); + + let defaultOutputId: string; + + before(async function () { + const { body: getOutputsRes } = await supertest.get(`/api/fleet/outputs`).expect(200); + + const defaultOutput = getOutputsRes.items.find((item: any) => item.is_default); + if (!defaultOutput) { + throw new Error('default output not set'); + } + + defaultOutputId = defaultOutput.id; + }); + + after(async () => { + await esArchiver.unload('fleet/empty_fleet_server'); + }); + + it('GET /outputs should list the default output', async () => { + const { body: getOutputsRes } = await supertest.get(`/api/fleet/outputs`).expect(200); + + expect(getOutputsRes.items.length).to.eql(1); + }); + + it('GET /outputs/{defaultOutputId} should return the default output', async () => { + const { body: getOutputRes } = await supertest + .get(`/api/fleet/outputs/${defaultOutputId}`) + .expect(200); + + expect(getOutputRes.item).to.have.keys('id', 'name', 'type', 'is_default', 'hosts'); + }); + + it('PUT /output/{defaultOutputId} should explicitly set port on ES hosts', async function () { + await supertest + .put(`/api/fleet/outputs/${defaultOutputId}`) + .set('kbn-xsrf', 'xxxx') + .send({ hosts: ['https://test.fr'] }) + .expect(200); + + const { body: getSettingsRes } = await supertest + .get(`/api/fleet/outputs/${defaultOutputId}`) + .expect(200); + expect(getSettingsRes.item.hosts).to.eql(['https://test.fr:443']); + }); + }); +} diff --git a/x-pack/test/fleet_api_integration/apis/outputs/index.js b/x-pack/test/fleet_api_integration/apis/outputs/index.js new file mode 100644 index 0000000000000..b799413638d47 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/outputs/index.js @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export default function loadTests({ loadTestFile }) { + describe('Output Endpoints', () => { + loadTestFile(require.resolve('./crud')); + }); +} diff --git a/x-pack/test/fleet_api_integration/apis/package_policy/update.ts b/x-pack/test/fleet_api_integration/apis/package_policy/update.ts index 5a0ff90669def..0956b6b0f515f 100644 --- a/x-pack/test/fleet_api_integration/apis/package_policy/update.ts +++ b/x-pack/test/fleet_api_integration/apis/package_policy/update.ts @@ -174,5 +174,42 @@ export default function (providerContext: FtrProviderContext) { }) .expect(500); }); + + it('should work with frozen input vars', async function () { + await supertest + .put(`/api/fleet/package_policies/${packagePolicyId}`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'filetest-1', + description: '', + namespace: 'updated_namespace', + policy_id: agentPolicyId, + enabled: true, + output_id: '', + inputs: [ + { + enabled: true, + type: 'test-input', + streams: [], + vars: { + frozen_var: { + type: 'text', + value: 'abc', + frozen: true, + }, + unfrozen_var: { + type: 'text', + value: 'def', + }, + }, + }, + ], + package: { + name: 'filetest', + title: 'For File Tests', + version: '0.1.0', + }, + }); + }); }); } diff --git a/x-pack/test/functional/apps/transform/cloning.ts b/x-pack/test/functional/apps/transform/cloning.ts index b736404eb529b..c36a89f6ecb50 100644 --- a/x-pack/test/functional/apps/transform/cloning.ts +++ b/x-pack/test/functional/apps/transform/cloning.ts @@ -32,7 +32,7 @@ function getTransformConfig(): TransformPivotConfig { aggregations: { 'products.base_price.avg': { avg: { field: 'products.base_price' } } }, }, description: - 'ecommerce batch transform with avg(products.base_price) grouped by terms(category.keyword)', + 'ecommerce batch transform with avg(products.base_price) grouped by terms(category)', frequency: '3s', settings: { max_page_search_size: 250, diff --git a/x-pack/test/functional/apps/transform/creation_index_pattern.ts b/x-pack/test/functional/apps/transform/creation_index_pattern.ts index 2c26a340a2a26..b01d5d8f66863 100644 --- a/x-pack/test/functional/apps/transform/creation_index_pattern.ts +++ b/x-pack/test/functional/apps/transform/creation_index_pattern.ts @@ -17,6 +17,7 @@ import { } from './index'; export default function ({ getService }: FtrProviderContext) { + const canvasElement = getService('canvasElement'); const esArchiver = getService('esArchiver'); const transform = getService('transform'); @@ -40,8 +41,8 @@ export default function ({ getService }: FtrProviderContext) { source: 'ft_ecommerce', groupByEntries: [ { - identifier: 'terms(category.keyword)', - label: 'category.keyword', + identifier: 'terms(category)', + label: 'category', } as GroupByEntry, { identifier: 'date_histogram(order_date)', @@ -85,16 +86,16 @@ export default function ({ getService }: FtrProviderContext) { ], transformId: `ec_1_${Date.now()}`, transformDescription: - 'ecommerce batch transform with groups terms(category.keyword) + date_histogram(order_date) 1m and aggregation avg(products.base_price)', + 'ecommerce batch transform with groups terms(category) + date_histogram(order_date) 1m and aggregation avg(products.base_price)', get destinationIndex(): string { return `user-${this.transformId}`; }, discoverAdjustSuperDatePicker: true, expected: { - pivotAdvancedEditorValueArr: ['{', ' "group_by": {', ' "category.keyword": {'], + pivotAdvancedEditorValueArr: ['{', ' "group_by": {', ' "category": {'], pivotAdvancedEditorValue: { group_by: { - 'category.keyword': { + category: { terms: { field: 'category.keyword', }, @@ -156,7 +157,15 @@ export default function ({ getService }: FtrProviderContext) { rows: 5, }, histogramCharts: [ - { chartAvailable: false, id: 'category', legend: 'Chart not supported.' }, + { + chartAvailable: true, + id: 'category', + legend: '6 categories', + colorStats: [ + { color: '#000000', percentage: 45 }, + { color: '#54B399', percentage: 55 }, + ], + }, { chartAvailable: true, id: 'currency', @@ -166,8 +175,24 @@ export default function ({ getService }: FtrProviderContext) { { color: '#54B399', percentage: 90 }, ], }, - { chartAvailable: false, id: 'customer_first_name', legend: 'Chart not supported.' }, - { chartAvailable: false, id: 'customer_full_name', legend: 'Chart not supported.' }, + { + chartAvailable: true, + id: 'customer_first_name', + legend: 'top 20 of 46 categories', + colorStats: [ + { color: '#000000', percentage: 60 }, + { color: '#54B399', percentage: 35 }, + ], + }, + { + chartAvailable: true, + id: 'customer_full_name', + legend: 'top 20 of 3321 categories', + colorStats: [ + { color: '#000000', percentage: 25 }, + { color: '#54B399', percentage: 67 }, + ], + }, { chartAvailable: true, id: 'customer_gender', @@ -186,7 +211,15 @@ export default function ({ getService }: FtrProviderContext) { { color: '#000000', percentage: 60 }, ], }, - { chartAvailable: false, id: 'customer_last_name', legend: 'Chart not supported.' }, + { + chartAvailable: true, + id: 'customer_last_name', + legend: 'top 20 of 183 categories', + colorStats: [ + { color: '#000000', percentage: 25 }, + { color: '#54B399', percentage: 70 }, + ], + }, { chartAvailable: true, id: 'customer_phone', @@ -403,6 +436,9 @@ export default function ({ getService }: FtrProviderContext) { await transform.wizard.assertAdvancedQueryEditorSwitchExists(); await transform.wizard.assertAdvancedQueryEditorSwitchCheckState(false); + // Disable anti-aliasing to stabilize canvas image rendering assertions + await canvasElement.disableAntiAliasing(); + await transform.testExecution.logTestStep('enables the index preview histogram charts'); await transform.wizard.enableIndexPreviewHistogramCharts(true); @@ -415,6 +451,8 @@ export default function ({ getService }: FtrProviderContext) { ); } + await canvasElement.resetAntiAliasing(); + if (isPivotTransformTestData(testData)) { await transform.testExecution.logTestStep('adds the group by entries'); for (const [index, entry] of testData.groupByEntries.entries()) { diff --git a/x-pack/test/functional/apps/transform/creation_runtime_mappings.ts b/x-pack/test/functional/apps/transform/creation_runtime_mappings.ts index 030748026af91..6add0a3ca5408 100644 --- a/x-pack/test/functional/apps/transform/creation_runtime_mappings.ts +++ b/x-pack/test/functional/apps/transform/creation_runtime_mappings.ts @@ -53,7 +53,15 @@ export default function ({ getService }: FtrProviderContext) { chartAvailable: true, id: '@timestamp', }, - { chartAvailable: false, id: '@version', legend: 'Chart not supported.' }, + { + chartAvailable: true, + id: '@version', + legend: '1 category', + colorStats: [ + { color: '#000000', percentage: 10 }, + { color: '#54B399', percentage: 90 }, + ], + }, { chartAvailable: true, id: 'airline', @@ -67,7 +75,8 @@ export default function ({ getService }: FtrProviderContext) { chartAvailable: true, id: 'responsetime', colorStats: [ - { color: '#54B399', percentage: 5 }, + // below 10% threshold + // { color: '#54B399', percentage: 5 }, { color: '#000000', percentage: 95 }, ], }, @@ -84,11 +93,20 @@ export default function ({ getService }: FtrProviderContext) { chartAvailable: true, id: 'rt_responsetime_x_2', colorStats: [ - { color: '#54B399', percentage: 5 }, + // below 10% threshold + // { color: '#54B399', percentage: 5 }, { color: '#000000', percentage: 95 }, ], }, - { chartAvailable: false, id: 'type', legend: 'Chart not supported.' }, + { + chartAvailable: true, + id: 'type', + legend: '1 category', + colorStats: [ + { color: '#000000', percentage: 10 }, + { color: '#54B399', percentage: 90 }, + ], + }, ]; const testDataList: Array = [ diff --git a/x-pack/test/functional/apps/transform/editing.ts b/x-pack/test/functional/apps/transform/editing.ts index 1f0bb058bdc38..0fc698b0acd86 100644 --- a/x-pack/test/functional/apps/transform/editing.ts +++ b/x-pack/test/functional/apps/transform/editing.ts @@ -20,7 +20,7 @@ function getTransformConfig(): TransformPivotConfig { aggregations: { 'products.base_price.avg': { avg: { field: 'products.base_price' } } }, }, description: - 'ecommerce batch transform with avg(products.base_price) grouped by terms(category.keyword)', + 'ecommerce batch transform with avg(products.base_price) grouped by terms(category)', dest: { index: `user-ec_2_${date}` }, }; } diff --git a/x-pack/test/functional/es_archives/dashboard/feature_controls/security/mappings.json b/x-pack/test/functional/es_archives/dashboard/feature_controls/security/mappings.json index ebb5b19387faf..c66a45084441f 100644 --- a/x-pack/test/functional/es_archives/dashboard/feature_controls/security/mappings.json +++ b/x-pack/test/functional/es_archives/dashboard/feature_controls/security/mappings.json @@ -161,9 +161,9 @@ }, "map" : { "properties" : { - "bounds" : { - "type" : "geo_shape", - "tree" : "quadtree" + "bounds": { + "dynamic": false, + "properties": {} }, "description" : { "type" : "text" diff --git a/x-pack/test/functional/es_archives/dashboard/feature_controls/spaces/mappings.json b/x-pack/test/functional/es_archives/dashboard/feature_controls/spaces/mappings.json index 12cdc07e1d478..dafef0fa9c2c7 100644 --- a/x-pack/test/functional/es_archives/dashboard/feature_controls/spaces/mappings.json +++ b/x-pack/test/functional/es_archives/dashboard/feature_controls/spaces/mappings.json @@ -161,9 +161,9 @@ }, "gis-map" : { "properties" : { - "bounds" : { - "type" : "geo_shape", - "tree" : "quadtree" + "bounds": { + "dynamic": false, + "properties": {} }, "description" : { "type" : "text" diff --git a/x-pack/test/functional/es_archives/data/search_sessions/data.json.gz b/x-pack/test/functional/es_archives/data/search_sessions/data.json.gz index fff020036a8e3..9528149d3c0f9 100644 Binary files a/x-pack/test/functional/es_archives/data/search_sessions/data.json.gz and b/x-pack/test/functional/es_archives/data/search_sessions/data.json.gz differ diff --git a/x-pack/test/functional/es_archives/discover/feature_controls/security/mappings.json b/x-pack/test/functional/es_archives/discover/feature_controls/security/mappings.json index 6df7a19959b29..5e01af673ef2f 100644 --- a/x-pack/test/functional/es_archives/discover/feature_controls/security/mappings.json +++ b/x-pack/test/functional/es_archives/discover/feature_controls/security/mappings.json @@ -161,9 +161,9 @@ }, "gis-map" : { "properties" : { - "bounds" : { - "type" : "geo_shape", - "tree" : "quadtree" + "bounds": { + "dynamic": false, + "properties": {} }, "description" : { "type" : "text" diff --git a/x-pack/test/functional/es_archives/discover/feature_controls/spaces/mappings.json b/x-pack/test/functional/es_archives/discover/feature_controls/spaces/mappings.json index 12cdc07e1d478..dafef0fa9c2c7 100644 --- a/x-pack/test/functional/es_archives/discover/feature_controls/spaces/mappings.json +++ b/x-pack/test/functional/es_archives/discover/feature_controls/spaces/mappings.json @@ -161,9 +161,9 @@ }, "gis-map" : { "properties" : { - "bounds" : { - "type" : "geo_shape", - "tree" : "quadtree" + "bounds": { + "dynamic": false, + "properties": {} }, "description" : { "type" : "text" diff --git a/x-pack/test/functional/es_archives/fleet/agents/mappings.json b/x-pack/test/functional/es_archives/fleet/agents/mappings.json index 72a7368e4d0a8..e9a3a965c0523 100644 --- a/x-pack/test/functional/es_archives/fleet/agents/mappings.json +++ b/x-pack/test/functional/es_archives/fleet/agents/mappings.json @@ -51,7 +51,6 @@ "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231", "alert": "7b44fba6773e37c806ce290ea9b7024e", "siem-detection-engine-rule-status": "ae783f41c6937db6b7a2ef5c93a9e9b0", - "map": "23d7aa4a720d4938ccde3983f87bd58d", "uptime-dynamic-settings": "fcdb453a30092f022f2642db29523d80", "cases": "32aa96a6d3855ddda53010ae2048ac22", "apm-telemetry": "3525d7c22c42bc80f5e6e9cb3f2b26a2", @@ -2128,7 +2127,8 @@ "map": { "properties": { "bounds": { - "type": "geo_shape" + "dynamic": false, + "properties": {} }, "description": { "type": "text" diff --git a/x-pack/test/functional/es_archives/hybrid/kibana/mappings.json b/x-pack/test/functional/es_archives/hybrid/kibana/mappings.json index 5256e29956f4f..a352155899e79 100644 --- a/x-pack/test/functional/es_archives/hybrid/kibana/mappings.json +++ b/x-pack/test/functional/es_archives/hybrid/kibana/mappings.json @@ -22,7 +22,6 @@ "index-pattern": "66eccb05066c5a89924f48a9e9736499", "infrastructure-ui-source": "ddc0ecb18383f6b26101a2fadb2dab0c", "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", - "map": "23d7aa4a720d4938ccde3983f87bd58d", "maps-telemetry": "a4229f8b16a6820c6d724b7e0c1f729d", "migrationVersion": "4a1746014a75ade3a714e1db5763276f", "ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9", @@ -437,7 +436,8 @@ "map": { "properties": { "bounds": { - "type": "geo_shape" + "dynamic": false, + "properties": {} }, "description": { "type": "text" diff --git a/x-pack/test/functional/es_archives/infra/metrics_anomalies/data.json.gz b/x-pack/test/functional/es_archives/infra/metrics_anomalies/data.json.gz index 68ad6965d0fb1..7026c69407c7a 100644 Binary files a/x-pack/test/functional/es_archives/infra/metrics_anomalies/data.json.gz and b/x-pack/test/functional/es_archives/infra/metrics_anomalies/data.json.gz differ diff --git a/x-pack/test/functional/es_archives/infra/metrics_anomalies/mappings.json b/x-pack/test/functional/es_archives/infra/metrics_anomalies/mappings.json index 82f4b49201ef1..20984d5fce540 100644 --- a/x-pack/test/functional/es_archives/infra/metrics_anomalies/mappings.json +++ b/x-pack/test/functional/es_archives/infra/metrics_anomalies/mappings.json @@ -1,719 +1,3 @@ -{ - "type": "index", - "value": { - "aliases": { - }, - "index": ".ds-metrics-kubernetes.pod-slingshot-2021.04.23-000001", - "mappings": { - "_data_stream_timestamp": { - "enabled": true - }, - "date_detection": false, - "dynamic_templates": [ - { - "strings_as_keyword": { - "mapping": { - "ignore_above": 1024, - "type": "keyword" - }, - "match_mapping_type": "string" - } - } - ], - "properties": { - "@timestamp": { - "type": "date" - }, - "agent": { - "properties": { - "ephemeral_id": { - "ignore_above": 1024, - "type": "keyword" - }, - "hostname": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "cloud": { - "properties": { - "instance": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "provider": { - "ignore_above": 1024, - "type": "keyword" - }, - "region": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "data_stream": { - "properties": { - "dataset": { - "type": "constant_keyword" - }, - "namespace": { - "type": "constant_keyword" - }, - "type": { - "type": "constant_keyword", - "value": "metrics" - } - } - }, - "ecs": { - "properties": { - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "event": { - "properties": { - "dataset": { - "ignore_above": 1024, - "type": "keyword" - }, - "duration": { - "ignore_above": 1024, - "type": "keyword" - }, - "module": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "host": { - "properties": { - "ip": { - "type": "ip" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "kubernetes": { - "properties": { - "namespace": { - "ignore_above": 1024, - "type": "keyword" - }, - "node": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "pod": { - "properties": { - "cpu": { - "properties": { - "usage": { - "properties": { - "limit": { - "properties": { - "pct": { - "type": "long" - } - } - }, - "node": { - "properties": { - "pct": { - "type": "float" - } - } - } - } - } - } - }, - "host_ip": { - "ignore_above": 1024, - "type": "keyword" - }, - "ip": { - "ignore_above": 1024, - "type": "keyword" - }, - "memory": { - "properties": { - "usage": { - "properties": { - "limit": { - "properties": { - "pct": { - "type": "float" - } - } - }, - "node": { - "properties": { - "pct": { - "type": "float" - } - } - } - } - } - } - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "network": { - "properties": { - "in": { - "properties": { - "bytes": { - "type": "float" - }, - "errors": { - "type": "float" - } - } - }, - "out": { - "properties": { - "bytes": { - "type": "float" - }, - "errors": { - "type": "float" - } - } - } - } - }, - "status": { - "properties": { - "phase": { - "ignore_above": 1024, - "type": "keyword" - }, - "ready": { - "type": "boolean" - }, - "scheduled": { - "type": "boolean" - } - } - }, - "uid": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "metricset": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "period": { - "type": "long" - } - } - }, - "service": { - "properties": { - "address": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "settings": { - "index": { - "codec": "best_compression", - "hidden": "true", - "lifecycle": { - "name": "metrics" - }, - "number_of_replicas": "1", - "number_of_shards": "1" - } - } - } -} - -{ - "type": "index", - "value": { - "aliases": { - }, - "index": ".ds-metrics-system-slingshot-2021.04.23-000001", - "mappings": { - "_data_stream_timestamp": { - "enabled": true - }, - "date_detection": false, - "dynamic_templates": [ - { - "strings_as_keyword": { - "mapping": { - "ignore_above": 1024, - "type": "keyword" - }, - "match_mapping_type": "string" - } - } - ], - "properties": { - "@timestamp": { - "type": "date" - }, - "agent": { - "properties": { - "ephemeral_id": { - "ignore_above": 1024, - "type": "keyword" - }, - "hostname": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "cloud": { - "properties": { - "instance": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "provider": { - "ignore_above": 1024, - "type": "keyword" - }, - "region": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "data_stream": { - "properties": { - "dataset": { - "type": "constant_keyword" - }, - "namespace": { - "type": "constant_keyword" - }, - "type": { - "type": "constant_keyword", - "value": "metrics" - } - } - }, - "ecs": { - "properties": { - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "event": { - "properties": { - "dataset": { - "ignore_above": 1024, - "type": "keyword" - }, - "duration": { - "ignore_above": 1024, - "type": "keyword" - }, - "module": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "host": { - "properties": { - "architecture": { - "ignore_above": 1024, - "type": "keyword" - }, - "cpu": { - "properties": { - "pct": { - "type": "float" - } - } - }, - "hostname": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "ip": { - "type": "ip" - }, - "mac": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "network": { - "properties": { - "egress": { - "properties": { - "bytes": { - "type": "float" - } - } - }, - "in": { - "properties": { - "bytes": { - "type": "float" - } - } - }, - "ingress": { - "properties": { - "bytes": { - "type": "float" - } - } - }, - "out": { - "properties": { - "bytes": { - "type": "float" - } - } - } - } - }, - "os": { - "properties": { - "platform": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "metricset": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "period": { - "type": "long" - } - } - }, - "service": { - "properties": { - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "system": { - "properties": { - "cpu": { - "properties": { - "cores": { - "type": "long" - }, - "idle": { - "properties": { - "norm": { - "properties": { - "pct": { - "type": "float" - } - } - }, - "pct": { - "type": "float" - } - } - }, - "nice": { - "properties": { - "norm": { - "properties": { - "pct": { - "type": "long" - } - } - }, - "pct": { - "type": "long" - } - } - }, - "system": { - "properties": { - "norm": { - "properties": { - "pct": { - "type": "float" - } - } - }, - "pct": { - "type": "float" - } - } - }, - "total": { - "properties": { - "norm": { - "properties": { - "pct": { - "type": "float" - } - } - }, - "pct": { - "type": "float" - } - } - }, - "user": { - "properties": { - "norm": { - "properties": { - "pct": { - "type": "float" - } - } - }, - "pct": { - "type": "float" - } - } - } - } - }, - "load": { - "properties": { - "1": { - "type": "float" - }, - "15": { - "type": "float" - }, - "5": { - "type": "float" - }, - "norm": { - "properties": { - "1": { - "type": "float" - }, - "15": { - "type": "float" - }, - "5": { - "type": "float" - } - } - } - } - }, - "memory": { - "properties": { - "actual": { - "properties": { - "free": { - "type": "long" - }, - "used": { - "properties": { - "bytes": { - "type": "long" - }, - "pct": { - "type": "float" - } - } - } - } - }, - "free": { - "type": "long" - }, - "swap": { - "properties": { - "free": { - "type": "long" - }, - "total": { - "type": "long" - }, - "used": { - "properties": { - "bytes": { - "type": "long" - }, - "pct": { - "type": "float" - } - } - } - } - }, - "total": { - "type": "long" - }, - "used": { - "properties": { - "bytes": { - "type": "long" - }, - "pct": { - "type": "float" - } - } - } - } - }, - "network": { - "properties": { - "in": { - "properties": { - "bytes": { - "type": "float" - }, - "dropped": { - "type": "long" - }, - "errors": { - "type": "long" - }, - "packets": { - "type": "float" - } - } - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "out": { - "properties": { - "bytes": { - "type": "float" - }, - "dropped": { - "type": "float" - }, - "errors": { - "type": "float" - }, - "packets": { - "type": "float" - } - } - } - } - }, - "uptime": { - "properties": { - "duration": { - "properties": { - "ms": { - "type": "long" - } - } - } - } - } - } - } - } - }, - "settings": { - "index": { - "codec": "best_compression", - "hidden": "true", - "lifecycle": { - "name": "metrics" - }, - "number_of_replicas": "1", - "number_of_shards": "1" - } - } - } -} - { "type": "index", "value": { diff --git a/x-pack/test/functional/es_archives/kibana_scripted_fields_on_logstash/mappings.json b/x-pack/test/functional/es_archives/kibana_scripted_fields_on_logstash/mappings.json index 1816ebfc11891..c0ce939234a9d 100644 --- a/x-pack/test/functional/es_archives/kibana_scripted_fields_on_logstash/mappings.json +++ b/x-pack/test/functional/es_archives/kibana_scripted_fields_on_logstash/mappings.json @@ -32,7 +32,6 @@ "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", "lens": "21c3ea0763beb1ecb0162529706b88c5", "lens-ui-telemetry": "509bfa5978586998e05f9e303c07a327", - "map": "23d7aa4a720d4938ccde3983f87bd58d", "maps-telemetry": "268da3a48066123fc5baf35abaa55014", "metrics-explorer-view": "53c5365793677328df0ccb6138bf3cdd", "migrationVersion": "4a1746014a75ade3a714e1db5763276f", @@ -1801,7 +1800,8 @@ "map": { "properties": { "bounds": { - "type": "geo_shape" + "dynamic": false, + "properties": {} }, "description": { "type": "text" @@ -2760,4 +2760,4 @@ } } } -} \ No newline at end of file +} diff --git a/x-pack/test/functional/es_archives/lens/basic/mappings.json b/x-pack/test/functional/es_archives/lens/basic/mappings.json index f2a29f022ff5e..50230a5eff0c2 100644 --- a/x-pack/test/functional/es_archives/lens/basic/mappings.json +++ b/x-pack/test/functional/es_archives/lens/basic/mappings.json @@ -21,7 +21,6 @@ "inventory-view": "84b320fd67209906333ffce261128462", "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", "lens": "21c3ea0763beb1ecb0162529706b88c5", - "map": "23d7aa4a720d4938ccde3983f87bd58d", "maps-telemetry": "a4229f8b16a6820c6d724b7e0c1f729d", "metrics-explorer-view": "53c5365793677328df0ccb6138bf3cdd", "migrationVersion": "4a1746014a75ade3a714e1db5763276f", @@ -310,8 +309,8 @@ "gis-map": { "properties": { "bounds": { - "strategy": "recursive", - "type": "geo_shape" + "dynamic": false, + "properties": {} }, "description": { "type": "text" @@ -558,7 +557,8 @@ "map": { "properties": { "bounds": { - "type": "geo_shape" + "dynamic": false, + "properties": {} }, "description": { "type": "text" diff --git a/x-pack/test/functional/es_archives/lens/reporting/mappings.json b/x-pack/test/functional/es_archives/lens/reporting/mappings.json index 8b8e5a0e6e7f6..a71efbd3e59ec 100644 --- a/x-pack/test/functional/es_archives/lens/reporting/mappings.json +++ b/x-pack/test/functional/es_archives/lens/reporting/mappings.json @@ -21,7 +21,6 @@ "inventory-view": "84b320fd67209906333ffce261128462", "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", "lens": "21c3ea0763beb1ecb0162529706b88c5", - "map": "23d7aa4a720d4938ccde3983f87bd58d", "maps-telemetry": "a4229f8b16a6820c6d724b7e0c1f729d", "metrics-explorer-view": "53c5365793677328df0ccb6138bf3cdd", "migrationVersion": "4a1746014a75ade3a714e1db5763276f", @@ -310,8 +309,8 @@ "gis-map": { "properties": { "bounds": { - "strategy": "recursive", - "type": "geo_shape" + "dynamic": false, + "properties": {} }, "description": { "type": "text" @@ -558,7 +557,8 @@ "map": { "properties": { "bounds": { - "type": "geo_shape" + "dynamic": false, + "properties": {} }, "description": { "type": "text" diff --git a/x-pack/test/functional/es_archives/lens/rollup/config/mappings.json b/x-pack/test/functional/es_archives/lens/rollup/config/mappings.json index f2a29f022ff5e..50230a5eff0c2 100644 --- a/x-pack/test/functional/es_archives/lens/rollup/config/mappings.json +++ b/x-pack/test/functional/es_archives/lens/rollup/config/mappings.json @@ -21,7 +21,6 @@ "inventory-view": "84b320fd67209906333ffce261128462", "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", "lens": "21c3ea0763beb1ecb0162529706b88c5", - "map": "23d7aa4a720d4938ccde3983f87bd58d", "maps-telemetry": "a4229f8b16a6820c6d724b7e0c1f729d", "metrics-explorer-view": "53c5365793677328df0ccb6138bf3cdd", "migrationVersion": "4a1746014a75ade3a714e1db5763276f", @@ -310,8 +309,8 @@ "gis-map": { "properties": { "bounds": { - "strategy": "recursive", - "type": "geo_shape" + "dynamic": false, + "properties": {} }, "description": { "type": "text" @@ -558,7 +557,8 @@ "map": { "properties": { "bounds": { - "type": "geo_shape" + "dynamic": false, + "properties": {} }, "description": { "type": "text" diff --git a/x-pack/test/functional/es_archives/maps/kibana/mappings.json b/x-pack/test/functional/es_archives/maps/kibana/mappings.json index f370d4d5fe233..4786fb1f30f43 100644 --- a/x-pack/test/functional/es_archives/maps/kibana/mappings.json +++ b/x-pack/test/functional/es_archives/maps/kibana/mappings.json @@ -178,8 +178,8 @@ "map": { "properties": { "bounds": { - "tree": "quadtree", - "type": "geo_shape" + "dynamic": false, + "properties": {} }, "description": { "type": "text" diff --git a/x-pack/test/functional/es_archives/reporting/nanos/mappings.json b/x-pack/test/functional/es_archives/reporting/nanos/mappings.json index dd717387a2643..86451ee4303af 100644 --- a/x-pack/test/functional/es_archives/reporting/nanos/mappings.json +++ b/x-pack/test/functional/es_archives/reporting/nanos/mappings.json @@ -21,7 +21,6 @@ "index-pattern": "66eccb05066c5a89924f48a9e9736499", "infrastructure-ui-source": "ddc0ecb18383f6b26101a2fadb2dab0c", "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", - "map": "23d7aa4a720d4938ccde3983f87bd58d", "maps-telemetry": "a4229f8b16a6820c6d724b7e0c1f729d", "migrationVersion": "4a1746014a75ade3a714e1db5763276f", "ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9", @@ -419,7 +418,8 @@ "map": { "properties": { "bounds": { - "type": "geo_shape" + "dynamic": false, + "properties": {} }, "description": { "type": "text" diff --git a/x-pack/test/functional/es_archives/saved_objects_management/feature_controls/security/mappings.json b/x-pack/test/functional/es_archives/saved_objects_management/feature_controls/security/mappings.json index 96e6b7c0a19f1..6a416126d7f26 100644 --- a/x-pack/test/functional/es_archives/saved_objects_management/feature_controls/security/mappings.json +++ b/x-pack/test/functional/es_archives/saved_objects_management/feature_controls/security/mappings.json @@ -162,8 +162,8 @@ "map": { "properties": { "bounds": { - "type": "geo_shape", - "tree": "quadtree" + "dynamic": false, + "properties": {} }, "description": { "type": "text" @@ -456,4 +456,4 @@ } } } -} \ No newline at end of file +} diff --git a/x-pack/test/functional/es_archives/security/flstest/kibana/mappings.json b/x-pack/test/functional/es_archives/security/flstest/kibana/mappings.json index c7e786b20ac19..fb2ed91f3ce14 100644 --- a/x-pack/test/functional/es_archives/security/flstest/kibana/mappings.json +++ b/x-pack/test/functional/es_archives/security/flstest/kibana/mappings.json @@ -175,8 +175,8 @@ "map": { "properties": { "bounds": { - "tree": "quadtree", - "type": "geo_shape" + "dynamic": false, + "properties": {} }, "description": { "type": "text" diff --git a/x-pack/test/functional/es_archives/security_solution/anomalies/data.json.gz b/x-pack/test/functional/es_archives/security_solution/anomalies/data.json.gz new file mode 100644 index 0000000000000..5ef29f6813768 Binary files /dev/null and b/x-pack/test/functional/es_archives/security_solution/anomalies/data.json.gz differ diff --git a/x-pack/test/functional/es_archives/security_solution/anomalies/mappings.json b/x-pack/test/functional/es_archives/security_solution/anomalies/mappings.json new file mode 100644 index 0000000000000..484e0f3fc9aa0 --- /dev/null +++ b/x-pack/test/functional/es_archives/security_solution/anomalies/mappings.json @@ -0,0 +1,1176 @@ +{ + "type": "index", + "value": { + "aliases": { + ".ml-anomalies-.write-linux_anomalous_network_activity_ecs": { + "is_hidden": true + }, + ".ml-anomalies-linux_anomalous_network_activity_ecs": { + "filter": { + "term": { + "job_id": { + "boost": 1, + "value": "linux_anomalous_network_activity_ecs" + } + } + }, + "is_hidden": true + } + }, + "index": ".ml-anomalies-custom-linux_anomalous_network_activity_ecs", + "mappings": { + "_meta": { + "version": "8.0.0" + }, + "dynamic_templates": [ + { + "strings_as_keywords": { + "mapping": { + "type": "keyword" + }, + "match": "*" + } + } + ], + "properties": { + "actual": { + "type": "double" + }, + "all_field_values": { + "analyzer": "whitespace", + "type": "text" + }, + "anomaly_score": { + "type": "double" + }, + "average_bucket_processing_time_ms": { + "type": "double" + }, + "bucket_allocation_failures_count": { + "type": "long" + }, + "bucket_count": { + "type": "long" + }, + "bucket_influencers": { + "properties": { + "anomaly_score": { + "type": "double" + }, + "bucket_span": { + "type": "long" + }, + "influencer_field_name": { + "type": "keyword" + }, + "initial_anomaly_score": { + "type": "double" + }, + "is_interim": { + "type": "boolean" + }, + "job_id": { + "type": "keyword" + }, + "probability": { + "type": "double" + }, + "raw_anomaly_score": { + "type": "double" + }, + "result_type": { + "type": "keyword" + }, + "timestamp": { + "type": "date" + } + }, + "type": "nested" + }, + "bucket_span": { + "type": "long" + }, + "by_field_name": { + "type": "keyword" + }, + "by_field_value": { + "copy_to": [ + "all_field_values" + ], + "type": "keyword" + }, + "categorization_status": { + "type": "keyword" + }, + "categorized_doc_count": { + "type": "keyword" + }, + "category_id": { + "type": "long" + }, + "causes": { + "properties": { + "actual": { + "type": "double" + }, + "by_field_name": { + "type": "keyword" + }, + "by_field_value": { + "copy_to": [ + "all_field_values" + ], + "type": "keyword" + }, + "correlated_by_field_value": { + "copy_to": [ + "all_field_values" + ], + "type": "keyword" + }, + "field_name": { + "type": "keyword" + }, + "function": { + "type": "keyword" + }, + "function_description": { + "type": "keyword" + }, + "geo_results": { + "properties": { + "actual_point": { + "type": "geo_point" + }, + "typical_point": { + "type": "geo_point" + } + } + }, + "over_field_name": { + "type": "keyword" + }, + "over_field_value": { + "copy_to": [ + "all_field_values" + ], + "type": "keyword" + }, + "partition_field_name": { + "type": "keyword" + }, + "partition_field_value": { + "copy_to": [ + "all_field_values" + ], + "type": "keyword" + }, + "probability": { + "type": "double" + }, + "typical": { + "type": "double" + } + }, + "type": "nested" + }, + "dead_category_count": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "destination": { + "properties": { + "ip": { + "type": "keyword" + } + } + }, + "detector_index": { + "type": "integer" + }, + "earliest_record_timestamp": { + "type": "date" + }, + "empty_bucket_count": { + "type": "long" + }, + "event_count": { + "type": "long" + }, + "examples": { + "type": "text" + }, + "exponential_average_bucket_processing_time_ms": { + "type": "double" + }, + "exponential_average_calculation_context": { + "properties": { + "incremental_metric_value_ms": { + "type": "double" + }, + "latest_timestamp": { + "type": "date" + }, + "previous_exponential_average_ms": { + "type": "double" + } + } + }, + "field_name": { + "type": "keyword" + }, + "forecast_create_timestamp": { + "type": "date" + }, + "forecast_end_timestamp": { + "type": "date" + }, + "forecast_expiry_timestamp": { + "type": "date" + }, + "forecast_id": { + "type": "keyword" + }, + "forecast_lower": { + "type": "double" + }, + "forecast_memory_bytes": { + "type": "long" + }, + "forecast_messages": { + "type": "keyword" + }, + "forecast_prediction": { + "type": "double" + }, + "forecast_progress": { + "type": "double" + }, + "forecast_start_timestamp": { + "type": "date" + }, + "forecast_status": { + "type": "keyword" + }, + "forecast_upper": { + "type": "double" + }, + "frequent_category_count": { + "type": "keyword" + }, + "function": { + "type": "keyword" + }, + "function_description": { + "type": "keyword" + }, + "geo_results": { + "properties": { + "actual_point": { + "type": "geo_point" + }, + "typical_point": { + "type": "geo_point" + } + } + }, + "host": { + "properties": { + "name": { + "type": "keyword" + } + } + }, + "influencer_field_name": { + "type": "keyword" + }, + "influencer_field_value": { + "copy_to": [ + "all_field_values" + ], + "type": "keyword" + }, + "influencer_score": { + "type": "double" + }, + "influencers": { + "properties": { + "influencer_field_name": { + "type": "keyword" + }, + "influencer_field_values": { + "copy_to": [ + "all_field_values" + ], + "type": "keyword" + } + }, + "type": "nested" + }, + "initial_anomaly_score": { + "type": "double" + }, + "initial_influencer_score": { + "type": "double" + }, + "initial_record_score": { + "type": "double" + }, + "input_bytes": { + "type": "long" + }, + "input_field_count": { + "type": "long" + }, + "input_record_count": { + "type": "long" + }, + "invalid_date_count": { + "type": "long" + }, + "is_interim": { + "type": "boolean" + }, + "job_id": { + "copy_to": [ + "all_field_values" + ], + "type": "keyword" + }, + "last_data_time": { + "type": "date" + }, + "latest_empty_bucket_timestamp": { + "type": "date" + }, + "latest_record_time_stamp": { + "type": "date" + }, + "latest_record_timestamp": { + "type": "date" + }, + "latest_result_time_stamp": { + "type": "date" + }, + "latest_sparse_bucket_timestamp": { + "type": "date" + }, + "log_time": { + "type": "date" + }, + "max_matching_length": { + "type": "long" + }, + "maximum_bucket_processing_time_ms": { + "type": "double" + }, + "memory_status": { + "type": "keyword" + }, + "min_version": { + "type": "keyword" + }, + "minimum_bucket_processing_time_ms": { + "type": "double" + }, + "missing_field_count": { + "type": "long" + }, + "model_bytes": { + "type": "long" + }, + "model_bytes_exceeded": { + "type": "keyword" + }, + "model_bytes_memory_limit": { + "type": "keyword" + }, + "model_feature": { + "type": "keyword" + }, + "model_lower": { + "type": "double" + }, + "model_median": { + "type": "double" + }, + "model_size_stats": { + "properties": { + "bucket_allocation_failures_count": { + "type": "long" + }, + "categorization_status": { + "type": "keyword" + }, + "categorized_doc_count": { + "type": "keyword" + }, + "dead_category_count": { + "type": "keyword" + }, + "frequent_category_count": { + "type": "keyword" + }, + "job_id": { + "type": "keyword" + }, + "log_time": { + "type": "date" + }, + "memory_status": { + "type": "keyword" + }, + "model_bytes": { + "type": "long" + }, + "model_bytes_exceeded": { + "type": "keyword" + }, + "model_bytes_memory_limit": { + "type": "keyword" + }, + "rare_category_count": { + "type": "keyword" + }, + "result_type": { + "type": "keyword" + }, + "timestamp": { + "type": "date" + }, + "total_by_field_count": { + "type": "long" + }, + "total_category_count": { + "type": "keyword" + }, + "total_over_field_count": { + "type": "long" + }, + "total_partition_field_count": { + "type": "long" + } + } + }, + "model_upper": { + "type": "double" + }, + "multi_bucket_impact": { + "type": "double" + }, + "out_of_order_timestamp_count": { + "type": "long" + }, + "over_field_name": { + "type": "keyword" + }, + "over_field_value": { + "copy_to": [ + "all_field_values" + ], + "type": "keyword" + }, + "partition_field_name": { + "type": "keyword" + }, + "partition_field_value": { + "copy_to": [ + "all_field_values" + ], + "type": "keyword" + }, + "probability": { + "type": "double" + }, + "process": { + "properties": { + "name": { + "type": "keyword" + } + } + }, + "processed_field_count": { + "type": "long" + }, + "processed_record_count": { + "type": "long" + }, + "processing_time_ms": { + "type": "long" + }, + "quantiles": { + "enabled": false, + "type": "object" + }, + "rare_category_count": { + "type": "keyword" + }, + "raw_anomaly_score": { + "type": "double" + }, + "record_score": { + "type": "double" + }, + "regex": { + "type": "keyword" + }, + "result_type": { + "type": "keyword" + }, + "retain": { + "type": "boolean" + }, + "scheduled_events": { + "type": "keyword" + }, + "search_count": { + "type": "long" + }, + "snapshot_doc_count": { + "type": "integer" + }, + "snapshot_id": { + "type": "keyword" + }, + "sparse_bucket_count": { + "type": "long" + }, + "terms": { + "type": "text" + }, + "timestamp": { + "type": "date" + }, + "total_by_field_count": { + "type": "long" + }, + "total_category_count": { + "type": "keyword" + }, + "total_over_field_count": { + "type": "long" + }, + "total_partition_field_count": { + "type": "long" + }, + "total_search_time_ms": { + "type": "double" + }, + "typical": { + "type": "double" + }, + "user": { + "properties": { + "name": { + "type": "keyword" + } + } + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "1", + "number_of_shards": "1", + "query": { + "default_field": "all_field_values" + }, + "translog": { + "durability": "async" + }, + "unassigned": { + "node_left": { + "delayed_timeout": "1m" + } + } + } + } + } +} + +{ + "type": "index", + "value": { + "aliases": { + ".ml-anomalies-.write-rare_process_by_host_linux_ecs": { + }, + ".ml-anomalies-rare_process_by_host_linux_ecs": { + "filter": { + "term": { + "job_id": { + "boost": 1, + "value": "rare_process_by_host_linux_ecs" + } + } + } + } + }, + "index": ".ml-anomalies-custom-rare_process_by_host_linux_ecs", + "mappings": { + "_meta": { + "version": "8.0.0" + }, + "dynamic_templates": [ + { + "strings_as_keywords": { + "mapping": { + "type": "keyword" + }, + "match": "*" + } + } + ], + "properties": { + "actual": { + "type": "double" + }, + "all_field_values": { + "analyzer": "whitespace", + "type": "text" + }, + "anomaly_score": { + "type": "double" + }, + "average_bucket_processing_time_ms": { + "type": "double" + }, + "bucket_allocation_failures_count": { + "type": "long" + }, + "bucket_count": { + "type": "long" + }, + "bucket_influencers": { + "properties": { + "anomaly_score": { + "type": "double" + }, + "bucket_span": { + "type": "long" + }, + "influencer_field_name": { + "type": "keyword" + }, + "initial_anomaly_score": { + "type": "double" + }, + "is_interim": { + "type": "boolean" + }, + "job_id": { + "type": "keyword" + }, + "probability": { + "type": "double" + }, + "raw_anomaly_score": { + "type": "double" + }, + "result_type": { + "type": "keyword" + }, + "timestamp": { + "type": "date" + } + }, + "type": "nested" + }, + "bucket_span": { + "type": "long" + }, + "by_field_name": { + "type": "keyword" + }, + "by_field_value": { + "copy_to": [ + "all_field_values" + ], + "type": "keyword" + }, + "categorization_status": { + "type": "keyword" + }, + "categorized_doc_count": { + "type": "keyword" + }, + "category_id": { + "type": "long" + }, + "causes": { + "properties": { + "actual": { + "type": "double" + }, + "by_field_name": { + "type": "keyword" + }, + "by_field_value": { + "copy_to": [ + "all_field_values" + ], + "type": "keyword" + }, + "correlated_by_field_value": { + "copy_to": [ + "all_field_values" + ], + "type": "keyword" + }, + "field_name": { + "type": "keyword" + }, + "function": { + "type": "keyword" + }, + "function_description": { + "type": "keyword" + }, + "geo_results": { + "properties": { + "actual_point": { + "type": "geo_point" + }, + "typical_point": { + "type": "geo_point" + } + } + }, + "over_field_name": { + "type": "keyword" + }, + "over_field_value": { + "copy_to": [ + "all_field_values" + ], + "type": "keyword" + }, + "partition_field_name": { + "type": "keyword" + }, + "partition_field_value": { + "copy_to": [ + "all_field_values" + ], + "type": "keyword" + }, + "probability": { + "type": "double" + }, + "typical": { + "type": "double" + } + }, + "type": "nested" + }, + "dead_category_count": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "detector_index": { + "type": "integer" + }, + "earliest_record_timestamp": { + "type": "date" + }, + "empty_bucket_count": { + "type": "long" + }, + "event_count": { + "type": "long" + }, + "examples": { + "type": "text" + }, + "exponential_average_bucket_processing_time_ms": { + "type": "double" + }, + "exponential_average_calculation_context": { + "properties": { + "incremental_metric_value_ms": { + "type": "double" + }, + "latest_timestamp": { + "type": "date" + }, + "previous_exponential_average_ms": { + "type": "double" + } + } + }, + "field_name": { + "type": "keyword" + }, + "forecast_create_timestamp": { + "type": "date" + }, + "forecast_end_timestamp": { + "type": "date" + }, + "forecast_expiry_timestamp": { + "type": "date" + }, + "forecast_id": { + "type": "keyword" + }, + "forecast_lower": { + "type": "double" + }, + "forecast_memory_bytes": { + "type": "long" + }, + "forecast_messages": { + "type": "keyword" + }, + "forecast_prediction": { + "type": "double" + }, + "forecast_progress": { + "type": "double" + }, + "forecast_start_timestamp": { + "type": "date" + }, + "forecast_status": { + "type": "keyword" + }, + "forecast_upper": { + "type": "double" + }, + "frequent_category_count": { + "type": "keyword" + }, + "function": { + "type": "keyword" + }, + "function_description": { + "type": "keyword" + }, + "geo_results": { + "properties": { + "actual_point": { + "type": "geo_point" + }, + "typical_point": { + "type": "geo_point" + } + } + }, + "host": { + "properties": { + "name": { + "type": "keyword" + } + } + }, + "influencer_field_name": { + "type": "keyword" + }, + "influencer_field_value": { + "copy_to": [ + "all_field_values" + ], + "type": "keyword" + }, + "influencer_score": { + "type": "double" + }, + "influencers": { + "properties": { + "influencer_field_name": { + "type": "keyword" + }, + "influencer_field_values": { + "copy_to": [ + "all_field_values" + ], + "type": "keyword" + } + }, + "type": "nested" + }, + "initial_anomaly_score": { + "type": "double" + }, + "initial_influencer_score": { + "type": "double" + }, + "initial_record_score": { + "type": "double" + }, + "input_bytes": { + "type": "long" + }, + "input_field_count": { + "type": "long" + }, + "input_record_count": { + "type": "long" + }, + "invalid_date_count": { + "type": "long" + }, + "is_interim": { + "type": "boolean" + }, + "job_id": { + "copy_to": [ + "all_field_values" + ], + "type": "keyword" + }, + "last_data_time": { + "type": "date" + }, + "latest_empty_bucket_timestamp": { + "type": "date" + }, + "latest_record_time_stamp": { + "type": "date" + }, + "latest_record_timestamp": { + "type": "date" + }, + "latest_result_time_stamp": { + "type": "date" + }, + "latest_sparse_bucket_timestamp": { + "type": "date" + }, + "log_time": { + "type": "date" + }, + "max_matching_length": { + "type": "long" + }, + "maximum_bucket_processing_time_ms": { + "type": "double" + }, + "memory_status": { + "type": "keyword" + }, + "min_version": { + "type": "keyword" + }, + "minimum_bucket_processing_time_ms": { + "type": "double" + }, + "missing_field_count": { + "type": "long" + }, + "model_bytes": { + "type": "long" + }, + "model_bytes_exceeded": { + "type": "keyword" + }, + "model_bytes_memory_limit": { + "type": "keyword" + }, + "model_feature": { + "type": "keyword" + }, + "model_lower": { + "type": "double" + }, + "model_median": { + "type": "double" + }, + "model_size_stats": { + "properties": { + "bucket_allocation_failures_count": { + "type": "long" + }, + "categorization_status": { + "type": "keyword" + }, + "categorized_doc_count": { + "type": "keyword" + }, + "dead_category_count": { + "type": "keyword" + }, + "frequent_category_count": { + "type": "keyword" + }, + "job_id": { + "type": "keyword" + }, + "log_time": { + "type": "date" + }, + "memory_status": { + "type": "keyword" + }, + "model_bytes": { + "type": "long" + }, + "model_bytes_exceeded": { + "type": "keyword" + }, + "model_bytes_memory_limit": { + "type": "keyword" + }, + "rare_category_count": { + "type": "keyword" + }, + "result_type": { + "type": "keyword" + }, + "timestamp": { + "type": "date" + }, + "total_by_field_count": { + "type": "long" + }, + "total_category_count": { + "type": "keyword" + }, + "total_over_field_count": { + "type": "long" + }, + "total_partition_field_count": { + "type": "long" + } + } + }, + "model_upper": { + "type": "double" + }, + "multi_bucket_impact": { + "type": "double" + }, + "out_of_order_timestamp_count": { + "type": "long" + }, + "over_field_name": { + "type": "keyword" + }, + "over_field_value": { + "copy_to": [ + "all_field_values" + ], + "type": "keyword" + }, + "partition_field_name": { + "type": "keyword" + }, + "partition_field_value": { + "copy_to": [ + "all_field_values" + ], + "type": "keyword" + }, + "probability": { + "type": "double" + }, + "process": { + "properties": { + "name": { + "type": "keyword" + } + } + }, + "processed_field_count": { + "type": "long" + }, + "processed_record_count": { + "type": "long" + }, + "processing_time_ms": { + "type": "long" + }, + "quantiles": { + "enabled": false, + "type": "object" + }, + "rare_category_count": { + "type": "keyword" + }, + "raw_anomaly_score": { + "type": "double" + }, + "record_score": { + "type": "double" + }, + "regex": { + "type": "keyword" + }, + "result_type": { + "type": "keyword" + }, + "retain": { + "type": "boolean" + }, + "scheduled_events": { + "type": "keyword" + }, + "search_count": { + "type": "long" + }, + "snapshot_doc_count": { + "type": "integer" + }, + "snapshot_id": { + "type": "keyword" + }, + "sparse_bucket_count": { + "type": "long" + }, + "terms": { + "type": "text" + }, + "timestamp": { + "type": "date" + }, + "total_by_field_count": { + "type": "long" + }, + "total_category_count": { + "type": "keyword" + }, + "total_over_field_count": { + "type": "long" + }, + "total_partition_field_count": { + "type": "long" + }, + "total_search_time_ms": { + "type": "double" + }, + "typical": { + "type": "double" + }, + "user": { + "properties": { + "name": { + "type": "keyword" + } + } + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "1", + "number_of_shards": "1", + "query": { + "default_field": "all_field_values" + }, + "translog": { + "durability": "async" + }, + "unassigned": { + "node_left": { + "delayed_timeout": "1m" + } + } + } + } + } +} \ No newline at end of file diff --git a/x-pack/test/functional/es_archives/spaces/disabled_features/mappings.json b/x-pack/test/functional/es_archives/spaces/disabled_features/mappings.json index 88e63e23ea00c..d0ad5570373c4 100644 --- a/x-pack/test/functional/es_archives/spaces/disabled_features/mappings.json +++ b/x-pack/test/functional/es_archives/spaces/disabled_features/mappings.json @@ -161,9 +161,9 @@ }, "map" : { "properties" : { - "bounds" : { - "type" : "geo_shape", - "tree" : "quadtree" + "bounds": { + "dynamic": false, + "properties": {} }, "description" : { "type" : "text" diff --git a/x-pack/test/functional/es_archives/timelion/feature_controls/mappings.json b/x-pack/test/functional/es_archives/timelion/feature_controls/mappings.json index 88e63e23ea00c..d0ad5570373c4 100644 --- a/x-pack/test/functional/es_archives/timelion/feature_controls/mappings.json +++ b/x-pack/test/functional/es_archives/timelion/feature_controls/mappings.json @@ -161,9 +161,9 @@ }, "map" : { "properties" : { - "bounds" : { - "type" : "geo_shape", - "tree" : "quadtree" + "bounds": { + "dynamic": false, + "properties": {} }, "description" : { "type" : "text" diff --git a/x-pack/test/functional/es_archives/uptime/blank_data_stream/data.json b/x-pack/test/functional/es_archives/uptime/blank_data_stream/data.json new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/x-pack/test/functional/es_archives/uptime/blank_data_stream/mappings.json b/x-pack/test/functional/es_archives/uptime/blank_data_stream/mappings.json new file mode 100644 index 0000000000000..4b1c109e43857 --- /dev/null +++ b/x-pack/test/functional/es_archives/uptime/blank_data_stream/mappings.json @@ -0,0 +1,1333 @@ +{ + "type": "index", + "value": { + "aliases": {}, + "index": ".ds-synthetics-http-default-2021.04.20-000001", + "mappings": { + "_data_stream_timestamp": { + "enabled": true + }, + "_meta": { + "managed": true, + "managed_by": "ingest-manager", + "package": { + "name": "synthetics" + } + }, + "date_detection": false, + "dynamic_templates": [ + { + "strings_as_keyword": { + "mapping": { + "ignore_above": 1024, + "type": "keyword" + }, + "match_mapping_type": "string" + } + } + ], + "properties": { + "@timestamp": { + "type": "date" + }, + "agent": { + "properties": { + "build": { + "properties": { + "original": { + "type": "wildcard" + } + } + }, + "ephemeral_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "cloud": { + "properties": { + "account": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "availability_zone": { + "ignore_above": 1024, + "type": "keyword" + }, + "image": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "instance": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "machine": { + "properties": { + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "project": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "provider": { + "ignore_above": 1024, + "type": "keyword" + }, + "region": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "container": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "image": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "tag": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "labels": { + "type": "object" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "runtime": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "data_stream": { + "properties": { + "dataset": { + "type": "constant_keyword" + }, + "namespace": { + "type": "constant_keyword" + }, + "type": { + "type": "constant_keyword" + } + } + }, + "dataset": { + "properties": { + "name": { + "type": "constant_keyword" + }, + "namespace": { + "type": "constant_keyword" + }, + "type": { + "type": "constant_keyword" + } + } + }, + "dns": { + "properties": { + "answers": { + "properties": { + "class": { + "ignore_above": 1024, + "type": "keyword" + }, + "data": { + "type": "wildcard" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "ttl": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "header_flags": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "op_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "question": { + "properties": { + "class": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "type": "wildcard" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "subdomain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "resolved_ip": { + "type": "ip" + }, + "response_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "docker": { + "properties": { + "container": { + "properties": { + "labels": { + "type": "object" + } + } + } + } + }, + "ecs": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "error": { + "properties": { + "code": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "message": { + "type": "text" + }, + "stack_trace": { + "type": "wildcard" + }, + "type": { + "type": "wildcard" + } + } + }, + "event": { + "properties": { + "dataset": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "fields": { + "type": "object" + }, + "http": { + "properties": { + "request": { + "properties": { + "body": { + "properties": { + "bytes": { + "type": "long" + }, + "content": { + "type": "wildcard" + } + } + }, + "bytes": { + "type": "long" + }, + "method": { + "ignore_above": 1024, + "type": "keyword" + }, + "mime_type": { + "ignore_above": 1024, + "type": "keyword" + }, + "referrer": { + "type": "wildcard" + } + } + }, + "response": { + "properties": { + "body": { + "properties": { + "bytes": { + "type": "long" + }, + "content": { + "type": "wildcard" + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "bytes": { + "type": "long" + }, + "headers": { + "properties": { + "*": { + "enabled": false, + "type": "object" + }, + "Cache-Control": { + "ignore_above": 1024, + "type": "keyword" + }, + "Content-Length": { + "ignore_above": 1024, + "type": "keyword" + }, + "Content-Type": { + "ignore_above": 1024, + "type": "keyword" + }, + "Date": { + "ignore_above": 1024, + "type": "keyword" + }, + "Location": { + "ignore_above": 1024, + "type": "keyword" + }, + "Referrer-Policy": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "mime_type": { + "ignore_above": 1024, + "type": "keyword" + }, + "redirects": { + "ignore_above": 1024, + "type": "keyword" + }, + "status_code": { + "type": "long" + } + } + }, + "rtt": { + "properties": { + "content": { + "properties": { + "us": { + "type": "long" + } + } + }, + "response_header": { + "properties": { + "us": { + "type": "long" + } + } + }, + "total": { + "properties": { + "us": { + "type": "long" + } + } + }, + "validate": { + "properties": { + "us": { + "type": "long" + } + } + }, + "validate_body": { + "properties": { + "us": { + "type": "long" + } + } + }, + "write_request": { + "properties": { + "us": { + "type": "long" + } + } + } + } + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "jolokia": { + "properties": { + "agent": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "secured": { + "type": "boolean" + }, + "server": { + "properties": { + "product": { + "ignore_above": 1024, + "type": "keyword" + }, + "vendor": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "url": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "kubernetes": { + "properties": { + "annotations": { + "properties": { + "*": { + "type": "object" + } + } + }, + "container": { + "properties": { + "image": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "deployment": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "labels": { + "properties": { + "*": { + "type": "object" + } + } + }, + "namespace": { + "ignore_above": 1024, + "type": "keyword" + }, + "node": { + "properties": { + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pod": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "uid": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "replicaset": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "statefulset": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "labels": { + "type": "object" + }, + "monitor": { + "properties": { + "check_group": { + "ignore_above": 1024, + "type": "keyword" + }, + "duration": { + "properties": { + "us": { + "type": "long" + } + } + }, + "fleet_managed": { + "type": "boolean" + }, + "id": { + "fields": { + "text": { + "analyzer": "simple", + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "name": { + "fields": { + "text": { + "analyzer": "simple", + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "timespan": { + "type": "date_range" + }, + "type": { + "type": "constant_keyword", + "value": "http" + } + } + }, + "observer": { + "properties": { + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "type": "wildcard" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "type": "wildcard" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "type": "wildcard" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + }, + "serial_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "vendor": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "resolve": { + "properties": { + "ip": { + "type": "ip" + }, + "rtt": { + "properties": { + "us": { + "type": "long" + } + } + } + } + }, + "socks5": { + "properties": { + "rtt": { + "properties": { + "connect": { + "properties": { + "us": { + "type": "long" + } + } + } + } + } + } + }, + "summary": { + "properties": { + "down": { + "type": "long" + }, + "up": { + "type": "long" + } + } + }, + "tags": { + "ignore_above": 1024, + "type": "keyword" + }, + "tcp": { + "properties": { + "rtt": { + "properties": { + "connect": { + "properties": { + "us": { + "type": "long" + } + } + }, + "validate": { + "properties": { + "us": { + "type": "long" + } + } + } + } + } + } + }, + "tls": { + "properties": { + "certificate_not_valid_after": { + "type": "date" + }, + "certificate_not_valid_before": { + "type": "date" + }, + "cipher": { + "ignore_above": 1024, + "type": "keyword" + }, + "client": { + "properties": { + "certificate": { + "ignore_above": 1024, + "type": "keyword" + }, + "certificate_chain": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "issuer": { + "type": "wildcard" + }, + "ja3": { + "ignore_above": 1024, + "type": "keyword" + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "server_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject": { + "type": "wildcard" + }, + "supported_ciphers": { + "ignore_above": 1024, + "type": "keyword" + }, + "x509": { + "properties": { + "alternative_names": { + "ignore_above": 1024, + "type": "keyword" + }, + "issuer": { + "properties": { + "common_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country": { + "ignore_above": 1024, + "type": "keyword" + }, + "distinguished_name": { + "type": "wildcard" + }, + "locality": { + "ignore_above": 1024, + "type": "keyword" + }, + "organization": { + "ignore_above": 1024, + "type": "keyword" + }, + "organizational_unit": { + "ignore_above": 1024, + "type": "keyword" + }, + "state_or_province": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "public_key_algorithm": { + "ignore_above": 1024, + "type": "keyword" + }, + "public_key_curve": { + "ignore_above": 1024, + "type": "keyword" + }, + "public_key_exponent": { + "type": "long" + }, + "public_key_size": { + "type": "long" + }, + "serial_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "signature_algorithm": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject": { + "properties": { + "common_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country": { + "ignore_above": 1024, + "type": "keyword" + }, + "distinguished_name": { + "type": "wildcard" + }, + "locality": { + "ignore_above": 1024, + "type": "keyword" + }, + "organization": { + "ignore_above": 1024, + "type": "keyword" + }, + "organizational_unit": { + "ignore_above": 1024, + "type": "keyword" + }, + "state_or_province": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "version_number": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "curve": { + "ignore_above": 1024, + "type": "keyword" + }, + "established": { + "type": "boolean" + }, + "next_protocol": { + "ignore_above": 1024, + "type": "keyword" + }, + "resumed": { + "type": "boolean" + }, + "rtt": { + "properties": { + "handshake": { + "properties": { + "us": { + "type": "long" + } + } + } + } + }, + "server": { + "properties": { + "certificate": { + "ignore_above": 1024, + "type": "keyword" + }, + "certificate_chain": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "issuer": { + "type": "wildcard" + }, + "ja3s": { + "ignore_above": 1024, + "type": "keyword" + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "subject": { + "type": "wildcard" + }, + "version_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "x509": { + "properties": { + "alternative_names": { + "ignore_above": 1024, + "type": "keyword" + }, + "issuer": { + "properties": { + "common_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country": { + "ignore_above": 1024, + "type": "keyword" + }, + "distinguished_name": { + "type": "wildcard" + }, + "locality": { + "ignore_above": 1024, + "type": "keyword" + }, + "organization": { + "ignore_above": 1024, + "type": "keyword" + }, + "organizational_unit": { + "ignore_above": 1024, + "type": "keyword" + }, + "state_or_province": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "public_key_algorithm": { + "ignore_above": 1024, + "type": "keyword" + }, + "public_key_curve": { + "ignore_above": 1024, + "type": "keyword" + }, + "public_key_exponent": { + "type": "long" + }, + "public_key_size": { + "type": "long" + }, + "serial_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "signature_algorithm": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject": { + "properties": { + "common_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country": { + "ignore_above": 1024, + "type": "keyword" + }, + "distinguished_name": { + "type": "wildcard" + }, + "locality": { + "ignore_above": 1024, + "type": "keyword" + }, + "organization": { + "ignore_above": 1024, + "type": "keyword" + }, + "organizational_unit": { + "ignore_above": 1024, + "type": "keyword" + }, + "state_or_province": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "version_number": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + }, + "version_protocol": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "url": { + "properties": { + "domain": { + "type": "wildcard" + }, + "extension": { + "ignore_above": 1024, + "type": "keyword" + }, + "fragment": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "type": "wildcard" + }, + "original": { + "type": "wildcard" + }, + "password": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "type": "wildcard" + }, + "port": { + "type": "long" + }, + "query": { + "ignore_above": 1024, + "type": "keyword" + }, + "registered_domain": { + "type": "wildcard" + }, + "scheme": { + "ignore_above": 1024, + "type": "keyword" + }, + "subdomain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "username": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "x509": { + "properties": { + "alternative_names": { + "ignore_above": 1024, + "type": "keyword" + }, + "issuer": { + "properties": { + "common_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country": { + "ignore_above": 1024, + "type": "keyword" + }, + "distinguished_name": { + "type": "wildcard" + }, + "locality": { + "ignore_above": 1024, + "type": "keyword" + }, + "organization": { + "ignore_above": 1024, + "type": "keyword" + }, + "organizational_unit": { + "ignore_above": 1024, + "type": "keyword" + }, + "state_or_province": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "public_key_algorithm": { + "ignore_above": 1024, + "type": "keyword" + }, + "public_key_curve": { + "ignore_above": 1024, + "type": "keyword" + }, + "public_key_exponent": { + "type": "long" + }, + "public_key_size": { + "type": "long" + }, + "serial_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "signature_algorithm": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject": { + "properties": { + "common_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country": { + "ignore_above": 1024, + "type": "keyword" + }, + "distinguished_name": { + "type": "wildcard" + }, + "locality": { + "ignore_above": 1024, + "type": "keyword" + }, + "organization": { + "ignore_above": 1024, + "type": "keyword" + }, + "organizational_unit": { + "ignore_above": 1024, + "type": "keyword" + }, + "state_or_province": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "version_number": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "settings": { + "index": { + "codec": "best_compression", + "hidden": "true", + "lifecycle": { + "name": "synthetics" + }, + "mapping": { + "total_fields": { + "limit": "10000" + } + }, + "number_of_replicas": "1", + "number_of_shards": "1", + "refresh_interval": "5s" + } + } + } +} \ No newline at end of file diff --git a/x-pack/test/functional/page_objects/search_sessions_management_page.ts b/x-pack/test/functional/page_objects/search_sessions_management_page.ts index 3f0e6b80b483a..86391b568fdf2 100644 --- a/x-pack/test/functional/page_objects/search_sessions_management_page.ts +++ b/x-pack/test/functional/page_objects/search_sessions_management_page.ts @@ -33,7 +33,7 @@ export function SearchSessionsPageProvider({ getService, getPageObjects }: FtrPr const viewCell = await row.findByTestSubject('sessionManagementNameCol'); const actionsCell = await row.findByTestSubject('sessionManagementActionsCol'); return { - name: $.findTestSubject('sessionManagementNameCol').text(), + name: $.findTestSubject('sessionManagementNameCol').text().trim(), status: $.findTestSubject('sessionManagementStatusLabel').attr('data-test-status'), mainUrl: $.findTestSubject('sessionManagementNameCol').text(), created: $.findTestSubject('sessionManagementCreatedCol').text(), diff --git a/x-pack/test/functional/services/transform/wizard.ts b/x-pack/test/functional/services/transform/wizard.ts index cef6c2724033e..73a7ba52b384a 100644 --- a/x-pack/test/functional/services/transform/wizard.ts +++ b/x-pack/test/functional/services/transform/wizard.ts @@ -262,12 +262,18 @@ export function TransformWizardProvider({ getService, getPageObjects }: FtrProvi `[data-test-subj="mlDataGridChart-${id}-histogram"] .echCanvasRenderer`, sortedExpectedColorStats, undefined, - 4 + 10 ); expect(actualColorStats.length).to.eql( sortedExpectedColorStats.length, - `Expected and actual color stats for column '${expected.id}' should have the same amount of elements. Expected: ${sortedExpectedColorStats.length} (got ${actualColorStats.length})` + `Expected and actual color stats for column '${ + expected.id + }' should have the same amount of elements. Expected: ${ + sortedExpectedColorStats.length + } '${JSON.stringify(sortedExpectedColorStats)}' (got ${ + actualColorStats.length + } '${JSON.stringify(actualColorStats)}')` ); expect(actualColorStats.every((d) => d.withinTolerance)).to.eql( true, diff --git a/x-pack/test/plugin_functional/es_archives/global_search/basic/mappings.json b/x-pack/test/plugin_functional/es_archives/global_search/basic/mappings.json index ebb5b19387faf..c66a45084441f 100644 --- a/x-pack/test/plugin_functional/es_archives/global_search/basic/mappings.json +++ b/x-pack/test/plugin_functional/es_archives/global_search/basic/mappings.json @@ -161,9 +161,9 @@ }, "map" : { "properties" : { - "bounds" : { - "type" : "geo_shape", - "tree" : "quadtree" + "bounds": { + "dynamic": false, + "properties": {} }, "description" : { "type" : "text" diff --git a/x-pack/test/search_sessions_integration/tests/apps/management/search_sessions/sessions_management.ts b/x-pack/test/search_sessions_integration/tests/apps/management/search_sessions/sessions_management.ts index bb39c771f0aaa..27b4a887075fb 100644 --- a/x-pack/test/search_sessions_integration/tests/apps/management/search_sessions/sessions_management.ts +++ b/x-pack/test/search_sessions_integration/tests/apps/management/search_sessions/sessions_management.ts @@ -113,68 +113,79 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const searchSessionList = await PageObjects.searchSessionsManagement.getList(); expect(searchSessionList.length).to.be(10); - expectSnapshot(searchSessionList.map((ss) => [ss.app, ss.name, ss.created, ss.expires])) - .toMatchInline(` + expectSnapshot( + searchSessionList.map((ss) => [ss.app, ss.name, ss.created, ss.expires, ss.status]) + ).toMatchInline(` Array [ Array [ "graph", - "[eCommerce] Orders Test 6 ", + "[eCommerce] Orders Test 6", "16 Feb, 2021, 00:00:00", "--", + "error", ], Array [ "lens", "[eCommerce] Orders Test 7", "15 Feb, 2021, 00:00:00", - "24 Feb, 2021, 00:00:00", + "--", + "expired", ], Array [ "apm", "[eCommerce] Orders Test 8", "14 Feb, 2021, 00:00:00", - "24 Feb, 2021, 00:00:00", + "--", + "expired", ], Array [ "appSearch", "[eCommerce] Orders Test 9", "13 Feb, 2021, 00:00:00", - "24 Feb, 2021, 00:00:00", + "--", + "expired", ], Array [ "auditbeat", "[eCommerce] Orders Test 10", "12 Feb, 2021, 00:00:00", - "24 Feb, 2021, 00:00:00", + "--", + "expired", ], Array [ "code", "[eCommerce] Orders Test 11", "11 Feb, 2021, 00:00:00", - "24 Feb, 2021, 00:00:00", + "--", + "expired", ], Array [ "console", "[eCommerce] Orders Test 12", "10 Feb, 2021, 00:00:00", - "24 Feb, 2021, 00:00:00", + "--", + "expired", ], Array [ "security", - "[eCommerce] Orders Test 5 ", + "[eCommerce] Orders Test 5", "9 Feb, 2021, 00:00:00", - "24 Feb, 2021, 00:00:00", + "--", + "expired", ], Array [ "visualize", - "[eCommerce] Orders Test 4 ", + "[eCommerce] Orders Test 4", "8 Feb, 2021, 00:00:00", "--", + "cancelled", ], Array [ "canvas", "[eCommerce] Orders Test 3", "7 Feb, 2021, 00:00:00", - "24 Feb, 2021, 00:00:00", + "--", + "expired", ], ] `); @@ -201,13 +212,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { "discover", "[eCommerce] Orders Test 2", "6 Feb, 2021, 00:00:00", - "24 Feb, 2021, 00:00:00", + "--", ], Array [ "dashboard", "[eCommerce] Revenue Dashboard", "5 Feb, 2021, 00:00:00", - "24 Feb, 2021, 00:00:00", + "--", ], ] `); diff --git a/x-pack/test/security_functional/tests/login_selector/basic_functionality.ts b/x-pack/test/security_functional/tests/login_selector/basic_functionality.ts index ca6b9497b194e..e1b4396696acd 100644 --- a/x-pack/test/security_functional/tests/login_selector/basic_functionality.ts +++ b/x-pack/test/security_functional/tests/login_selector/basic_functionality.ts @@ -17,7 +17,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const deployment = getService('deployment'); const PageObjects = getPageObjects(['security', 'common']); - describe('Basic functionality', function () { + // FLAKY: https://github.com/elastic/kibana/issues/98562 + describe.skip('Basic functionality', function () { this.tags('includeFirefox'); const testCredentials = { username: 'admin_user', password: 'change_me' }; diff --git a/yarn.lock b/yarn.lock index 29db5e0d4830a..3afbfee0af875 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2615,7 +2615,7 @@ version "0.0.0" uid "" -"@kbn/config@link:packages/kbn-config": +"@kbn/config@link:bazel-bin/packages/kbn-config/npm_module": version "0.0.0" uid "" @@ -2691,6 +2691,12 @@ version "0.0.0" uid "" +"@kbn/securitysolution-constants@link:bazel-bin/packages/kbn-securitysolution-constants/npm_module": +"@kbn/securitysolution-utils@link:bazel-bin/packages/kbn-securitysolution-utils/npm_module": +"@kbn/securitysolution-io-ts-utils@link:bazel-bin/packages/kbn-securitysolution-io-ts-utils/npm_module": + version "0.0.0" + uid "" + "@kbn/server-http-tools@link:packages/kbn-server-http-tools": version "0.0.0" uid "" @@ -27369,15 +27375,10 @@ underscore.string@~3.3.5: sprintf-js "^1.0.3" util-deprecate "^1.0.2" -underscore@^1.8.3: - version "1.9.1" - resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.9.1.tgz#06dce34a0e68a7babc29b365b8e74b8925203961" - integrity sha512-5/4etnCkd9c8gwgowi5/om/mYO5ajCaOgdzj/oW+0eQV9WxKBDZw5+ycmKmeaTXjInS/W0BzpGLo2xR2aBwZdg== - -underscore@~1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.6.0.tgz#8b38b10cacdef63337b8b24e4ff86d45aea529a8" - integrity sha1-izixDKze9jM3uLJOT/htRa6lKag= +underscore@^1.13.1, underscore@^1.8.3, underscore@~1.6.0: + version "1.13.1" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.1.tgz#0c1c6bd2df54b6b69f2314066d65b6cde6fcf9d1" + integrity sha512-hzSoAVtJF+3ZtiFX0VgfFPHEDRm7Y/QPjGyNo4TVdnDTdft3tr8hEkD25a1jC+TjTuE7tkHGKkhwCgs9dgBB2g== undertaker-registry@^1.0.0: version "1.0.1"