From b37b6ed4a26e5578d69b6fad1facdc72fb525ffa Mon Sep 17 00:00:00 2001 From: Nick Partridge Date: Wed, 1 Jul 2020 14:01:01 -0500 Subject: [PATCH 01/34] Add more detailed unmet deps logging (#70098) --- .../server/plugins/plugins_service.test.ts | 34 ++++++++++++-- src/core/server/plugins/plugins_service.ts | 45 ++++++++++++++----- 2 files changed, 63 insertions(+), 16 deletions(-) diff --git a/src/core/server/plugins/plugins_service.test.ts b/src/core/server/plugins/plugins_service.test.ts index c277dc85e5e04..46fd2b00c2304 100644 --- a/src/core/server/plugins/plugins_service.test.ts +++ b/src/core/server/plugins/plugins_service.test.ts @@ -51,7 +51,7 @@ const logger = loggingSystemMock.create(); expect.addSnapshotSerializer(createAbsolutePathSerializer()); -['path-1', 'path-2', 'path-3', 'path-4', 'path-5'].forEach((path) => { +['path-1', 'path-2', 'path-3', 'path-4', 'path-5', 'path-6', 'path-7', 'path-8'].forEach((path) => { jest.doMock(join(path, 'server'), () => ({}), { virtual: true, }); @@ -227,6 +227,26 @@ describe('PluginsService', () => { path: 'path-4', configPath: 'path-4-disabled', }), + createPlugin('plugin-with-disabled-optional-dep', { + path: 'path-5', + configPath: 'path-5', + optionalPlugins: ['explicitly-disabled-plugin'], + }), + createPlugin('plugin-with-missing-optional-dep', { + path: 'path-6', + configPath: 'path-6', + optionalPlugins: ['missing-plugin'], + }), + createPlugin('plugin-with-disabled-nested-transitive-dep', { + path: 'path-7', + configPath: 'path-7', + requiredPlugins: ['plugin-with-disabled-transitive-dep'], + }), + createPlugin('plugin-with-missing-nested-dep', { + path: 'path-8', + configPath: 'path-8', + requiredPlugins: ['plugin-with-missing-required-deps'], + }), ]), }); @@ -234,7 +254,7 @@ describe('PluginsService', () => { const setup = await pluginsService.setup(setupDeps); expect(setup.contracts).toBeInstanceOf(Map); - expect(mockPluginSystem.addPlugin).not.toHaveBeenCalled(); + expect(mockPluginSystem.addPlugin).toHaveBeenCalledTimes(2); expect(mockPluginSystem.setupPlugins).toHaveBeenCalledTimes(1); expect(mockPluginSystem.setupPlugins).toHaveBeenCalledWith(setupDeps); @@ -244,14 +264,20 @@ describe('PluginsService', () => { "Plugin \\"explicitly-disabled-plugin\\" is disabled.", ], Array [ - "Plugin \\"plugin-with-missing-required-deps\\" has been disabled since some of its direct or transitive dependencies are missing or disabled.", + "Plugin \\"plugin-with-missing-required-deps\\" has been disabled since the following direct or transitive dependencies are missing or disabled: [missing-plugin]", ], Array [ - "Plugin \\"plugin-with-disabled-transitive-dep\\" has been disabled since some of its direct or transitive dependencies are missing or disabled.", + "Plugin \\"plugin-with-disabled-transitive-dep\\" has been disabled since the following direct or transitive dependencies are missing or disabled: [another-explicitly-disabled-plugin]", ], Array [ "Plugin \\"another-explicitly-disabled-plugin\\" is disabled.", ], + Array [ + "Plugin \\"plugin-with-disabled-nested-transitive-dep\\" has been disabled since the following direct or transitive dependencies are missing or disabled: [plugin-with-disabled-transitive-dep]", + ], + Array [ + "Plugin \\"plugin-with-missing-nested-dep\\" has been disabled since the following direct or transitive dependencies are missing or disabled: [plugin-with-missing-required-deps]", + ], ] `); }); diff --git a/src/core/server/plugins/plugins_service.ts b/src/core/server/plugins/plugins_service.ts index 7441e753efa6a..5d1261e697bc0 100644 --- a/src/core/server/plugins/plugins_service.ts +++ b/src/core/server/plugins/plugins_service.ts @@ -239,11 +239,15 @@ export class PluginsService implements CoreService, parents: PluginName[] = [] - ): boolean { + ): { enabled: true } | { enabled: false; missingDependencies: string[] } { const pluginInfo = pluginEnableStatuses.get(pluginName); - return ( - pluginInfo !== undefined && - pluginInfo.isEnabled && - pluginInfo.plugin.requiredPlugins - .filter((dep) => !parents.includes(dep)) - .every((dependencyName) => - this.shouldEnablePlugin(dependencyName, pluginEnableStatuses, [...parents, pluginName]) - ) - ); + + if (pluginInfo === undefined || !pluginInfo.isEnabled) { + return { + enabled: false, + missingDependencies: [], + }; + } + + const missingDependencies = pluginInfo.plugin.requiredPlugins + .filter((dep) => !parents.includes(dep)) + .filter( + (dependencyName) => + !this.shouldEnablePlugin(dependencyName, pluginEnableStatuses, [...parents, pluginName]) + .enabled + ); + + if (missingDependencies.length === 0) { + return { + enabled: true, + }; + } + + return { + enabled: false, + missingDependencies, + }; } private registerPluginStaticDirs(deps: PluginsServiceSetupDeps) { From 37d7d788d912a5cc93cd94986e5fb858e696ee1a Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Wed, 1 Jul 2020 15:13:03 -0400 Subject: [PATCH 02/34] [Security_Solution][Endpoint] Refactor resolver generator for ancestry array (#70129) * Refactor generator for ancestry support * Adding optional ancestry array * Fixing tests and type errors * Removing useAncestry field * Fixing test * An empty array will be returned because that's how ES will do it too Co-authored-by: Elastic Machine --- .../common/endpoint/generate_data.test.ts | 65 +++++- .../common/endpoint/generate_data.ts | 205 +++++++++++------- .../scripts/endpoint/resolver_generator.ts | 8 + .../resolver/utils/children_helper.test.ts | 21 +- .../routes/resolver/utils/tree.test.ts | 2 +- 5 files changed, 207 insertions(+), 94 deletions(-) diff --git a/x-pack/plugins/security_solution/common/endpoint/generate_data.test.ts b/x-pack/plugins/security_solution/common/endpoint/generate_data.test.ts index 4516007580edc..f64462f71a87b 100644 --- a/x-pack/plugins/security_solution/common/endpoint/generate_data.test.ts +++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.test.ts @@ -101,6 +101,30 @@ describe('data generator', () => { expect(processEvent.process.name).not.toBeNull(); }); + describe('creates events with an empty ancestry array', () => { + let tree: Tree; + beforeEach(() => { + tree = generator.generateTree({ + alwaysGenMaxChildrenPerNode: true, + ancestors: 3, + children: 3, + generations: 3, + percentTerminated: 100, + percentWithRelated: 100, + relatedEvents: 0, + relatedAlerts: 0, + ancestryArraySize: 0, + }); + tree.ancestry.delete(tree.origin.id); + }); + + it('creates all events with an empty ancestry array', () => { + for (const event of tree.allEvents) { + expect(event.process.Ext.ancestry.length).toEqual(0); + } + }); + }); + describe('creates an origin alert when no related alerts are requested', () => { let tree: Tree; beforeEach(() => { @@ -113,6 +137,7 @@ describe('data generator', () => { percentWithRelated: 100, relatedEvents: 0, relatedAlerts: 0, + ancestryArraySize: ANCESTRY_LIMIT, }); tree.ancestry.delete(tree.origin.id); }); @@ -150,6 +175,7 @@ describe('data generator', () => { { category: RelatedEventCategory.Network, count: 1 }, ], relatedAlerts, + ancestryArraySize: ANCESTRY_LIMIT, }); }); @@ -162,29 +188,46 @@ describe('data generator', () => { }; const verifyAncestry = (event: Event, genTree: Tree) => { - if (event.process.Ext.ancestry.length > 0) { - expect(event.process.parent?.entity_id).toBe(event.process.Ext.ancestry[0]); + if (event.process.Ext.ancestry!.length > 0) { + expect(event.process.parent?.entity_id).toBe(event.process.Ext.ancestry![0]); } - for (let i = 0; i < event.process.Ext.ancestry.length; i++) { - const ancestor = event.process.Ext.ancestry[i]; + for (let i = 0; i < event.process.Ext.ancestry!.length; i++) { + const ancestor = event.process.Ext.ancestry![i]; const parent = genTree.children.get(ancestor) || genTree.ancestry.get(ancestor); expect(ancestor).toBe(parent?.lifecycle[0].process.entity_id); // the next ancestor should be the grandparent - if (i + 1 < event.process.Ext.ancestry.length) { - const grandparent = event.process.Ext.ancestry[i + 1]; + if (i + 1 < event.process.Ext.ancestry!.length) { + const grandparent = event.process.Ext.ancestry![i + 1]; expect(grandparent).toBe(parent?.lifecycle[0].process.parent?.entity_id); } } }; it('has ancestry array defined', () => { - expect(tree.origin.lifecycle[0].process.Ext.ancestry.length).toBe(ANCESTRY_LIMIT); + expect(tree.origin.lifecycle[0].process.Ext.ancestry!.length).toBe(ANCESTRY_LIMIT); for (const event of tree.allEvents) { verifyAncestry(event, tree); } }); + it('creates the right number childrenLevels', () => { + let totalChildren = 0; + for (const level of tree.childrenLevels) { + totalChildren += level.size; + } + expect(totalChildren).toEqual(tree.children.size); + expect(tree.childrenLevels.length).toEqual(generations); + }); + + it('has the right nodes in both the childrenLevels and children map', () => { + for (const level of tree.childrenLevels) { + for (const node of level.values()) { + expect(tree.children.get(node.id)).toEqual(node); + } + } + }); + it('has the right related events for each node', () => { const checkRelatedEvents = (node: TreeNode) => { expect(node.relatedEvents.length).toEqual(4); @@ -290,7 +333,11 @@ describe('data generator', () => { let events: Event[]; beforeEach(() => { - events = generator.createAlertEventAncestry(3, 0, 0, 0, 0); + events = generator.createAlertEventAncestry({ + ancestors: 3, + percentTerminated: 0, + percentWithRelated: 0, + }); }); it('with n-1 process events', () => { @@ -375,7 +422,7 @@ describe('data generator', () => { const timestamp = new Date().getTime(); const root = generator.generateEvent({ timestamp }); const generations = 2; - const events = [root, ...generator.descendantsTreeGenerator(root, generations)]; + const events = [root, ...generator.descendantsTreeGenerator(root, { generations })]; const rootNode = buildResolverTree(events); const visitedEvents = countResolverEvents(rootNode, generations); expect(visitedEvents).toEqual(events.length); diff --git a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts index 563e2e4ccc9f2..8ebb130f5fc83 100644 --- a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts @@ -17,6 +17,7 @@ import { EndpointStatus, } from './types'; import { factory as policyFactory } from './models/policy_config'; +import { parentEntityId } from './models/event'; export type Event = AlertEvent | EndpointEvent; /** @@ -38,6 +39,7 @@ interface EventOptions { eventCategory?: string | string[]; processName?: string; ancestry?: string[]; + ancestryArrayLimit?: number; pid?: number; parentPid?: number; extensions?: object; @@ -266,6 +268,11 @@ export interface Tree { * Map of entity_id to node */ children: Map; + /** + * An array of levels of the children, that doesn't include the origin or any ancestors + * childrenLevels[0] are the direct children of the origin node. The next level would be those children's descendants + */ + childrenLevels: Array>; /** * Map of entity_id to node */ @@ -289,12 +296,33 @@ export interface TreeOptions { percentWithRelated?: number; percentTerminated?: number; alwaysGenMaxChildrenPerNode?: boolean; + ancestryArraySize?: number; +} + +type TreeOptionDefaults = Required; + +/** + * This function provides defaults for fields that are not specified in the options + * + * @param options tree options for defining the structure of the tree + */ +export function getTreeOptionsWithDef(options?: TreeOptions): TreeOptionDefaults { + return { + ancestors: options?.ancestors ?? 3, + generations: options?.generations ?? 2, + children: options?.children ?? 2, + relatedEvents: options?.relatedEvents ?? 5, + relatedAlerts: options?.relatedAlerts ?? 3, + percentWithRelated: options?.percentWithRelated ?? 30, + percentTerminated: options?.percentTerminated ?? 100, + alwaysGenMaxChildrenPerNode: options?.alwaysGenMaxChildrenPerNode ?? false, + ancestryArraySize: options?.ancestryArraySize ?? ANCESTRY_LIMIT, + }; } export class EndpointDocGenerator { commonInfo: HostInfo; random: seedrandom.prng; - constructor(seed: string | seedrandom.prng = Math.random().toString()) { if (typeof seed === 'string') { this.random = seedrandom(seed); @@ -373,6 +401,7 @@ export class EndpointDocGenerator { * @param ts - Timestamp to put in the event * @param entityID - entityID of the originating process * @param parentEntityID - optional entityID of the parent process, if it exists + * @param ancestryArray - an array of ancestors for the generated alert */ public generateAlert( ts = new Date().getTime(), @@ -438,9 +467,7 @@ export class EndpointDocGenerator { sha256: 'fake sha256', }, Ext: { - // simulate a finite ancestry array size, the endpoint limits the ancestry array to 20 entries we'll use - // 2 so that the backend can handle that case - ancestry: ancestryArray.slice(0, ANCESTRY_LIMIT), + ancestry: ancestryArray, code_signature: [ { trusted: false, @@ -503,6 +530,10 @@ export class EndpointDocGenerator { * @param options - Allows event field values to be specified */ public generateEvent(options: EventOptions = {}): EndpointEvent { + // this will default to an empty array for the ancestry field if options.ancestry isn't included + const ancestry: string[] = + options.ancestry?.slice(0, options?.ancestryArrayLimit ?? ANCESTRY_LIMIT) ?? []; + const processName = options.processName ? options.processName : randomProcessName(); const detailRecordForEventType = options.extensions || @@ -563,7 +594,9 @@ export class EndpointDocGenerator { name: processName, // simulate a finite ancestry array size, the endpoint limits the ancestry array to 20 entries we'll use // 2 so that the backend can handle that case - Ext: { ancestry: options.ancestry?.slice(0, ANCESTRY_LIMIT) || [] }, + Ext: { + ancestry, + }, }, user: { domain: this.randomString(10), @@ -581,6 +614,7 @@ export class EndpointDocGenerator { * @returns a Tree structure that makes accessing specific events easier */ public generateTree(options: TreeOptions = {}): Tree { + const optionsWithDef = getTreeOptionsWithDef(options); const addEventToMap = (nodeMap: Map, event: Event) => { const nodeId = event.process.entity_id; // if a node already exists for the entity_id we'll use that one, otherwise let's create a new empty node @@ -604,13 +638,46 @@ export class EndpointDocGenerator { return nodeMap.set(nodeId, node); }; - const ancestry = this.createAlertEventAncestry( - options.ancestors, - options.relatedEvents, - options.relatedAlerts, - options.percentWithRelated, - options.percentTerminated - ); + const groupNodesByParent = (children: Map) => { + const nodesByParent: Map> = new Map(); + for (const node of children.values()) { + const parentID = parentEntityId(node.lifecycle[0]); + if (parentID) { + let groupedNodes = nodesByParent.get(parentID); + + if (!groupedNodes) { + groupedNodes = new Map(); + nodesByParent.set(parentID, groupedNodes); + } + groupedNodes.set(node.id, node); + } + } + + return nodesByParent; + }; + + const createLevels = ( + childrenByParent: Map>, + levels: Array>, + currentNodes: Map | undefined + ): Array> => { + if (!currentNodes || currentNodes.size === 0) { + return levels; + } + levels.push(currentNodes); + const nextLevel: Map = new Map(); + for (const node of currentNodes.values()) { + const children = childrenByParent.get(node.id); + if (children) { + for (const child of children.values()) { + nextLevel.set(child.id, child); + } + } + } + return createLevels(childrenByParent, levels, nextLevel); + }; + + const ancestry = this.createAlertEventAncestry(optionsWithDef); // create a mapping of entity_id -> {lifecycle, related events, and related alerts} const ancestryNodes: Map = ancestry.reduce(addEventToMap, new Map()); @@ -621,26 +688,18 @@ export class EndpointDocGenerator { throw Error(`could not find origin while building tree: ${alert.process.entity_id}`); } - const children = Array.from( - this.descendantsTreeGenerator( - alert, - options.generations, - options.children, - options.relatedEvents, - options.relatedAlerts, - options.percentWithRelated, - options.percentTerminated, - options.alwaysGenMaxChildrenPerNode - ) - ); + const children = Array.from(this.descendantsTreeGenerator(alert, optionsWithDef)); const childrenNodes: Map = children.reduce(addEventToMap, new Map()); + const childrenByParent = groupNodesByParent(childrenNodes); + const levels = createLevels(childrenByParent, [], childrenByParent.get(origin.id)); return { children: childrenNodes, ancestry: ancestryNodes, allEvents: [...ancestry, ...children], origin, + childrenLevels: levels, }; } @@ -658,8 +717,9 @@ export class EndpointDocGenerator { * @param alwaysGenMaxChildrenPerNode - flag to always return the max children per node instead of it being a random number of children */ public *alertsGenerator(numAlerts: number, options: TreeOptions = {}) { + const opts = getTreeOptionsWithDef(options); for (let i = 0; i < numAlerts; i++) { - yield* this.fullResolverTreeGenerator(options); + yield* this.fullResolverTreeGenerator(opts); } } @@ -678,27 +738,14 @@ export class EndpointDocGenerator { * @param alwaysGenMaxChildrenPerNode - flag to always return the max children per node instead of it being a random number of children */ public *fullResolverTreeGenerator(options: TreeOptions = {}) { - const ancestry = this.createAlertEventAncestry( - options.ancestors, - options.relatedEvents, - options.relatedAlerts, - options.percentWithRelated, - options.percentTerminated - ); + const opts = getTreeOptionsWithDef(options); + + const ancestry = this.createAlertEventAncestry(opts); for (let i = 0; i < ancestry.length; i++) { yield ancestry[i]; } // ancestry will always have at least 2 elements, and the last element will be the alert - yield* this.descendantsTreeGenerator( - ancestry[ancestry.length - 1], - options.generations, - options.children, - options.relatedEvents, - options.relatedAlerts, - options.percentWithRelated, - options.percentTerminated, - options.alwaysGenMaxChildrenPerNode - ); + yield* this.descendantsTreeGenerator(ancestry[ancestry.length - 1], opts); } /** @@ -710,16 +757,14 @@ export class EndpointDocGenerator { * @param pctWithRelated - percent of ancestors that will have related events and alerts * @param pctWithTerminated - percent of ancestors that will have termination events */ - public createAlertEventAncestry( - alertAncestors = 3, - relatedEventsPerNode: RelatedEventInfo[] | number = 5, - relatedAlertsPerNode: number = 3, - pctWithRelated = 30, - pctWithTerminated = 100 - ): Event[] { + public createAlertEventAncestry(options: TreeOptions = {}): Event[] { + const opts = getTreeOptionsWithDef(options); + const events = []; const startDate = new Date().getTime(); - const root = this.generateEvent({ timestamp: startDate + 1000 }); + const root = this.generateEvent({ + timestamp: startDate + 1000, + }); events.push(root); let ancestor = root; let timestamp = root['@timestamp'] + 1000; @@ -738,7 +783,7 @@ export class EndpointDocGenerator { const addRelatedEvents = (node: Event, secBeforeEvent: number, eventList: Event[]) => { for (const relatedEvent of this.relatedEventsGenerator( node, - relatedEventsPerNode, + opts.relatedEvents, secBeforeEvent )) { eventList.push(relatedEvent); @@ -747,13 +792,13 @@ export class EndpointDocGenerator { // generate related alerts for root const processDuration: number = 6 * 3600; - if (this.randomN(100) < pctWithRelated) { + if (this.randomN(100) < opts.percentWithRelated) { addRelatedEvents(ancestor, processDuration, events); - addRelatedAlerts(ancestor, relatedAlertsPerNode, processDuration, events); + addRelatedAlerts(ancestor, opts.relatedAlerts, processDuration, events); } // generate the termination event for the root - if (this.randomN(100) < pctWithTerminated) { + if (this.randomN(100) < opts.percentTerminated) { const termProcessDuration = this.randomN(1000000); // This lets termination events be up to 1 million seconds after the creation event (~11 days) events.push( this.generateEvent({ @@ -766,19 +811,20 @@ export class EndpointDocGenerator { ); } - for (let i = 0; i < alertAncestors; i++) { + for (let i = 0; i < opts.ancestors; i++) { ancestor = this.generateEvent({ timestamp, parentEntityID: ancestor.process.entity_id, // add the parent to the ancestry array - ancestry: [ancestor.process.entity_id, ...ancestor.process.Ext.ancestry], + ancestry: [ancestor.process.entity_id, ...(ancestor.process.Ext.ancestry ?? [])], + ancestryArrayLimit: opts.ancestryArraySize, parentPid: ancestor.process.pid, pid: this.randomN(5000), }); events.push(ancestor); timestamp = timestamp + 1000; - if (this.randomN(100) < pctWithTerminated) { + if (this.randomN(100) < opts.percentTerminated) { const termProcessDuration = this.randomN(1000000); // This lets termination events be up to 1 million seconds after the creation event (~11 days) events.push( this.generateEvent({ @@ -788,18 +834,19 @@ export class EndpointDocGenerator { eventCategory: 'process', eventType: 'end', ancestry: ancestor.process.Ext.ancestry, + ancestryArrayLimit: opts.ancestryArraySize, }) ); } // generate related alerts for ancestor - if (this.randomN(100) < pctWithRelated) { + if (this.randomN(100) < opts.percentWithRelated) { addRelatedEvents(ancestor, processDuration, events); - let numAlertsPerNode = relatedAlertsPerNode; + let numAlertsPerNode = opts.relatedAlerts; // if this is the last ancestor, create one less related alert so that we have a uniform amount of related alerts // for each node. The last alert at the end of this function should always be created even if the related alerts // amount is 0 - if (i === alertAncestors - 1) { + if (i === opts.ancestors - 1) { numAlertsPerNode -= 1; } addRelatedAlerts(ancestor, numAlertsPerNode, processDuration, events); @@ -827,19 +874,11 @@ export class EndpointDocGenerator { * @param percentChildrenTerminated - percent of nodes which will have process termination events * @param alwaysGenMaxChildrenPerNode - flag to always return the max children per node instead of it being a random number of children */ - public *descendantsTreeGenerator( - root: Event, - generations = 2, - maxChildrenPerNode = 2, - relatedEventsPerNode: RelatedEventInfo[] | number = 3, - relatedAlertsPerNode: number = 3, - percentNodesWithRelated = 100, - percentChildrenTerminated = 100, - alwaysGenMaxChildrenPerNode = false - ) { - let maxChildren = this.randomN(maxChildrenPerNode + 1); - if (alwaysGenMaxChildrenPerNode) { - maxChildren = maxChildrenPerNode; + public *descendantsTreeGenerator(root: Event, options: TreeOptions = {}) { + const opts = getTreeOptionsWithDef(options); + let maxChildren = this.randomN(opts.children + 1); + if (opts.alwaysGenMaxChildrenPerNode) { + maxChildren = opts.children; } const rootState: NodeState = { @@ -854,7 +893,7 @@ export class EndpointDocGenerator { // If we get to a state node and it has made all the children, move back up a level if ( currentState.childrenCreated === currentState.maxChildren || - lineage.length === generations + 1 + lineage.length === opts.generations + 1 ) { lineage.pop(); // eslint-disable-next-line no-continue @@ -868,13 +907,14 @@ export class EndpointDocGenerator { parentEntityID: currentState.event.process.entity_id, ancestry: [ currentState.event.process.entity_id, - ...currentState.event.process.Ext.ancestry, + ...(currentState.event.process.Ext.ancestry ?? []), ], + ancestryArrayLimit: opts.ancestryArraySize, }); - maxChildren = this.randomN(maxChildrenPerNode + 1); - if (alwaysGenMaxChildrenPerNode) { - maxChildren = maxChildrenPerNode; + maxChildren = this.randomN(opts.children + 1); + if (opts.alwaysGenMaxChildrenPerNode) { + maxChildren = opts.children; } lineage.push({ event: child, @@ -883,7 +923,7 @@ export class EndpointDocGenerator { }); yield child; let processDuration: number = 6 * 3600; - if (this.randomN(100) < percentChildrenTerminated) { + if (this.randomN(100) < opts.percentTerminated) { processDuration = this.randomN(1000000); // This lets termination events be up to 1 million seconds after the creation event (~11 days) yield this.generateEvent({ timestamp: timestamp + processDuration * 1000, @@ -892,11 +932,12 @@ export class EndpointDocGenerator { eventCategory: 'process', eventType: 'end', ancestry: child.process.Ext.ancestry, + ancestryArrayLimit: opts.ancestryArraySize, }); } - if (this.randomN(100) < percentNodesWithRelated) { - yield* this.relatedEventsGenerator(child, relatedEventsPerNode, processDuration); - yield* this.relatedAlertsGenerator(child, relatedAlertsPerNode, processDuration); + if (this.randomN(100) < opts.percentWithRelated) { + yield* this.relatedEventsGenerator(child, opts.relatedEvents, processDuration); + yield* this.relatedAlertsGenerator(child, opts.relatedAlerts, processDuration); } } } diff --git a/x-pack/plugins/security_solution/scripts/endpoint/resolver_generator.ts b/x-pack/plugins/security_solution/scripts/endpoint/resolver_generator.ts index 53fa59060550f..cfe1c741ef3f1 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/resolver_generator.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/resolver_generator.ts @@ -11,6 +11,7 @@ import fetch from 'node-fetch'; import { Client, ClientOptions } from '@elastic/elasticsearch'; import { ResponseError } from '@elastic/elasticsearch/lib/errors'; import { indexHostsAndAlerts } from '../../common/endpoint/index_data'; +import { ANCESTRY_LIMIT } from '../../common/endpoint/generate_data'; main(); @@ -122,6 +123,12 @@ async function main() { type: 'number', default: 3, }, + ancestryArraySize: { + alias: 'ancSize', + describe: 'the upper bound size of the ancestry array, 0 will mark the field as undefined', + type: 'number', + default: ANCESTRY_LIMIT, + }, generations: { alias: 'gen', describe: 'number of child generations to create', @@ -229,6 +236,7 @@ async function main() { percentWithRelated: argv.percentWithRelated, percentTerminated: argv.percentTerminated, alwaysGenMaxChildrenPerNode: argv.maxChildrenPerNode, + ancestryArraySize: argv.ancestryArraySize, } ); console.log(`Creating and indexing documents took: ${new Date().getTime() - startTime}ms`); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.test.ts index 51c9cef08a466..1d55cb7cfd735 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.test.ts @@ -34,7 +34,16 @@ describe('Children helper', () => { const root = generator.generateEvent(); it('builds the children response structure', () => { - const children = Array.from(generator.descendantsTreeGenerator(root, 3, 3, 0, 0, 0, 100, true)); + const children = Array.from( + generator.descendantsTreeGenerator(root, { + generations: 3, + children: 3, + relatedEvents: 0, + relatedAlerts: 0, + percentTerminated: 100, + alwaysGenMaxChildrenPerNode: true, + }) + ); // because we requested the generator to always return the max children, there will always be at least 2 parents const parents = findParents(children); @@ -66,7 +75,15 @@ describe('Children helper', () => { }); it('builds the children response structure twice', () => { - const children = Array.from(generator.descendantsTreeGenerator(root, 3, 3, 0, 0, 100)); + const children = Array.from( + generator.descendantsTreeGenerator(root, { + generations: 3, + children: 3, + relatedEvents: 0, + relatedAlerts: 0, + percentTerminated: 100, + }) + ); const helper = new ChildrenNodesHelper(root.process.entity_id); helper.addChildren({}, children); helper.getNodes(); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/tree.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/tree.test.ts index eb80c840783ef..21db11f3affd3 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/tree.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/tree.test.ts @@ -20,7 +20,7 @@ describe('Tree', () => { // transform the generator's array of events into the format expected by the tree class const ancestorInfo: ResolverAncestry = { ancestors: generator - .createAlertEventAncestry(5, 0, 0) + .createAlertEventAncestry({ ancestors: 5, percentTerminated: 0, percentWithRelated: 0 }) .filter((event) => { return event.event.kind === 'event'; }) From 1192c904aeefd3274b4aeb1897918c8416eeffe5 Mon Sep 17 00:00:00 2001 From: Aaron Caldwell Date: Wed, 1 Jul 2020 13:39:12 -0600 Subject: [PATCH 03/34] Remove injected vars from maps legacy (#69744) Co-authored-by: Elastic Machine --- .../vis_type_vega/vega_visualization.js | 42 ++----------------- src/plugins/maps_legacy/public/index.ts | 22 +--------- .../maps_legacy/public/kibana_services.js | 12 ++++-- .../public/map/base_maps_visualization.js | 4 +- .../public/map/service_settings.js | 6 +-- src/plugins/maps_legacy/public/plugin.ts | 29 ++++++++++--- .../__tests__/region_map_visualization.js | 20 ++++----- src/plugins/region_map/public/plugin.ts | 4 +- .../coordinate_maps_visualization.js | 27 ++++-------- src/plugins/tile_map/public/plugin.ts | 4 +- src/plugins/vis_type_vega/public/plugin.ts | 4 +- 11 files changed, 62 insertions(+), 112 deletions(-) diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_visualization.js b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_visualization.js index 17610702a0bc7..30e7587707d2e 100644 --- a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_visualization.js +++ b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_visualization.js @@ -66,10 +66,9 @@ import { // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../../../../plugins/vis_type_vega/public/services'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { setInjectedVarFunc } from '../../../../../../plugins/maps_legacy/public/kibana_services'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths import { ServiceSettings } from '../../../../../../plugins/maps_legacy/public/map/service_settings'; -import { getKibanaMapFactoryProvider } from '../../../../../../plugins/maps_legacy/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { KibanaMap } from '../../../../../../plugins/maps_legacy/public/map/kibana_map'; const THRESHOLD = 0.1; const PIXEL_DIFF = 30; @@ -82,18 +81,7 @@ describe('VegaVisualizations', () => { let vegaVisualizationDependencies; let vegaVisType; - const coreSetupMock = { - notifications: { - toasts: {}, - }, - uiSettings: { - get: () => {}, - }, - injectedMetadata: { - getInjectedVar: () => {}, - }, - }; - setKibanaMapFactory(getKibanaMapFactoryProvider(coreSetupMock)); + setKibanaMapFactory((...args) => new KibanaMap(...args)); setInjectedVars({ emsTileLayerId: {}, enableExternalUrls: true, @@ -139,30 +127,6 @@ describe('VegaVisualizations', () => { beforeEach(ngMock.module('kibana')); beforeEach( ngMock.inject(() => { - setInjectedVarFunc((injectedVar) => { - switch (injectedVar) { - case 'mapConfig': - return { - emsFileApiUrl: '', - emsTileApiUrl: '', - emsLandingPageUrl: '', - }; - case 'tilemapsConfig': - return { - deprecated: { - config: { - options: { - attribution: '123', - }, - }, - }, - }; - case 'version': - return '123'; - default: - return 'not found'; - } - }); const serviceSettings = new ServiceSettings(mockMapConfig, mockMapConfig.tilemap); vegaVisualizationDependencies = { serviceSettings, diff --git a/src/plugins/maps_legacy/public/index.ts b/src/plugins/maps_legacy/public/index.ts index cbe8b9213d577..6b9c7d1c52db9 100644 --- a/src/plugins/maps_legacy/public/index.ts +++ b/src/plugins/maps_legacy/public/index.ts @@ -18,12 +18,10 @@ */ // @ts-ignore -import { CoreSetup, PluginInitializerContext } from 'kibana/public'; +import { PluginInitializerContext } from 'kibana/public'; // @ts-ignore import { L } from './leaflet'; -// @ts-ignore -import { KibanaMap } from './map/kibana_map'; -import { bindSetupCoreAndPlugins, MapsLegacyPlugin } from './plugin'; +import { MapsLegacyPlugin } from './plugin'; // @ts-ignore import * as colorUtil from './map/color_util'; // @ts-ignore @@ -32,8 +30,6 @@ import { KibanaMapLayer } from './map/kibana_map_layer'; import { convertToGeoJson } from './map/convert_to_geojson'; // @ts-ignore import { scaleBounds, getPrecision, geoContains } from './map/decode_geo_hash'; -// @ts-ignore -import { BaseMapsVisualizationProvider } from './map/base_maps_visualization'; import { VectorLayer, FileLayerField, @@ -75,20 +71,6 @@ export { L, }; -// Due to a leaflet/leaflet-draw bug, it's not possible to consume leaflet maps w/ draw control -// through a pipeline leveraging angular. For this reason, client plugins need to -// init kibana map and the basemaps visualization directly rather than consume through -// the usual plugin interface -export function getKibanaMapFactoryProvider(core: CoreSetup) { - bindSetupCoreAndPlugins(core); - return (...args: any) => new KibanaMap(...args); -} - -export function getBaseMapsVis(core: CoreSetup, serviceSettings: IServiceSettings) { - const getKibanaMap = getKibanaMapFactoryProvider(core); - return new BaseMapsVisualizationProvider(getKibanaMap, serviceSettings); -} - export * from './common/types'; export { ORIGIN } from './common/constants/origin'; diff --git a/src/plugins/maps_legacy/public/kibana_services.js b/src/plugins/maps_legacy/public/kibana_services.js index e0a6a6e21ab00..256b5f386d5f7 100644 --- a/src/plugins/maps_legacy/public/kibana_services.js +++ b/src/plugins/maps_legacy/public/kibana_services.js @@ -25,6 +25,12 @@ let uiSettings; export const setUiSettings = (coreUiSettings) => (uiSettings = coreUiSettings); export const getUiSettings = () => uiSettings; -let getInjectedVar; -export const setInjectedVarFunc = (getInjectedVarFunc) => (getInjectedVar = getInjectedVarFunc); -export const getInjectedVarFunc = () => getInjectedVar; +let kibanaVersion; +export const setKibanaVersion = (version) => (kibanaVersion = version); +export const getKibanaVersion = () => kibanaVersion; + +let mapsLegacyConfig; +export const setMapsLegacyConfig = (config) => (mapsLegacyConfig = config); +export const getMapsLegacyConfig = () => mapsLegacyConfig; + +export const getEmsTileLayerId = () => getMapsLegacyConfig().emsTileLayerId; diff --git a/src/plugins/maps_legacy/public/map/base_maps_visualization.js b/src/plugins/maps_legacy/public/map/base_maps_visualization.js index 2d1a45beb5d87..2d78fdc246e19 100644 --- a/src/plugins/maps_legacy/public/map/base_maps_visualization.js +++ b/src/plugins/maps_legacy/public/map/base_maps_visualization.js @@ -21,7 +21,7 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; import * as Rx from 'rxjs'; import { filter, first } from 'rxjs/operators'; -import { getInjectedVarFunc, getUiSettings, getToasts } from '../kibana_services'; +import { getEmsTileLayerId, getUiSettings, getToasts } from '../kibana_services'; const WMS_MINZOOM = 0; const WMS_MAXZOOM = 22; //increase this to 22. Better for WMS @@ -129,7 +129,7 @@ export function BaseMapsVisualizationProvider(getKibanaMap, mapServiceSettings) } async _updateBaseLayer() { - const emsTileLayerId = getInjectedVarFunc()('emsTileLayerId', true); + const emsTileLayerId = getEmsTileLayerId(); if (!this._kibanaMap) { return; diff --git a/src/plugins/maps_legacy/public/map/service_settings.js b/src/plugins/maps_legacy/public/map/service_settings.js index 7c2b841e4adf3..f4f88bd5807d5 100644 --- a/src/plugins/maps_legacy/public/map/service_settings.js +++ b/src/plugins/maps_legacy/public/map/service_settings.js @@ -21,22 +21,20 @@ import _ from 'lodash'; import MarkdownIt from 'markdown-it'; import { EMSClient } from '@elastic/ems-client'; import { i18n } from '@kbn/i18n'; -import { getInjectedVarFunc } from '../kibana_services'; +import { getKibanaVersion } from '../kibana_services'; import { ORIGIN } from '../common/constants/origin'; const TMS_IN_YML_ID = 'TMS in config/kibana.yml'; export class ServiceSettings { constructor(mapConfig, tilemapsConfig) { - const getInjectedVar = getInjectedVarFunc(); this._mapConfig = mapConfig; this._tilemapsConfig = tilemapsConfig; - const kbnVersion = getInjectedVar('version'); this._showZoomMessage = true; this._emsClient = new EMSClient({ language: i18n.getLocale(), - appVersion: kbnVersion, + appVersion: getKibanaVersion(), appName: 'kibana', fileApiUrl: this._mapConfig.emsFileApiUrl, tileApiUrl: this._mapConfig.emsTileApiUrl, diff --git a/src/plugins/maps_legacy/public/plugin.ts b/src/plugins/maps_legacy/public/plugin.ts index 78c2498b9ee90..6b4e06fec9ccc 100644 --- a/src/plugins/maps_legacy/public/plugin.ts +++ b/src/plugins/maps_legacy/public/plugin.ts @@ -20,13 +20,17 @@ // @ts-ignore import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'kibana/public'; // @ts-ignore -import { setToasts, setUiSettings, setInjectedVarFunc } from './kibana_services'; +import { setToasts, setUiSettings, setKibanaVersion, setMapsLegacyConfig } from './kibana_services'; // @ts-ignore import { ServiceSettings } from './map/service_settings'; // @ts-ignore import { getPrecision, getZoomPrecision } from './map/precision'; +// @ts-ignore +import { KibanaMap } from './map/kibana_map'; import { MapsLegacyConfigType, MapsLegacyPluginSetup, MapsLegacyPluginStart } from './index'; import { ConfigSchema } from '../config'; +// @ts-ignore +import { BaseMapsVisualizationProvider } from './map/base_maps_visualization'; /** * These are the interfaces with your public contracts. You should export these @@ -34,10 +38,15 @@ import { ConfigSchema } from '../config'; * @public */ -export const bindSetupCoreAndPlugins = (core: CoreSetup) => { +export const bindSetupCoreAndPlugins = ( + core: CoreSetup, + config: MapsLegacyConfigType, + kibanaVersion: string +) => { setToasts(core.notifications.toasts); setUiSettings(core.uiSettings); - setInjectedVarFunc(core.injectedMetadata.getInjectedVar); + setKibanaVersion(kibanaVersion); + setMapsLegacyConfig(config); }; // eslint-disable-next-line @typescript-eslint/no-empty-interface @@ -53,15 +62,23 @@ export class MapsLegacyPlugin implements Plugin(); + const kibanaVersion = this._initializerContext.env.packageInfo.version; + + bindSetupCoreAndPlugins(core, config, kibanaVersion); + + const serviceSettings = new ServiceSettings(config, config.tilemap); + const getKibanaMapFactoryProvider = (...args: any) => new KibanaMap(...args); + const getBaseMapsVis = () => + new BaseMapsVisualizationProvider(getKibanaMapFactoryProvider, serviceSettings); return { - serviceSettings: new ServiceSettings(config, config.tilemap), + serviceSettings, getZoomPrecision, getPrecision, config, + getKibanaMapFactoryProvider, + getBaseMapsVis, }; } diff --git a/src/plugins/region_map/public/__tests__/region_map_visualization.js b/src/plugins/region_map/public/__tests__/region_map_visualization.js index 3dcfc7c2fc6fa..0a2a18c7cef4f 100644 --- a/src/plugins/region_map/public/__tests__/region_map_visualization.js +++ b/src/plugins/region_map/public/__tests__/region_map_visualization.js @@ -52,10 +52,11 @@ import { ExprVis } from '../../../visualizations/public/expressions/vis'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { BaseVisType } from '../../../visualizations/public/vis_types/base_vis_type'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { setInjectedVarFunc } from '../../../maps_legacy/public/kibana_services'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths import { ServiceSettings } from '../../../maps_legacy/public/map/service_settings'; -import { getBaseMapsVis } from '../../../maps_legacy/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { BaseMapsVisualizationProvider } from '../../../maps_legacy/public/map/base_maps_visualization'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { KibanaMap } from '../../../maps_legacy/public/map/kibana_map'; const THRESHOLD = 0.45; const PIXEL_DIFF = 96; @@ -118,14 +119,6 @@ describe('RegionMapsVisualizationTests', function () { }, }, }; - setInjectedVarFunc((injectedVar) => { - switch (injectedVar) { - case 'version': - return '123'; - default: - return 'not found'; - } - }); const serviceSettings = new ServiceSettings(mapConfig, tilemapsConfig); const regionmapsConfig = { includeElasticMapsService: true, @@ -142,7 +135,10 @@ describe('RegionMapsVisualizationTests', function () { getInjectedVar: () => {}, }, }; - const BaseMapsVisualization = getBaseMapsVis(coreSetupMock, serviceSettings); + const BaseMapsVisualization = new BaseMapsVisualizationProvider( + (...args) => new KibanaMap(...args), + serviceSettings + ); dependencies = { serviceSettings, diff --git a/src/plugins/region_map/public/plugin.ts b/src/plugins/region_map/public/plugin.ts index 6b31de758a4ca..04a2ba2f23f4e 100644 --- a/src/plugins/region_map/public/plugin.ts +++ b/src/plugins/region_map/public/plugin.ts @@ -30,7 +30,7 @@ import { VisualizationsSetup } from '../../visualizations/public'; import { createRegionMapFn } from './region_map_fn'; // @ts-ignore import { createRegionMapTypeDefinition } from './region_map_type'; -import { getBaseMapsVis, IServiceSettings, MapsLegacyPluginSetup } from '../../maps_legacy/public'; +import { IServiceSettings, MapsLegacyPluginSetup } from '../../maps_legacy/public'; import { setFormatService, setNotifications, setKibanaLegacy } from './kibana_services'; import { DataPublicPluginStart } from '../../data/public'; import { RegionMapsConfigType } from './index'; @@ -94,7 +94,7 @@ export class RegionMapPlugin implements Plugin { - switch (injectedVar) { - case 'version': - return '123'; - default: - return 'not found'; - } - }); - const coreSetupMock = { - notifications: { - toasts: {}, - }, - uiSettings: {}, - injectedMetadata: { - getInjectedVar: () => {}, - }, - }; const serviceSettings = new ServiceSettings(mapConfig, tilemapsConfig); - const BaseMapsVisualization = getBaseMapsVis(coreSetupMock, serviceSettings); + const BaseMapsVisualization = new BaseMapsVisualizationProvider( + (...args) => new KibanaMap(...args), + serviceSettings + ); const uiSettings = $injector.get('config'); dependencies = { diff --git a/src/plugins/tile_map/public/plugin.ts b/src/plugins/tile_map/public/plugin.ts index 20a45c586074a..1f79104b183ee 100644 --- a/src/plugins/tile_map/public/plugin.ts +++ b/src/plugins/tile_map/public/plugin.ts @@ -32,7 +32,7 @@ import 'angular-sanitize'; import { createTileMapFn } from './tile_map_fn'; // @ts-ignore import { createTileMapTypeDefinition } from './tile_map_type'; -import { getBaseMapsVis, MapsLegacyPluginSetup } from '../../maps_legacy/public'; +import { MapsLegacyPluginSetup } from '../../maps_legacy/public'; import { DataPublicPluginStart } from '../../data/public'; import { setFormatService, setQueryService } from './services'; import { setKibanaLegacy } from './services'; @@ -85,7 +85,7 @@ export class TileMapPlugin implements Plugin = { getZoomPrecision, getPrecision, - BaseMapsVisualization: getBaseMapsVis(core, mapsLegacy.serviceSettings), + BaseMapsVisualization: mapsLegacy.getBaseMapsVis(), uiSettings: core.uiSettings, }; diff --git a/src/plugins/vis_type_vega/public/plugin.ts b/src/plugins/vis_type_vega/public/plugin.ts index b3e35dac3711f..c20a104736291 100644 --- a/src/plugins/vis_type_vega/public/plugin.ts +++ b/src/plugins/vis_type_vega/public/plugin.ts @@ -33,7 +33,7 @@ import { import { createVegaFn } from './vega_fn'; import { createVegaTypeDefinition } from './vega_type'; -import { getKibanaMapFactoryProvider, IServiceSettings } from '../../maps_legacy/public'; +import { IServiceSettings } from '../../maps_legacy/public'; import './index.scss'; import { ConfigSchema } from '../config'; @@ -77,7 +77,7 @@ export class VegaPlugin implements Plugin, void> { emsTileLayerId: core.injectedMetadata.getInjectedVar('emsTileLayerId', true), }); setUISettings(core.uiSettings); - setKibanaMapFactory(getKibanaMapFactoryProvider(core)); + setKibanaMapFactory(mapsLegacy.getKibanaMapFactoryProvider); setMapsLegacyConfig(mapsLegacy.config); const visualizationDependencies: Readonly = { From 97924a6d86b243e477b1e310e7c79440934adf10 Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Wed, 1 Jul 2020 12:40:17 -0700 Subject: [PATCH 04/34] Revert "[eslint][ts] Enable prefer-ts-expect-error (#70022)" (#70474) This reverts commit f3c393eaaffc688ce0d8a17277501207692dd816. --- packages/eslint-config-kibana/typescript.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/eslint-config-kibana/typescript.js b/packages/eslint-config-kibana/typescript.js index 270614ed84b69..a55ca9391011d 100644 --- a/packages/eslint-config-kibana/typescript.js +++ b/packages/eslint-config-kibana/typescript.js @@ -124,7 +124,6 @@ module.exports = { }], '@typescript-eslint/no-var-requires': 'error', '@typescript-eslint/unified-signatures': 'error', - '@typescript-eslint/prefer-ts-expect-error': 'warn', 'constructor-super': 'error', 'dot-notation': 'error', 'eqeqeq': ['error', 'always', {'null': 'ignore'}], From 91b8e7de24cc521629562082265808ce9c24cdc0 Mon Sep 17 00:00:00 2001 From: CJ Cenizal Date: Wed, 1 Jul 2020 12:40:52 -0700 Subject: [PATCH 05/34] Use modern mount context in Dev Tools and Console (#70379) * Use modern mount context in Dev Tools and Console, and clean up plugin definitions of Grok Debugger, Search Profiler, and Painless Lab. * Remove return value from Console lifecycle method. Co-authored-by: Elastic Machine --- src/plugins/console/public/plugin.ts | 27 +++++--- src/plugins/dev_tools/public/application.tsx | 68 +++++++++---------- src/plugins/dev_tools/public/dev_tool.ts | 7 +- src/plugins/dev_tools/public/plugin.ts | 27 +++++--- x-pack/plugins/grokdebugger/public/index.js | 4 +- x-pack/plugins/grokdebugger/public/plugin.js | 7 +- x-pack/plugins/painless_lab/public/plugin.tsx | 12 ++-- x-pack/plugins/searchprofiler/public/index.ts | 5 +- .../plugins/searchprofiler/public/plugin.ts | 14 ++-- 9 files changed, 88 insertions(+), 83 deletions(-) diff --git a/src/plugins/console/public/plugin.ts b/src/plugins/console/public/plugin.ts index 49a232ce35cd0..851dc7a063d7b 100644 --- a/src/plugins/console/public/plugin.ts +++ b/src/plugins/console/public/plugin.ts @@ -18,17 +18,13 @@ */ import { i18n } from '@kbn/i18n'; - -import { CoreSetup, CoreStart, Plugin } from 'kibana/public'; +import { Plugin, CoreSetup } from 'src/core/public'; import { FeatureCatalogueCategory } from '../../home/public'; - import { AppSetupUIPluginDependencies } from './types'; export class ConsoleUIPlugin implements Plugin { - constructor() {} - - async setup( + public setup( { notifications, getStartServices }: CoreSetup, { devTools, home, usageCollection }: AppSetupUIPluginDependencies ) { @@ -53,16 +49,25 @@ export class ConsoleUIPlugin implements Plugin { + mount: async ({ element }) => { + const [core] = await getStartServices(); + + const { + injectedMetadata, + i18n: { Context: I18nContext }, + docLinks: { DOC_LINK_VERSION }, + } = core; + const { renderApp } = await import('./application'); - const [{ injectedMetadata }] = await getStartServices(); + const elasticsearchUrl = injectedMetadata.getInjectedVar( 'elasticsearchUrl', 'http://localhost:9200' ) as string; + return renderApp({ - docLinkVersion: docLinks.DOC_LINK_VERSION, - I18nContext: i18nDep.Context, + docLinkVersion: DOC_LINK_VERSION, + I18nContext, notifications, elasticsearchUrl, usageCollection, @@ -72,5 +77,5 @@ export class ConsoleUIPlugin implements Plugin void; } @@ -40,12 +40,7 @@ interface MountedDevToolDescriptor { unmountHandler: () => void; } -function DevToolsWrapper({ - devTools, - activeDevTool, - appMountContext, - updateRoute, -}: DevToolsWrapperProps) { +function DevToolsWrapper({ devTools, activeDevTool, updateRoute }: DevToolsWrapperProps) { const mountedTool = useRef(null); useEffect( @@ -90,6 +85,7 @@ function DevToolsWrapper({ if (mountedTool.current) { mountedTool.current.unmountHandler(); } + const params = { element, appBasePath: '', @@ -97,9 +93,9 @@ function DevToolsWrapper({ // TODO: adapt to use Core's ScopedHistory history: {} as any, }; - const unmountHandler = isAppMountDeprecated(activeDevTool.mount) - ? await activeDevTool.mount(appMountContext, params) - : await activeDevTool.mount(params); + + const unmountHandler = await activeDevTool.mount(params); + mountedTool.current = { devTool: activeDevTool, mountpoint: element, @@ -112,19 +108,20 @@ function DevToolsWrapper({ ); } -function redirectOnMissingCapabilities(appMountContext: AppMountContext) { - if (!appMountContext.core.application.capabilities.dev_tools.show) { - appMountContext.core.application.navigateToApp('home'); +function redirectOnMissingCapabilities(application: ApplicationStart) { + if (!application.capabilities.dev_tools.show) { + application.navigateToApp('home'); return true; } return false; } -function setBadge(appMountContext: AppMountContext) { - if (appMountContext.core.application.capabilities.dev_tools.save) { +function setBadge(application: ApplicationStart, chrome: ChromeStart) { + if (application.capabilities.dev_tools.save) { return; } - appMountContext.core.chrome.setBadge({ + + chrome.setBadge({ text: i18n.translate('devTools.badge.readOnly.text', { defaultMessage: 'Read only', }), @@ -135,16 +132,16 @@ function setBadge(appMountContext: AppMountContext) { }); } -function setTitle(appMountContext: AppMountContext) { - appMountContext.core.chrome.docTitle.change( +function setTitle(chrome: ChromeStart) { + chrome.docTitle.change( i18n.translate('devTools.pageTitle', { defaultMessage: 'Dev Tools', }) ); } -function setBreadcrumbs(appMountContext: AppMountContext) { - appMountContext.core.chrome.setBreadcrumbs([ +function setBreadcrumbs(chrome: ChromeStart) { + chrome.setBreadcrumbs([ { text: i18n.translate('devTools.k7BreadcrumbsDevToolsLabel', { defaultMessage: 'Dev Tools', @@ -156,16 +153,19 @@ function setBreadcrumbs(appMountContext: AppMountContext) { export function renderApp( element: HTMLElement, - appMountContext: AppMountContext, + application: ApplicationStart, + chrome: ChromeStart, history: ScopedHistory, devTools: readonly DevToolApp[] ) { - if (redirectOnMissingCapabilities(appMountContext)) { + if (redirectOnMissingCapabilities(application)) { return () => {}; } - setBadge(appMountContext); - setBreadcrumbs(appMountContext); - setTitle(appMountContext); + + setBadge(application, chrome); + setBreadcrumbs(chrome); + setTitle(chrome); + ReactDOM.render( @@ -183,7 +183,6 @@ export function renderApp( updateRoute={props.history.push} activeDevTool={devTool} devTools={devTools} - appMountContext={appMountContext} /> )} /> @@ -208,8 +207,3 @@ export function renderApp( unlisten(); }; } - -function isAppMountDeprecated(mount: (...args: any[]) => any): mount is AppMountDeprecated { - // Mount functions with two arguments are assumed to expect deprecated `context` object. - return mount.length === 2; -} diff --git a/src/plugins/dev_tools/public/dev_tool.ts b/src/plugins/dev_tools/public/dev_tool.ts index 943cca286a722..932897cdd7861 100644 --- a/src/plugins/dev_tools/public/dev_tool.ts +++ b/src/plugins/dev_tools/public/dev_tool.ts @@ -16,7 +16,8 @@ * specific language governing permissions and limitations * under the License. */ -import { App } from 'kibana/public'; + +import { AppMount } from 'src/core/public'; /** * Descriptor for a dev tool. A dev tool works similar to an application @@ -38,7 +39,7 @@ export class DevToolApp { * This will be used as a label in the tab above the actual tool. */ public readonly title: string; - public readonly mount: App['mount']; + public readonly mount: AppMount; /** * Flag indicating to disable the tab of this dev tool. Navigating to a @@ -66,7 +67,7 @@ export class DevToolApp { constructor( id: string, title: string, - mount: App['mount'], + mount: AppMount, enableRouting: boolean, order: number, toolTipContent = '', diff --git a/src/plugins/dev_tools/public/plugin.ts b/src/plugins/dev_tools/public/plugin.ts index 130d07b441b83..3ee44aaa0816e 100644 --- a/src/plugins/dev_tools/public/plugin.ts +++ b/src/plugins/dev_tools/public/plugin.ts @@ -18,12 +18,14 @@ */ import { BehaviorSubject } from 'rxjs'; -import { AppUpdater, CoreSetup, Plugin } from 'kibana/public'; +import { Plugin, CoreSetup, AppMountParameters } from 'src/core/public'; +import { AppUpdater } from 'kibana/public'; import { i18n } from '@kbn/i18n'; import { sortBy } from 'lodash'; + +import { AppNavLinkStatus, DEFAULT_APP_CATEGORIES } from '../../../core/public'; import { KibanaLegacySetup } from '../../kibana_legacy/public'; import { CreateDevToolArgs, DevToolApp, createDevToolApp } from './dev_tool'; -import { AppNavLinkStatus, DEFAULT_APP_CATEGORIES } from '../../../core/public'; import './index.scss'; @@ -49,8 +51,10 @@ export class DevToolsPlugin implements Plugin { return sortBy([...this.devTools.values()], 'order'); } - public setup(core: CoreSetup, { kibanaLegacy }: { kibanaLegacy: KibanaLegacySetup }) { - core.application.register({ + public setup(coreSetup: CoreSetup, { kibanaLegacy }: { kibanaLegacy: KibanaLegacySetup }) { + const { application: applicationSetup, getStartServices } = coreSetup; + + applicationSetup.register({ id: 'dev_tools', title: i18n.translate('devTools.devToolsTitle', { defaultMessage: 'Dev Tools', @@ -59,15 +63,18 @@ export class DevToolsPlugin implements Plugin { euiIconType: 'devToolsApp', order: 9001, category: DEFAULT_APP_CATEGORIES.management, - mount: async (appMountContext, params) => { - if (!this.getSortedDevTools) { - throw new Error('not started yet'); - } + mount: async (params: AppMountParameters) => { + const { element, history } = params; + element.classList.add('devAppWrapper'); + + const [core] = await getStartServices(); + const { application, chrome } = core; + const { renderApp } = await import('./application'); - params.element.classList.add('devAppWrapper'); - return renderApp(params.element, appMountContext, params.history, this.getSortedDevTools()); + return renderApp(element, application, chrome, history, this.getSortedDevTools()); }, }); + kibanaLegacy.forwardApp('dev_tools', 'dev_tools'); return { diff --git a/x-pack/plugins/grokdebugger/public/index.js b/x-pack/plugins/grokdebugger/public/index.js index 960c9d8d58e4a..d97410a2fe355 100644 --- a/x-pack/plugins/grokdebugger/public/index.js +++ b/x-pack/plugins/grokdebugger/public/index.js @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Plugin } from './plugin'; +import { GrokDebuggerUIPlugin } from './plugin'; export function plugin(initializerContext) { - return new Plugin(initializerContext); + return new GrokDebuggerUIPlugin(initializerContext); } diff --git a/x-pack/plugins/grokdebugger/public/plugin.js b/x-pack/plugins/grokdebugger/public/plugin.js index 6ac600c9dc97b..c83eb85ce4d75 100644 --- a/x-pack/plugins/grokdebugger/public/plugin.js +++ b/x-pack/plugins/grokdebugger/public/plugin.js @@ -6,10 +6,11 @@ import { i18n } from '@kbn/i18n'; import { first } from 'rxjs/operators'; -import { registerFeature } from './register_feature'; + import { PLUGIN } from '../common/constants'; +import { registerFeature } from './register_feature'; -export class Plugin { +export class GrokDebuggerUIPlugin { setup(coreSetup, plugins) { registerFeature(plugins.home); @@ -20,7 +21,7 @@ export class Plugin { }), id: PLUGIN.ID, enableRouting: false, - async mount(context, { element }) { + async mount({ element }) { const [coreStart] = await coreSetup.getStartServices(); const license = await plugins.licensing.license$.pipe(first()).toPromise(); const { renderApp } = await import('./render_app'); diff --git a/x-pack/plugins/painless_lab/public/plugin.tsx b/x-pack/plugins/painless_lab/public/plugin.tsx index 1ea6991d6023e..a5e88c8eb7fde 100644 --- a/x-pack/plugins/painless_lab/public/plugin.tsx +++ b/x-pack/plugins/painless_lab/public/plugin.tsx @@ -5,10 +5,10 @@ */ import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { Plugin, CoreStart, CoreSetup } from 'kibana/public'; import { first } from 'rxjs/operators'; import { EuiBetaBadge, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { Plugin, CoreSetup } from 'src/core/public'; import { FeatureCatalogueCategory } from '../../../../src/plugins/home/public'; @@ -27,7 +27,7 @@ const checkLicenseStatus = (license: ILicense) => { export class PainlessLabUIPlugin implements Plugin { languageService = new LanguageService(); - async setup( + public setup( { http, getStartServices, uiSettings }: CoreSetup, { devTools, home, licensing }: PluginDependencies ) { @@ -70,7 +70,7 @@ export class PainlessLabUIPlugin implements Plugin { + mount: async ({ element }) => { const [core] = await getStartServices(); const { @@ -115,9 +115,9 @@ export class PainlessLabUIPlugin implements Plugin { }; export class SearchProfilerUIPlugin implements Plugin { - constructor(ctx: PluginInitializerContext) {} - - async setup( + public setup( { http, getStartServices }: CoreSetup, { devTools, home, licensing }: AppPublicPluginDependencies ) { @@ -47,7 +45,7 @@ export class SearchProfilerUIPlugin implements Plugin { + mount: async (params) => { const [coreStart] = await getStartServices(); const { notifications, i18n: i18nDep } = coreStart; const { boot } = await import('./application/boot'); @@ -74,7 +72,7 @@ export class SearchProfilerUIPlugin implements Plugin Date: Wed, 1 Jul 2020 12:47:33 -0700 Subject: [PATCH 06/34] skip flaky suite (#70493) --- x-pack/test/functional/apps/uptime/certificates.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/uptime/certificates.ts b/x-pack/test/functional/apps/uptime/certificates.ts index c7ba7816e0255..ccf35a1e63e37 100644 --- a/x-pack/test/functional/apps/uptime/certificates.ts +++ b/x-pack/test/functional/apps/uptime/certificates.ts @@ -14,7 +14,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const es = getService('es'); - describe('certificates', function () { + // Failing: See https://github.com/elastic/kibana/issues/70493 + describe.skip('certificates', function () { before(async () => { await makeCheck({ es, tls: true }); await uptime.goToRoot(true); From f2833f6d00bce8c51da803de5f54d6e72dd32993 Mon Sep 17 00:00:00 2001 From: Dave Snider Date: Wed, 1 Jul 2020 13:07:48 -0700 Subject: [PATCH 07/34] More permissive with IE warning (#70388) Show the IE11 warning regardless of if the user supresses the warning --- src/core/public/chrome/chrome_service.tsx | 40 +- .../__snapshots__/monitor_list.test.tsx.snap | 768 +++++++----------- 2 files changed, 299 insertions(+), 509 deletions(-) diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx index 0fe3c1f083cf0..1b894bc400f08 100644 --- a/src/core/public/chrome/chrome_service.tsx +++ b/src/core/public/chrome/chrome_service.tsx @@ -185,27 +185,27 @@ export class ChromeService { /> ), }); + } - if (isIE()) { - notifications.toasts.addWarning({ - title: mountReactNode( - - - - ), - }} - /> - ), - }); - } + if (isIE()) { + notifications.toasts.addWarning({ + title: mountReactNode( + + + + ), + }} + /> + ), + }); } return { diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap index 7fc6405aaa303..d593dcc21b590 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap @@ -51,56 +51,21 @@ exports[`MonitorList component MonitorListPagination component renders a no item } } > - - - + pageSize={10} + setPageSize={[MockFunction]} + /> `; @@ -155,118 +120,83 @@ exports[`MonitorList component MonitorListPagination component renders the pagin } } > - - - + } + pageSize={10} + setPageSize={[MockFunction]} + /> `; @@ -321,56 +251,21 @@ exports[`MonitorList component renders a no items message when no data is provid } } > - - - + pageSize={10} + setPageSize={[MockFunction]} + /> `; @@ -425,119 +320,84 @@ exports[`MonitorList component renders error list 1`] = ` } } > - - - + } + pageSize={10} + setPageSize={[MockFunction]} + /> `; @@ -592,118 +452,83 @@ exports[`MonitorList component renders loading state 1`] = ` } } > - - - + } + pageSize={10} + setPageSize={[MockFunction]} + /> `; @@ -1476,117 +1301,82 @@ exports[`MonitorList component shallow renders the monitor list 1`] = ` } } > - - - + } + pageSize={10} + setPageSize={[MockFunction]} + /> `; From 3305b22d4f44c35f5ee7e31c9bf57471591ec73d Mon Sep 17 00:00:00 2001 From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> Date: Wed, 1 Jul 2020 16:22:39 -0400 Subject: [PATCH 08/34] [ENDPOINT][SIEM] Display dismissible Endpoint notice on Overview page if no endpoints are deployed (#70122) --- .../security_solution/common/constants.ts | 1 + .../use_messages_storage.test.tsx | 15 +++ .../local_storage/use_messages_storage.tsx | 10 ++ .../public/common/containers/source/index.tsx | 10 +- .../public/management/common/constants.ts | 3 + .../hooks/use_management_format_url.ts | 18 +++ .../configure_datasource.tsx | 3 +- .../pages/policy/view/policy_list.tsx | 5 +- .../components/endpoint_notice/index.tsx | 65 +++++++++++ .../public/overview/pages/overview.test.tsx | 108 ++++++++++++++++++ .../public/overview/pages/overview.tsx | 31 ++++- 11 files changed, 262 insertions(+), 7 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/management/components/hooks/use_management_format_url.ts create mode 100644 x-pack/plugins/security_solution/public/overview/components/endpoint_notice/index.tsx diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 1654c4b2a1ffc..f547bc8185d02 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -33,6 +33,7 @@ export const DEFAULT_INTERVAL_TYPE = 'manual'; export const DEFAULT_INTERVAL_VALUE = 300000; // ms export const DEFAULT_TIMEPICKER_QUICK_RANGES = 'timepicker:quickRanges'; export const NO_ALERT_INDEX = 'no-alert-index-049FC71A-4C2C-446F-9901-37XMC5024C51'; +export const ENDPOINT_METADATA_INDEX = 'metrics-endpoint.metadata-*'; export const APP_OVERVIEW_PATH = `${APP_PATH}/overview`; export const APP_ALERTS_PATH = `${APP_PATH}/alerts`; diff --git a/x-pack/plugins/security_solution/public/common/containers/local_storage/use_messages_storage.test.tsx b/x-pack/plugins/security_solution/public/common/containers/local_storage/use_messages_storage.test.tsx index d52bc4b1a267d..7085894e4a51c 100644 --- a/x-pack/plugins/security_solution/public/common/containers/local_storage/use_messages_storage.test.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/local_storage/use_messages_storage.test.tsx @@ -69,6 +69,21 @@ describe('useLocalStorage', () => { }); }); + it('should return presence of a message', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useMessagesStorage() + ); + await waitForNextUpdate(); + const { hasMessage, addMessage, removeMessage } = result.current; + addMessage('case', 'id-1'); + addMessage('case', 'id-2'); + removeMessage('case', 'id-2'); + expect(hasMessage('case', 'id-1')).toEqual(true); + expect(hasMessage('case', 'id-2')).toEqual(false); + }); + }); + it('should clear all messages', async () => { await act(async () => { const { result, waitForNextUpdate } = renderHook(() => diff --git a/x-pack/plugins/security_solution/public/common/containers/local_storage/use_messages_storage.tsx b/x-pack/plugins/security_solution/public/common/containers/local_storage/use_messages_storage.tsx index 0c96712ad9c53..7b9c3f74a18df 100644 --- a/x-pack/plugins/security_solution/public/common/containers/local_storage/use_messages_storage.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/local_storage/use_messages_storage.tsx @@ -12,6 +12,7 @@ export interface UseMessagesStorage { addMessage: (plugin: string, id: string) => void; removeMessage: (plugin: string, id: string) => void; clearAllMessages: (plugin: string) => void; + hasMessage: (plugin: string, id: string) => boolean; } export const useMessagesStorage = (): UseMessagesStorage => { @@ -30,6 +31,14 @@ export const useMessagesStorage = (): UseMessagesStorage => { [storage] ); + const hasMessage = useCallback( + (plugin: string, id: string): boolean => { + const pluginStorage = storage.get(`${plugin}-messages`) ?? []; + return pluginStorage.filter((val: string) => val === id).length > 0; + }, + [storage] + ); + const removeMessage = useCallback( (plugin: string, id: string) => { const pluginStorage = storage.get(`${plugin}-messages`) ?? []; @@ -48,5 +57,6 @@ export const useMessagesStorage = (): UseMessagesStorage => { addMessage, clearAllMessages, removeMessage, + hasMessage, }; }; diff --git a/x-pack/plugins/security_solution/public/common/containers/source/index.tsx b/x-pack/plugins/security_solution/public/common/containers/source/index.tsx index 5e80953914c97..9aa3b007511a1 100644 --- a/x-pack/plugins/security_solution/public/common/containers/source/index.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/source/index.tsx @@ -89,14 +89,18 @@ interface UseWithSourceState { loading: boolean; } -export const useWithSource = (sourceId = 'default', indexToAdd?: string[] | null) => { +export const useWithSource = ( + sourceId = 'default', + indexToAdd?: string[] | null, + onlyCheckIndexToAdd?: boolean +) => { const [configIndex] = useUiSetting$(DEFAULT_INDEX_KEY); const defaultIndex = useMemo(() => { if (indexToAdd != null && !isEmpty(indexToAdd)) { - return [...configIndex, ...indexToAdd]; + return [...(!onlyCheckIndexToAdd ? configIndex : []), ...indexToAdd]; } return configIndex; - }, [configIndex, indexToAdd]); + }, [configIndex, indexToAdd, onlyCheckIndexToAdd]); const [state, setState] = useState({ browserFields: EMPTY_BROWSER_FIELDS, diff --git a/x-pack/plugins/security_solution/public/management/common/constants.ts b/x-pack/plugins/security_solution/public/management/common/constants.ts index 7456be1d6784d..0fad1273c7279 100644 --- a/x-pack/plugins/security_solution/public/management/common/constants.ts +++ b/x-pack/plugins/security_solution/public/management/common/constants.ts @@ -4,8 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ import { ManagementStoreGlobalNamespace, ManagementSubTab } from '../types'; +import { APP_ID } from '../../../common/constants'; +import { SecurityPageName } from '../../app/types'; // --[ ROUTING ]--------------------------------------------------------------------------- +export const MANAGEMENT_APP_ID = `${APP_ID}:${SecurityPageName.management}`; export const MANAGEMENT_ROUTING_ROOT_PATH = ''; export const MANAGEMENT_ROUTING_ENDPOINTS_PATH = `${MANAGEMENT_ROUTING_ROOT_PATH}/:tabName(${ManagementSubTab.endpoints})`; export const MANAGEMENT_ROUTING_POLICIES_PATH = `${MANAGEMENT_ROUTING_ROOT_PATH}/:tabName(${ManagementSubTab.policies})`; diff --git a/x-pack/plugins/security_solution/public/management/components/hooks/use_management_format_url.ts b/x-pack/plugins/security_solution/public/management/components/hooks/use_management_format_url.ts new file mode 100644 index 0000000000000..ea7d929f6044f --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/components/hooks/use_management_format_url.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useKibana } from '../../../common/lib/kibana'; +import { MANAGEMENT_APP_ID } from '../../common/constants'; + +/** + * Returns a full URL to the provided Management page path by using + * kibana's `getUrlForApp()` + * + * @param managementPath + */ +export const useManagementFormatUrl = (managementPath: string) => { + return `${useKibana().services.application.getUrlForApp(MANAGEMENT_APP_ID)}${managementPath}`; +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/configure_datasource.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/configure_datasource.tsx index df1591bf78778..9b2b4b19ce55c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/configure_datasource.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/configure_datasource.tsx @@ -14,6 +14,7 @@ import { CustomConfigureDatasourceProps, } from '../../../../../../../ingest_manager/public'; import { getPolicyDetailPath } from '../../../../common/routing'; +import { MANAGEMENT_APP_ID } from '../../../../common/constants'; /** * Exports Endpoint-specific datasource configuration instructions @@ -59,7 +60,7 @@ export const ConfigureEndpointDatasource = memo { endpointPackageVersion ? `/endpoint-${endpointPackageVersion}/add-datasource` : '' }`, state: { - onCancelNavigateTo: ['securitySolution:management', { path: getPoliciesPath() }], + onCancelNavigateTo: [MANAGEMENT_APP_ID, { path: getPoliciesPath() }], onCancelUrl: formatUrl(getPoliciesPath()), - onSaveNavigateTo: ['securitySolution:management', { path: getPoliciesPath() }], + onSaveNavigateTo: [MANAGEMENT_APP_ID, { path: getPoliciesPath() }], }, } ); diff --git a/x-pack/plugins/security_solution/public/overview/components/endpoint_notice/index.tsx b/x-pack/plugins/security_solution/public/overview/components/endpoint_notice/index.tsx new file mode 100644 index 0000000000000..ee048f0d61212 --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/components/endpoint_notice/index.tsx @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { memo } from 'react'; +import { EuiCallOut, EuiButton, EuiButtonEmpty } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { getEndpointListPath } from '../../../management/common/routing'; +import { useNavigateToAppEventHandler } from '../../../common/hooks/endpoint/use_navigate_to_app_event_handler'; +import { useManagementFormatUrl } from '../../../management/components/hooks/use_management_format_url'; +import { MANAGEMENT_APP_ID } from '../../../management/common/constants'; + +export const EndpointNotice = memo<{ onDismiss: () => void }>(({ onDismiss }) => { + const endpointsPath = getEndpointListPath({ name: 'endpointList' }); + const endpointsLink = useManagementFormatUrl(endpointsPath); + const handleGetStartedClick = useNavigateToAppEventHandler(MANAGEMENT_APP_ID, { + path: endpointsPath, + }); + + return ( + + + + + + + } + > + <> +

+ +

+ {/* eslint-disable-next-line @elastic/eui/href-or-on-click*/} + + + + + + + +
+ ); +}); +EndpointNotice.displayName = 'EndpointNotice'; diff --git a/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx b/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx index d6e8fb984ac0f..bf5e7f0c211b1 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx @@ -11,6 +11,10 @@ import { MemoryRouter } from 'react-router-dom'; import '../../common/mock/match_media'; import { TestProviders } from '../../common/mock'; import { useWithSource } from '../../common/containers/source'; +import { + useMessagesStorage, + UseMessagesStorage, +} from '../../common/containers/local_storage/use_messages_storage'; import { Overview } from './index'; jest.mock('../../common/lib/kibana'); @@ -24,6 +28,17 @@ jest.mock('../../common/components/search_bar', () => ({ jest.mock('../../common/components/query_bar', () => ({ QueryBar: () => null, })); +jest.mock('../../common/containers/local_storage/use_messages_storage'); + +const endpointNoticeMessage = (hasMessageValue: boolean) => { + return { + hasMessage: () => hasMessageValue, + getMessages: () => [], + addMessage: () => undefined, + removeMessage: () => undefined, + clearAllMessages: () => undefined, + }; +}; describe('Overview', () => { describe('rendering', () => { @@ -32,6 +47,9 @@ describe('Overview', () => { indicesExist: false, }); + const mockuseMessagesStorage: jest.Mock = useMessagesStorage as jest.Mock; + mockuseMessagesStorage.mockImplementation(() => endpointNoticeMessage(false)); + const wrapper = mount( @@ -48,6 +66,10 @@ describe('Overview', () => { indicesExist: true, indexPattern: {}, }); + + const mockuseMessagesStorage: jest.Mock = useMessagesStorage as jest.Mock; + mockuseMessagesStorage.mockImplementation(() => endpointNoticeMessage(false)); + const wrapper = mount( @@ -57,5 +79,91 @@ describe('Overview', () => { ); expect(wrapper.find('[data-test-subj="empty-page"]').exists()).toBe(false); }); + + test('it DOES render the Endpoint banner when the endpoint index is NOT available AND storage is NOT set', async () => { + (useWithSource as jest.Mock).mockReturnValueOnce({ + indicesExist: true, + indexPattern: {}, + }); + + (useWithSource as jest.Mock).mockReturnValueOnce({ + indicesExist: false, + indexPattern: {}, + }); + + const mockuseMessagesStorage: jest.Mock = useMessagesStorage as jest.Mock; + mockuseMessagesStorage.mockImplementation(() => endpointNoticeMessage(false)); + + const wrapper = mount( + + + + + + ); + expect(wrapper.find('[data-test-subj="endpoint-prompt-banner"]').exists()).toBe(true); + }); + + test('it does NOT render the Endpoint banner when the endpoint index is NOT available but storage is set', async () => { + (useWithSource as jest.Mock).mockReturnValueOnce({ + indicesExist: true, + indexPattern: {}, + }); + + (useWithSource as jest.Mock).mockReturnValueOnce({ + indicesExist: false, + indexPattern: {}, + }); + + const mockuseMessagesStorage: jest.Mock = useMessagesStorage as jest.Mock; + mockuseMessagesStorage.mockImplementation(() => endpointNoticeMessage(true)); + + const wrapper = mount( + + + + + + ); + expect(wrapper.find('[data-test-subj="endpoint-prompt-banner"]').exists()).toBe(false); + }); + + test('it does NOT render the Endpoint banner when the endpoint index is available AND storage is set', async () => { + (useWithSource as jest.Mock).mockReturnValue({ + indicesExist: true, + indexPattern: {}, + }); + + const mockuseMessagesStorage: jest.Mock = useMessagesStorage as jest.Mock; + mockuseMessagesStorage.mockImplementation(() => endpointNoticeMessage(true)); + + const wrapper = mount( + + + + + + ); + expect(wrapper.find('[data-test-subj="endpoint-prompt-banner"]').exists()).toBe(false); + }); + + test('it does NOT render the Endpoint banner when an index IS available but storage is NOT set', async () => { + (useWithSource as jest.Mock).mockReturnValue({ + indicesExist: true, + indexPattern: {}, + }); + + const mockuseMessagesStorage: jest.Mock = useMessagesStorage as jest.Mock; + mockuseMessagesStorage.mockImplementation(() => endpointNoticeMessage(false)); + + const wrapper = mount( + + + + + + ); + expect(wrapper.find('[data-test-subj="endpoint-prompt-banner"]').exists()).toBe(false); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/overview/pages/overview.tsx b/x-pack/plugins/security_solution/public/overview/pages/overview.tsx index 53cb32a16a9de..b8b8a67024c9f 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/overview.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/overview.tsx @@ -5,7 +5,7 @@ */ import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; -import React from 'react'; +import React, { useState, useMemo } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import { StickyContainer } from 'react-sticky'; import { Query, Filter } from 'src/plugins/data/public'; @@ -26,6 +26,9 @@ import { inputsSelectors, State } from '../../common/store'; import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../common/store/inputs/actions'; import { SpyRoute } from '../../common/utils/route/spy_routes'; import { SecurityPageName } from '../../app/types'; +import { EndpointNotice } from '../components/endpoint_notice'; +import { useMessagesStorage } from '../../common/containers/local_storage/use_messages_storage'; +import { ENDPOINT_METADATA_INDEX } from '../../../common/constants'; const DEFAULT_QUERY: Query = { query: '', language: 'kuery' }; const NO_FILTERS: Filter[] = []; @@ -39,7 +42,27 @@ const OverviewComponent: React.FC = ({ query = DEFAULT_QUERY, setAbsoluteRangeDatePicker, }) => { + const endpointMetadataIndex = useMemo(() => { + return [ENDPOINT_METADATA_INDEX]; + }, []); + const { indicesExist, indexPattern } = useWithSource(); + const { indicesExist: metadataIndexExists } = useWithSource( + 'default', + endpointMetadataIndex, + true + ); + const { addMessage, hasMessage } = useMessagesStorage(); + const hasDismissEndpointNoticeMessage: boolean = useMemo( + () => hasMessage('management', 'dismissEndpointNotice'), + [hasMessage] + ); + + const [dismissMessage, setDismissMessage] = useState(hasDismissEndpointNoticeMessage); + const dismissEndpointNotice = () => { + setDismissMessage(true); + addMessage('management', 'dismissEndpointNotice'); + }; return ( <> @@ -50,6 +73,12 @@ const OverviewComponent: React.FC = ({ + {!dismissMessage && !metadataIndexExists && ( + <> + + + + )} From ba48a9facfbdebead7df4988aa8091d530dcfa47 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Wed, 1 Jul 2020 22:56:05 +0200 Subject: [PATCH 09/34] [RUM Dashboard] Added service name filter (#70349) Co-authored-by: Elastic Machine --- x-pack/plugins/apm/common/agent_name.ts | 6 +- .../app/RumDashboard/ClientMetrics/index.tsx | 6 +- .../PageLoadDistribution/index.tsx | 13 +++- .../PageLoadDistribution/use_breakdowns.ts | 6 +- .../app/RumDashboard/PageViewsTrend/index.tsx | 6 +- .../components/app/RumDashboard/index.tsx | 43 ++++++++++- .../ServiceNameFilter/index.tsx | 74 +++++++++++++++++++ .../lib/ui_filters/local_ui_filters/config.ts | 7 ++ 8 files changed, 145 insertions(+), 16 deletions(-) create mode 100644 x-pack/plugins/apm/public/components/shared/LocalUIFilters/ServiceNameFilter/index.tsx diff --git a/x-pack/plugins/apm/common/agent_name.ts b/x-pack/plugins/apm/common/agent_name.ts index 630f5739806af..9d462dad87ec0 100644 --- a/x-pack/plugins/apm/common/agent_name.ts +++ b/x-pack/plugins/apm/common/agent_name.ts @@ -30,10 +30,12 @@ export function isAgentName(agentName: string): agentName is AgentName { return AGENT_NAMES.includes(agentName as AgentName); } +export const RUM_AGENTS = ['js-base', 'rum-js']; + export function isRumAgentName( - agentName: string | undefined + agentName?: string ): agentName is 'js-base' | 'rum-js' { - return agentName === 'js-base' || agentName === 'rum-js'; + return RUM_AGENTS.includes(agentName!); } export function isJavaAgentName( diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx index 776f74a169966..df72fa604e4b3 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx @@ -22,11 +22,11 @@ const ClFlexGroup = styled(EuiFlexGroup)` export function ClientMetrics() { const { urlParams, uiFilters } = useUrlParams(); - const { start, end } = urlParams; + const { start, end, serviceName } = urlParams; const { data, status } = useFetcher( (callApmApi) => { - if (start && end) { + if (start && end && serviceName) { return callApmApi({ pathname: '/api/apm/rum/client-metrics', params: { @@ -35,7 +35,7 @@ export function ClientMetrics() { }); } }, - [start, end, uiFilters] + [start, end, serviceName, uiFilters] ); const STAT_STYLE = { width: '240px' }; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx index c6b34c8b76698..7d48cee49b104 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx @@ -27,7 +27,7 @@ export interface PercentileRange { export const PageLoadDistribution = () => { const { urlParams, uiFilters } = useUrlParams(); - const { start, end } = urlParams; + const { start, end, serviceName } = urlParams; const [percentileRange, setPercentileRange] = useState({ min: null, @@ -38,7 +38,7 @@ export const PageLoadDistribution = () => { const { data, status } = useFetcher( (callApmApi) => { - if (start && end) { + if (start && end && serviceName) { return callApmApi({ pathname: '/api/apm/rum-client/page-load-distribution', params: { @@ -57,7 +57,14 @@ export const PageLoadDistribution = () => { }); } }, - [end, start, uiFilters, percentileRange.min, percentileRange.max] + [ + end, + start, + serviceName, + uiFilters, + percentileRange.min, + percentileRange.max, + ] ); const onPercentileChange = (min: number, max: number) => { diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/use_breakdowns.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/use_breakdowns.ts index 814cf977c9569..805d19e2321d5 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/use_breakdowns.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/use_breakdowns.ts @@ -17,13 +17,13 @@ interface Props { export const useBreakdowns = ({ percentileRange, field, value }: Props) => { const { urlParams, uiFilters } = useUrlParams(); - const { start, end } = urlParams; + const { start, end, serviceName } = urlParams; const { min: minP, max: maxP } = percentileRange ?? {}; return useFetcher( (callApmApi) => { - if (start && end && field && value) { + if (start && end && serviceName && field && value) { return callApmApi({ pathname: '/api/apm/rum-client/page-load-distribution/breakdown', params: { @@ -43,6 +43,6 @@ export const useBreakdowns = ({ percentileRange, field, value }: Props) => { }); } }, - [end, start, uiFilters, field, value, minP, maxP] + [end, start, serviceName, uiFilters, field, value, minP, maxP] ); }; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx index 34347f3f95947..328b873ef8562 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx @@ -16,13 +16,13 @@ import { BreakdownItem } from '../../../../../typings/ui_filters'; export const PageViewsTrend = () => { const { urlParams, uiFilters } = useUrlParams(); - const { start, end } = urlParams; + const { start, end, serviceName } = urlParams; const [breakdowns, setBreakdowns] = useState([]); const { data, status } = useFetcher( (callApmApi) => { - if (start && end) { + if (start && end && serviceName) { return callApmApi({ pathname: '/api/apm/rum-client/page-view-trends', params: { @@ -40,7 +40,7 @@ export const PageViewsTrend = () => { }); } }, - [end, start, uiFilters, breakdowns] + [end, start, serviceName, uiFilters, breakdowns] ); const onBreakdownChange = (values: BreakdownItem[]) => { diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx index 8f21065b0dab0..c9e475ef15316 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx @@ -4,12 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiSpacer, +} from '@elastic/eui'; import React, { useMemo } from 'react'; import { useTrackPageview } from '../../../../../observability/public'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; import { PROJECTION } from '../../../../common/projections/typings'; import { RumDashboard } from './RumDashboard'; +import { ServiceNameFilter } from '../../shared/LocalUIFilters/ServiceNameFilter'; +import { useUrlParams } from '../../../hooks/useUrlParams'; +import { useFetcher } from '../../../hooks/useFetcher'; +import { RUM_AGENTS } from '../../../../common/agent_name'; export function RumOverview() { useTrackPageview({ app: 'apm', path: 'rum_overview' }); @@ -24,12 +33,42 @@ export function RumOverview() { return config; }, []); + const { + urlParams: { start, end }, + } = useUrlParams(); + + const { data } = useFetcher( + (callApmApi) => { + if (start && end) { + return callApmApi({ + pathname: '/api/apm/services', + params: { + query: { + start, + end, + uiFilters: JSON.stringify({ agentName: RUM_AGENTS }), + }, + }, + }); + } + }, + [start, end] + ); + return ( <> - + + service.serviceName) ?? [] + } + /> + + + diff --git a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/ServiceNameFilter/index.tsx b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/ServiceNameFilter/index.tsx new file mode 100644 index 0000000000000..e12a4a4831e17 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/ServiceNameFilter/index.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useEffect } from 'react'; +import { + EuiTitle, + EuiHorizontalRule, + EuiSpacer, + EuiSelect, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { history } from '../../../../utils/history'; +import { fromQuery, toQuery } from '../../Links/url_helpers'; + +interface Props { + serviceNames: string[]; +} + +const ServiceNameFilter = ({ serviceNames }: Props) => { + const { + urlParams: { serviceName }, + } = useUrlParams(); + + const options = serviceNames.map((type) => ({ + text: type, + value: type, + })); + + const updateServiceName = (serviceN: string) => { + const newLocation = { + ...history.location, + search: fromQuery({ + ...toQuery(history.location.search), + serviceName: serviceN, + }), + }; + history.push(newLocation); + }; + + useEffect(() => { + if (!serviceName && serviceNames.length > 0) { + updateServiceName(serviceNames[0]); + } + }, [serviceNames, serviceName]); + + return ( + <> + +

+ {i18n.translate('xpack.apm.localFilters.titles.serviceName', { + defaultMessage: 'Service name', + })} +

+
+ + + + { + updateServiceName(event.target.value); + }} + /> + + ); +}; + +export { ServiceNameFilter }; diff --git a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/config.ts b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/config.ts index 7a3d9d94dec8e..9f2483ab8a24e 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/config.ts +++ b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/config.ts @@ -16,6 +16,7 @@ import { USER_AGENT_DEVICE, USER_AGENT_OS, CLIENT_GEO_COUNTRY_ISO_CODE, + SERVICE_NAME, } from '../../../../common/elasticsearch_fieldnames'; const filtersByName = { @@ -85,6 +86,12 @@ const filtersByName = { }), fieldName: USER_AGENT_OS, }, + serviceName: { + title: i18n.translate('xpack.apm.localFilters.titles.serviceName', { + defaultMessage: 'Service name', + }), + fieldName: SERVICE_NAME, + }, }; export type LocalUIFilterName = keyof typeof filtersByName; From 80ae5648adbd5c69200cf10aa71439ae06fc3fcc Mon Sep 17 00:00:00 2001 From: Sandra Gonzales Date: Wed, 1 Jul 2020 17:07:14 -0400 Subject: [PATCH 10/34] [Ingest Manager] remove requirements component (#70462) * remove requirements component * fix ts error Co-authored-by: Elastic Machine --- .../sections/epm/screens/detail/content.tsx | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/content.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/content.tsx index 96aebb08e0c63..4c14e2556de09 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/content.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/content.tsx @@ -4,13 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiSpacer } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import React from 'react'; import styled from 'styled-components'; import { DEFAULT_PANEL, DetailParams } from '.'; import { PackageInfo } from '../../../../types'; import { AssetsFacetGroup } from '../../components/assets_facet_group'; -import { Requirements } from '../../components/requirements'; import { CenterColumn, LeftColumn, RightColumn } from './layout'; import { OverviewPanel } from './overview_panel'; import { SideNavLinks } from './side_nav_links'; @@ -73,17 +72,11 @@ export function ContentPanel(props: ContentPanelProps) { type RightColumnContentProps = PackageInfo & Pick; function RightColumnContent(props: RightColumnContentProps) { - const { assets, requirement, panel } = props; + const { assets, panel } = props; switch (panel) { case 'overview': return ( - - - - - - From eedb5f71155042d6eed6669dc106148913aaee4b Mon Sep 17 00:00:00 2001 From: nnamdifrankie <56440728+nnamdifrankie@users.noreply.github.com> Date: Wed, 1 Jul 2020 17:52:16 -0400 Subject: [PATCH 11/34] [SECURITY-ENDPOINT] use ingest manager unenroll services to remove unenrolled endpoint (#70393) [SECURITY-ENDPOINT] EMT-451 - use ingest manager unenroll services to remove unenrolled endpoint --- .../common/endpoint/constants.ts | 1 - .../server/endpoint/routes/metadata/index.ts | 50 +++-- .../endpoint/routes/metadata/metadata.test.ts | 115 ++++++----- .../routes/metadata/query_builders.test.ts | 12 +- .../routes/metadata/query_builders.ts | 16 +- .../routes/metadata/support/unenroll.test.ts | 179 +++++------------- .../routes/metadata/support/unenroll.ts | 128 +++---------- .../apis/endpoint/data_stream_helper.ts | 5 - .../api_integration/apis/endpoint/metadata.ts | 36 +--- .../unenroll_feature/metadata/data.json.gz | Bin 598 -> 0 bytes .../metadata_mirror/data.json.gz | Bin 535 -> 0 bytes 11 files changed, 175 insertions(+), 367 deletions(-) delete mode 100644 x-pack/test/functional/es_archives/endpoint/metadata/unenroll_feature/metadata/data.json.gz delete mode 100644 x-pack/test/functional/es_archives/endpoint/metadata/unenroll_feature/metadata_mirror/data.json.gz diff --git a/x-pack/plugins/security_solution/common/endpoint/constants.ts b/x-pack/plugins/security_solution/common/endpoint/constants.ts index 984cd7d2506a9..e311e358e6146 100644 --- a/x-pack/plugins/security_solution/common/endpoint/constants.ts +++ b/x-pack/plugins/security_solution/common/endpoint/constants.ts @@ -7,6 +7,5 @@ export const eventsIndexPattern = 'logs-endpoint.events.*'; export const alertsIndexPattern = 'logs-endpoint.alerts-*'; export const metadataIndexPattern = 'metrics-endpoint.metadata-*'; -export const metadataMirrorIndexPattern = 'metrics-endpoint.metadata_mirror-*'; export const policyIndexPattern = 'metrics-endpoint.policy-*'; export const telemetryIndexPattern = 'metrics-endpoint.telemetry-*'; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts index 7c50a10846f9a..235e7152b83cf 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts @@ -18,8 +18,8 @@ import { HostStatus, } from '../../../../common/endpoint/types'; import { EndpointAppContext } from '../../types'; -import { AgentStatus } from '../../../../../ingest_manager/common/types/models'; -import { findAllUnenrolledHostIds, findUnenrolledHostByHostId, HostId } from './support/unenroll'; +import { Agent, AgentStatus } from '../../../../../ingest_manager/common/types/models'; +import { findAllUnenrolledAgentIds } from './support/unenroll'; interface HitSource { _source: HostMetadata; @@ -70,8 +70,9 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp }, async (context, req, res) => { try { - const unenrolledHostIds = await findAllUnenrolledHostIds( - context.core.elasticsearch.legacy.client + const unenrolledAgentIds = await findAllUnenrolledAgentIds( + endpointAppContext.service.getAgentService(), + context.core.savedObjects.client ); const queryParams = await kibanaRequestToMetadataListESQuery( @@ -79,9 +80,10 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp endpointAppContext, metadataIndexPattern, { - unenrolledHostIds: unenrolledHostIds.map((host: HostId) => host.host.id), + unenrolledAgentIds, } ); + const response = (await context.core.elasticsearch.legacy.client.callAsCurrentUser( 'search', queryParams @@ -138,13 +140,6 @@ export async function getHostData( metadataRequestContext: MetadataRequestContext, id: string ): Promise { - const unenrolledHostId = await findUnenrolledHostByHostId( - metadataRequestContext.requestHandlerContext.core.elasticsearch.legacy.client, - id - ); - if (unenrolledHostId) { - throw Boom.badRequest('the requested endpoint is unenrolled'); - } const query = getESQueryHostMetadataByID(id, metadataIndexPattern); const response = (await metadataRequestContext.requestHandlerContext.core.elasticsearch.legacy.client.callAsCurrentUser( 'search', @@ -155,7 +150,36 @@ export async function getHostData( return undefined; } - return enrichHostMetadata(response.hits.hits[0]._source, metadataRequestContext); + const hostMetadata: HostMetadata = response.hits.hits[0]._source; + const agent = await findAgent(metadataRequestContext, hostMetadata); + + if (agent && !agent.active) { + throw Boom.badRequest('the requested endpoint is unenrolled'); + } + + return enrichHostMetadata(hostMetadata, metadataRequestContext); +} + +async function findAgent( + metadataRequestContext: MetadataRequestContext, + hostMetadata: HostMetadata +): Promise { + const logger = metadataRequestContext.endpointAppContext.logFactory.get('metadata'); + try { + return await metadataRequestContext.endpointAppContext.service + .getAgentService() + .getAgent( + metadataRequestContext.requestHandlerContext.core.savedObjects.client, + hostMetadata.elastic.agent.id + ); + } catch (e) { + if (e.isBoom && e.output.statusCode === 404) { + logger.warn(`agent with id ${hostMetadata.elastic.agent.id} not found`); + return undefined; + } else { + throw e; + } + } } async function mapToHostResultList( diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts index f6ae2c584a346..668911b8d1f29 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts @@ -35,7 +35,7 @@ import Boom from 'boom'; import { EndpointAppContextService } from '../../endpoint_app_context_services'; import { createMockConfig } from '../../../lib/detection_engine/routes/__mocks__'; import { EndpointDocGenerator } from '../../../../common/endpoint/generate_data'; -import { HostId } from './support/unenroll'; +import { Agent } from '../../../../../ingest_manager/common/types/models'; describe('test endpoint route', () => { let routerMock: jest.Mocked; @@ -51,12 +51,12 @@ describe('test endpoint route', () => { typeof createMockEndpointAppContextServiceStartContract >['agentService']; let endpointAppContextService: EndpointAppContextService; - const noUnenrolledEndpoint = () => - Promise.resolve(({ - hits: { - hits: [], - }, - } as unknown) as SearchResponse); + const noUnenrolledAgent = { + agents: [], + total: 0, + page: 1, + perPage: 1, + }; beforeEach(() => { mockClusterClient = elasticsearchServiceMock.createClusterClient() as jest.Mocked< @@ -84,20 +84,19 @@ describe('test endpoint route', () => { it('test find the latest of all endpoints', async () => { const mockRequest = httpServerMock.createKibanaRequest({}); const response = createSearchResponse(new EndpointDocGenerator().generateHostMetadata()); - mockScopedClient.callAsCurrentUser - .mockImplementationOnce(noUnenrolledEndpoint) - .mockImplementationOnce(() => Promise.resolve(response)); + mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); [routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) => path.startsWith('/api/endpoint/metadata') )!; mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error'); + mockAgentService.listAgents = jest.fn().mockReturnValue(noUnenrolledAgent); await routeHandler( createRouteHandlerContext(mockScopedClient, mockSavedObjectClient), mockRequest, mockResponse ); - expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(2); + expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1); expect(routeConfig.options).toEqual({ authRequired: true }); expect(mockResponse.ok).toBeCalled(); const endpointResultList = mockResponse.ok.mock.calls[0][0]?.body as HostResultList; @@ -122,11 +121,10 @@ describe('test endpoint route', () => { }); mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error'); - mockScopedClient.callAsCurrentUser - .mockImplementationOnce(noUnenrolledEndpoint) - .mockImplementationOnce(() => - Promise.resolve(createSearchResponse(new EndpointDocGenerator().generateHostMetadata())) - ); + mockAgentService.listAgents = jest.fn().mockReturnValue(noUnenrolledAgent); + mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => + Promise.resolve(createSearchResponse(new EndpointDocGenerator().generateHostMetadata())) + ); [routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) => path.startsWith('/api/endpoint/metadata') )!; @@ -137,8 +135,8 @@ describe('test endpoint route', () => { mockResponse ); - expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(2); - expect(mockScopedClient.callAsCurrentUser.mock.calls[1][1]?.body?.query).toEqual({ + expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1); + expect(mockScopedClient.callAsCurrentUser.mock.calls[0][1]?.body?.query).toEqual({ match_all: {}, }); expect(routeConfig.options).toEqual({ authRequired: true }); @@ -167,11 +165,10 @@ describe('test endpoint route', () => { }); mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error'); - mockScopedClient.callAsCurrentUser - .mockImplementationOnce(noUnenrolledEndpoint) - .mockImplementationOnce(() => - Promise.resolve(createSearchResponse(new EndpointDocGenerator().generateHostMetadata())) - ); + mockAgentService.listAgents = jest.fn().mockReturnValue(noUnenrolledAgent); + mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => + Promise.resolve(createSearchResponse(new EndpointDocGenerator().generateHostMetadata())) + ); [routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) => path.startsWith('/api/endpoint/metadata') )!; @@ -183,7 +180,7 @@ describe('test endpoint route', () => { ); expect(mockScopedClient.callAsCurrentUser).toBeCalled(); - expect(mockScopedClient.callAsCurrentUser.mock.calls[1][1]?.body?.query).toEqual({ + expect(mockScopedClient.callAsCurrentUser.mock.calls[0][1]?.body?.query).toEqual({ bool: { must: [ { @@ -218,11 +215,15 @@ describe('test endpoint route', () => { it('should return 404 on no results', async () => { const mockRequest = httpServerMock.createKibanaRequest({ params: { id: 'BADID' } }); - mockScopedClient.callAsCurrentUser - .mockImplementationOnce(noUnenrolledEndpoint) - .mockImplementationOnce(() => Promise.resolve(createSearchResponse())); + mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => + Promise.resolve(createSearchResponse()) + ); mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error'); + mockAgentService.getAgent = jest.fn().mockReturnValue(({ + active: true, + } as unknown) as Agent); + [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => path.startsWith('/api/endpoint/metadata') )!; @@ -232,7 +233,7 @@ describe('test endpoint route', () => { mockResponse ); - expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(2); + expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1); expect(routeConfig.options).toEqual({ authRequired: true }); expect(mockResponse.notFound).toBeCalled(); const message = mockResponse.notFound.mock.calls[0][0]?.body; @@ -246,9 +247,10 @@ describe('test endpoint route', () => { }); mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('online'); - mockScopedClient.callAsCurrentUser - .mockImplementationOnce(noUnenrolledEndpoint) - .mockImplementationOnce(() => Promise.resolve(response)); + mockAgentService.getAgent = jest.fn().mockReturnValue(({ + active: true, + } as unknown) as Agent); + mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => path.startsWith('/api/endpoint/metadata') @@ -260,7 +262,7 @@ describe('test endpoint route', () => { mockResponse ); - expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(2); + expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1); expect(routeConfig.options).toEqual({ authRequired: true }); expect(mockResponse.ok).toBeCalled(); const result = mockResponse.ok.mock.calls[0][0]?.body as HostInfo; @@ -279,9 +281,11 @@ describe('test endpoint route', () => { throw Boom.notFound('Agent not found'); }); - mockScopedClient.callAsCurrentUser - .mockImplementationOnce(noUnenrolledEndpoint) - .mockImplementationOnce(() => Promise.resolve(response)); + mockAgentService.getAgent = jest.fn().mockImplementation(() => { + throw Boom.notFound('Agent not found'); + }); + + mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => path.startsWith('/api/endpoint/metadata') @@ -293,7 +297,7 @@ describe('test endpoint route', () => { mockResponse ); - expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(2); + expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1); expect(routeConfig.options).toEqual({ authRequired: true }); expect(mockResponse.ok).toBeCalled(); const result = mockResponse.ok.mock.calls[0][0]?.body as HostInfo; @@ -308,10 +312,10 @@ describe('test endpoint route', () => { }); mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('warning'); - - mockScopedClient.callAsCurrentUser - .mockImplementationOnce(noUnenrolledEndpoint) - .mockImplementationOnce(() => Promise.resolve(response)); + mockAgentService.getAgent = jest.fn().mockReturnValue(({ + active: true, + } as unknown) as Agent); + mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => path.startsWith('/api/endpoint/metadata') @@ -323,36 +327,23 @@ describe('test endpoint route', () => { mockResponse ); - expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(2); + expect(mockScopedClient.callAsCurrentUser).toHaveBeenCalledTimes(1); expect(routeConfig.options).toEqual({ authRequired: true }); expect(mockResponse.ok).toBeCalled(); const result = mockResponse.ok.mock.calls[0][0]?.body as HostInfo; expect(result.host_status).toEqual(HostStatus.ERROR); }); - it('should throw error when endpoint is unenrolled', async () => { + it('should throw error when endpoint egent is not active', async () => { + const response = createSearchResponse(new EndpointDocGenerator().generateHostMetadata()); + const mockRequest = httpServerMock.createKibanaRequest({ - params: { id: 'hostId' }, + params: { id: response.hits.hits[0]._id }, }); - - mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => - Promise.resolve(({ - hits: { - hits: [ - { - _index: 'metrics-endpoint.metadata_mirror-default', - _id: 'S5M1yHIBLSMVtiLw6Wpr', - _score: 0.0, - _source: { - host: { - id: 'hostId', - }, - }, - }, - ], - }, - } as unknown) as SearchResponse) - ); + mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); + mockAgentService.getAgent = jest.fn().mockReturnValue(({ + active: false, + } as unknown) as Agent); [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => path.startsWith('/api/endpoint/metadata') diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.test.ts index a5b578f3f9813..266d522e8a41d 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.test.ts @@ -63,7 +63,7 @@ describe('query builder', () => { 'test default query params for all endpoints metadata when no params or body is provided ' + 'with unenrolled host ids excluded', async () => { - const unenrolledHostId = '1fdca33f-799f-49f4-939c-ea4383c77672'; + const unenrolledElasticAgentId = '1fdca33f-799f-49f4-939c-ea4383c77672'; const mockRequest = httpServerMock.createKibanaRequest({ body: {}, }); @@ -76,7 +76,7 @@ describe('query builder', () => { }, metadataIndexPattern, { - unenrolledHostIds: [unenrolledHostId], + unenrolledAgentIds: [unenrolledElasticAgentId], } ); @@ -86,7 +86,7 @@ describe('query builder', () => { bool: { must_not: { terms: { - 'host.id': ['1fdca33f-799f-49f4-939c-ea4383c77672'], + 'elastic.agent.id': [unenrolledElasticAgentId], }, }, }, @@ -198,7 +198,7 @@ describe('query builder', () => { 'test default query params for all endpoints endpoint metadata excluding unerolled endpoint ' + 'and when body filter is provided', async () => { - const unenrolledHostId = '1fdca33f-799f-49f4-939c-ea4383c77672'; + const unenrolledElasticAgentId = '1fdca33f-799f-49f4-939c-ea4383c77672'; const mockRequest = httpServerMock.createKibanaRequest({ body: { filter: 'not host.ip:10.140.73.246', @@ -213,7 +213,7 @@ describe('query builder', () => { }, metadataIndexPattern, { - unenrolledHostIds: [unenrolledHostId], + unenrolledAgentIds: [unenrolledElasticAgentId], } ); @@ -226,7 +226,7 @@ describe('query builder', () => { bool: { must_not: { terms: { - 'host.id': [unenrolledHostId], + 'elastic.agent.id': [unenrolledElasticAgentId], }, }, }, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts index b6ec91675f248..f6385d2710047 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts @@ -8,7 +8,7 @@ import { esKuery } from '../../../../../../../src/plugins/data/server'; import { EndpointAppContext } from '../../types'; export interface QueryBuilderOptions { - unenrolledHostIds?: string[]; + unenrolledAgentIds?: string[]; } export async function kibanaRequestToMetadataListESQuery( @@ -22,7 +22,7 @@ export async function kibanaRequestToMetadataListESQuery( const pagingProperties = await getPagingProperties(request, endpointAppContext); return { body: { - query: buildQueryBody(request, queryBuilderOptions?.unenrolledHostIds!), + query: buildQueryBody(request, queryBuilderOptions?.unenrolledAgentIds!), collapse: { field: 'host.id', inner_hits: { @@ -76,21 +76,21 @@ async function getPagingProperties( function buildQueryBody( // eslint-disable-next-line @typescript-eslint/no-explicit-any request: KibanaRequest, - unerolledHostIds: string[] | undefined + unerolledAgentIds: string[] | undefined // eslint-disable-next-line @typescript-eslint/no-explicit-any ): Record { - const filterUnenrolledHosts = unerolledHostIds && unerolledHostIds.length > 0; + const filterUnenrolledAgents = unerolledAgentIds && unerolledAgentIds.length > 0; if (typeof request?.body?.filter === 'string') { const kqlQuery = esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(request.body.filter)); return { bool: { - must: filterUnenrolledHosts + must: filterUnenrolledAgents ? [ { bool: { must_not: { terms: { - 'host.id': unerolledHostIds, + 'elastic.agent.id': unerolledAgentIds, }, }, }, @@ -107,12 +107,12 @@ function buildQueryBody( }, }; } - return filterUnenrolledHosts + return filterUnenrolledAgents ? { bool: { must_not: { terms: { - 'host.id': unerolledHostIds, + 'elastic.agent.id': unerolledAgentIds, }, }, }, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/unenroll.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/unenroll.test.ts index 545095a6a0c16..30c8f14287cae 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/unenroll.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/unenroll.test.ts @@ -4,144 +4,57 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ILegacyScopedClusterClient } from 'kibana/server'; -import { - findAllUnenrolledHostIds, - fetchAllUnenrolledHostIdsWithScroll, - HostId, - findUnenrolledHostByHostId, -} from './unenroll'; -import { elasticsearchServiceMock } from '../../../../../../../../src/core/server/mocks'; -import { SearchResponse } from 'elasticsearch'; -import { metadataMirrorIndexPattern } from '../../../../../common/endpoint/constants'; -import { EndpointStatus } from '../../../../../common/endpoint/types'; - -const noUnenrolledEndpoint = () => - Promise.resolve(({ - hits: { - hits: [], - }, - } as unknown) as SearchResponse); - -describe('test find all unenrolled HostId', () => { - let mockScopedClient: jest.Mocked; - - it('can find all hits with scroll', async () => { - const firstHostId = '1fdca33f-799f-49f4-939c-ea4383c77671'; - const secondHostId = '2fdca33f-799f-49f4-939c-ea4383c77672'; - mockScopedClient = elasticsearchServiceMock.createScopedClusterClient(); - mockScopedClient.callAsCurrentUser - .mockImplementationOnce(() => Promise.resolve(createSearchResponse(secondHostId, 'scrollId'))) - .mockImplementationOnce(noUnenrolledEndpoint); - - const initialResponse = createSearchResponse(firstHostId, 'initialScrollId'); - const hostIds = await fetchAllUnenrolledHostIdsWithScroll( - initialResponse, - mockScopedClient.callAsCurrentUser - ); - - expect(hostIds).toEqual([{ host: { id: firstHostId } }, { host: { id: secondHostId } }]); +import { SavedObjectsClientContract } from 'kibana/server'; +import { findAllUnenrolledAgentIds } from './unenroll'; +import { savedObjectsClientMock } from '../../../../../../../../src/core/server/mocks'; +import { AgentService } from '../../../../../../ingest_manager/server/services'; +import { createMockAgentService } from '../../../mocks'; +import { Agent } from '../../../../../../ingest_manager/common/types/models'; + +describe('test find all unenrolled Agent id', () => { + let mockSavedObjectClient: jest.Mocked; + let mockAgentService: jest.Mocked; + beforeEach(() => { + mockSavedObjectClient = savedObjectsClientMock.create(); + mockAgentService = createMockAgentService(); }); - it('can find all unerolled endpoint host ids', async () => { - const firstEndpointHostId = '1fdca33f-799f-49f4-939c-ea4383c77671'; - const secondEndpointHostId = '2fdca33f-799f-49f4-939c-ea4383c77672'; - mockScopedClient = elasticsearchServiceMock.createScopedClusterClient(); - mockScopedClient.callAsCurrentUser + it('can find all unerolled endpoint agent ids', async () => { + mockAgentService.listAgents .mockImplementationOnce(() => - Promise.resolve(createSearchResponse(firstEndpointHostId, 'initialScrollId')) + Promise.resolve({ + agents: [ + ({ + id: 'id1', + } as unknown) as Agent, + ], + total: 2, + page: 1, + perPage: 1, + }) ) .mockImplementationOnce(() => - Promise.resolve(createSearchResponse(secondEndpointHostId, 'scrollId')) - ) - .mockImplementationOnce(noUnenrolledEndpoint); - const hostIds = await findAllUnenrolledHostIds(mockScopedClient); - - expect(mockScopedClient.callAsCurrentUser.mock.calls[0][1]).toEqual({ - index: metadataMirrorIndexPattern, - scroll: '30s', - body: { - size: 1000, - _source: ['host.id'], - query: { - bool: { - filter: { - term: { - 'Endpoint.status': EndpointStatus.unenrolled, - }, - }, - }, - }, - }, - }); - expect(hostIds).toEqual([ - { host: { id: firstEndpointHostId } }, - { host: { id: secondEndpointHostId } }, - ]); - }); -}); - -describe('test find unenrolled endpoint host id by hostId', () => { - let mockScopedClient: jest.Mocked; - - it('can find unenrolled endpoint by the host id when unenrolled', async () => { - const firstEndpointHostId = '1fdca33f-799f-49f4-939c-ea4383c77671'; - mockScopedClient = elasticsearchServiceMock.createScopedClusterClient(); - mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => - Promise.resolve(createSearchResponse(firstEndpointHostId, 'initialScrollId')) - ); - const endpointHostId = await findUnenrolledHostByHostId(mockScopedClient, firstEndpointHostId); - expect(mockScopedClient.callAsCurrentUser.mock.calls[0][1]?.index).toEqual( - metadataMirrorIndexPattern - ); - expect(mockScopedClient.callAsCurrentUser.mock.calls[0][1]?.body).toEqual({ - size: 1, - _source: ['host.id'], - query: { - bool: { - filter: [ - { - term: { - 'Endpoint.status': EndpointStatus.unenrolled, - }, - }, - { - term: { - 'host.id': firstEndpointHostId, - }, - }, + Promise.resolve({ + agents: [ + ({ + id: 'id2', + } as unknown) as Agent, ], - }, - }, - }); - expect(endpointHostId).toEqual({ host: { id: firstEndpointHostId } }); - }); - - it('find unenrolled endpoint host by the host id return undefined when no unenrolled host', async () => { - const firstHostId = '1fdca33f-799f-49f4-939c-ea4383c77671'; - mockScopedClient = elasticsearchServiceMock.createScopedClusterClient(); - mockScopedClient.callAsCurrentUser.mockImplementationOnce(noUnenrolledEndpoint); - const hostId = await findUnenrolledHostByHostId(mockScopedClient, firstHostId); - expect(hostId).toBeFalsy(); + total: 2, + page: 1, + perPage: 1, + }) + ) + .mockImplementationOnce(() => + Promise.resolve({ + agents: [], + total: 2, + page: 1, + perPage: 1, + }) + ); + const agentIds = await findAllUnenrolledAgentIds(mockAgentService, mockSavedObjectClient); + expect(agentIds).toBeTruthy(); + expect(agentIds).toEqual(['id1', 'id2']); }); }); - -function createSearchResponse(hostId: string, scrollId: string): SearchResponse { - return ({ - hits: { - hits: [ - { - _index: metadataMirrorIndexPattern, - _id: 'S5M1yHIBLSMVtiLw6Wpr', - _score: 0.0, - _source: { - host: { - id: hostId, - }, - }, - }, - ], - }, - _scroll_id: scrollId, - } as unknown) as SearchResponse; -} diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/unenroll.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/unenroll.ts index 332f969ddf7e5..bba9d921310da 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/unenroll.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/unenroll.ts @@ -4,113 +4,33 @@ * you may not use this file except in compliance with the Elastic License. */ -import { LegacyAPICaller, ILegacyScopedClusterClient } from 'kibana/server'; -import { SearchResponse } from 'elasticsearch'; -import { metadataMirrorIndexPattern } from '../../../../../common/endpoint/constants'; -import { EndpointStatus } from '../../../../../common/endpoint/types'; - -const KEEPALIVE = '30s'; -const SIZE = 1000; - -export interface HostId { - host: { - id: string; - }; -} - -interface HitSource { - _source: HostId; -} - -export async function findUnenrolledHostByHostId( - client: ILegacyScopedClusterClient, - hostId: string -): Promise { - const queryParams = { - index: metadataMirrorIndexPattern, - body: { - size: 1, - _source: ['host.id'], - query: { - bool: { - filter: [ - { - term: { - 'Endpoint.status': EndpointStatus.unenrolled, - }, - }, - { - term: { - 'host.id': hostId, - }, - }, - ], - }, - }, - }, - }; - - const response = (await client.callAsCurrentUser('search', queryParams)) as SearchResponse< - HostId - >; - const newHits = response.hits?.hits || []; - - if (newHits.length > 0) { - const hostIds = newHits.map((hitSource: HitSource) => hitSource._source); - return hostIds[0]; - } else { - return undefined; - } -} - -export async function findAllUnenrolledHostIds( - client: ILegacyScopedClusterClient -): Promise { - const queryParams = { - index: metadataMirrorIndexPattern, - scroll: KEEPALIVE, - body: { - size: SIZE, - _source: ['host.id'], - query: { - bool: { - filter: { - term: { - 'Endpoint.status': EndpointStatus.unenrolled, - }, - }, - }, - }, - }, +import { SavedObjectsClientContract } from 'kibana/server'; +import { AgentService } from '../../../../../../ingest_manager/server'; +import { Agent } from '../../../../../../ingest_manager/common/types/models'; + +export async function findAllUnenrolledAgentIds( + agentService: AgentService, + soClient: SavedObjectsClientContract, + pageSize: number = 1000 +): Promise { + const searchOptions = (pageNum: number) => { + return { + page: pageNum, + perPage: pageSize, + showInactive: true, + kuery: 'fleet-agents.packages:endpoint AND fleet-agents.active:false', + }; }; - const response = (await client.callAsCurrentUser('search', queryParams)) as SearchResponse< - HostId - >; - - return fetchAllUnenrolledHostIdsWithScroll(response, client.callAsCurrentUser); -} - -export async function fetchAllUnenrolledHostIdsWithScroll( - response: SearchResponse, - client: LegacyAPICaller, - hits: HostId[] = [] -): Promise { - let newHits = response.hits?.hits || []; - let scrollId = response._scroll_id; - while (newHits.length > 0) { - const hostIds: HostId[] = newHits.map((hitSource: HitSource) => hitSource._source); - hits.push(...hostIds); + let page = 1; - const innerResponse = await client('scroll', { - body: { - scroll: KEEPALIVE, - scroll_id: scrollId, - }, - }); + const result: string[] = []; + let hasMore = true; - newHits = innerResponse.hits?.hits || []; - scrollId = innerResponse._scroll_id; + while (hasMore) { + const unenrolledAgents = await agentService.listAgents(soClient, searchOptions(page++)); + result.push(...unenrolledAgents.agents.map((agent: Agent) => agent.id)); + hasMore = unenrolledAgents.agents.length > 0; } - return hits; + return result; } diff --git a/x-pack/test/api_integration/apis/endpoint/data_stream_helper.ts b/x-pack/test/api_integration/apis/endpoint/data_stream_helper.ts index d2e99a80ef8a1..b239ab41e41f1 100644 --- a/x-pack/test/api_integration/apis/endpoint/data_stream_helper.ts +++ b/x-pack/test/api_integration/apis/endpoint/data_stream_helper.ts @@ -10,7 +10,6 @@ import { eventsIndexPattern, alertsIndexPattern, policyIndexPattern, - metadataMirrorIndexPattern, } from '../../../../plugins/security_solution/common/endpoint/constants'; export async function deleteDataStream(getService: (serviceName: 'es') => Client, index: string) { @@ -30,10 +29,6 @@ export async function deleteMetadataStream(getService: (serviceName: 'es') => Cl await deleteDataStream(getService, metadataIndexPattern); } -export async function deleteMetadataMirrorStream(getService: (serviceName: 'es') => Client) { - await deleteDataStream(getService, metadataMirrorIndexPattern); -} - export async function deleteEventsStream(getService: (serviceName: 'es') => Client) { await deleteDataStream(getService, eventsIndexPattern); } diff --git a/x-pack/test/api_integration/apis/endpoint/metadata.ts b/x-pack/test/api_integration/apis/endpoint/metadata.ts index 0d77486e07536..41531269ddeb9 100644 --- a/x-pack/test/api_integration/apis/endpoint/metadata.ts +++ b/x-pack/test/api_integration/apis/endpoint/metadata.ts @@ -5,7 +5,7 @@ */ import expect from '@kbn/expect/expect.js'; import { FtrProviderContext } from '../../ftr_provider_context'; -import { deleteMetadataMirrorStream, deleteMetadataStream } from './data_stream_helper'; +import { deleteMetadataStream } from './data_stream_helper'; /** * The number of host documents in the es archive. @@ -33,40 +33,6 @@ export default function ({ getService }: FtrProviderContext) { }); }); - describe('POST /api/endpoint/metadata when metadata mirror index contains unenrolled host', () => { - before(async () => { - await esArchiver.load('endpoint/metadata/unenroll_feature/metadata', { useCreate: true }); - await esArchiver.load('endpoint/metadata/unenroll_feature/metadata_mirror', { - useCreate: true, - }); - }); - - after(async () => { - await deleteMetadataStream(getService); - await deleteMetadataMirrorStream(getService); - }); - - it('metadata api should return only enrolled host', async () => { - const { body } = await supertest - .post('/api/endpoint/metadata') - .set('kbn-xsrf', 'xxx') - .send() - .expect(200); - expect(body.total).to.eql(1); - expect(body.hosts.length).to.eql(1); - expect(body.request_page_size).to.eql(10); - expect(body.request_page_index).to.eql(0); - }); - - it('metadata api should return 400 when an unenrolled host is retrieved', async () => { - const { body } = await supertest - .get('/api/endpoint/metadata/1fdca33f-799f-49f4-939c-ea4383c77671') - .send() - .expect(400); - expect(body.message).to.eql('the requested endpoint is unenrolled'); - }); - }); - describe('POST /api/endpoint/metadata when index is not empty', () => { before( async () => await esArchiver.load('endpoint/metadata/api_feature', { useCreate: true }) diff --git a/x-pack/test/functional/es_archives/endpoint/metadata/unenroll_feature/metadata/data.json.gz b/x-pack/test/functional/es_archives/endpoint/metadata/unenroll_feature/metadata/data.json.gz deleted file mode 100644 index d7b130e4051569b283b25ab9337bcc07cd4eeb16..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 598 zcmV-c0;&BUiwFpV`RrZ*17u-zVJ>QOZ*BnXR7sE1FciM$S5!IImbWbK35@_vkvK6D zFo=WWWw4r!EG;vm{&$>C(`KnOED{I62P^*Gd-nI1e2?B@;WziC_E!sE71CdJz*eMf zhdjE2J6hFQ4XHjpT(7Uf8}Mdl>D9wpzCO5j9=X!rI;TuGm6bKnxhe~rH_!n>iADgW zjcC&b;6A1<+De{Zamb6tX1Z=fRyq_1oG=o`h@v=L_AalE_YT4wS{A95_an@qqAXLZ z)dW7}gN_Sa*!tx!$C0_n4wZWOl+4uZxHoNmD3-8kTWNn_-=Dts=deMD&Z{C#9ba$a z<%>H#&G;z=91%m3C;~{(05BRvAPI2*bYfJX5sR3?1CFOg_uU!Vwz{fqk$2`05=iDW zbSmn`$}y2Sw=+8=IYUVT6pbZdA!0x%Vt^t9#Yje!hU8qJ{rtV{ENxk7(HvUp6GU9E zLV)=VrmFz0f*5knZs)we6!qkq4(VHY?Y@ECB|J;%MtmKX(WuC_o8C{ua$p1rLeO;!Vva{c)7dbElt4N+5XxX2LmcCCnLZC*%7mOg zl}KPBU^L(i&=9E0fki#-m}%3rOZL6{lZ#!wc&95j5DS7Z8Pn>^wmUkySs6QQMPeSn{{P16=M&>`QQ{;J_BL9L;u@#~#5<-ON k@8}fU-08Vak>_=a{De-CU)3C~{}R#p4wUF`<{SwC0D?arZ2$lO diff --git a/x-pack/test/functional/es_archives/endpoint/metadata/unenroll_feature/metadata_mirror/data.json.gz b/x-pack/test/functional/es_archives/endpoint/metadata/unenroll_feature/metadata_mirror/data.json.gz deleted file mode 100644 index 3b4da7c47d9f22017c0b80365a9e613e90dbbf5e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 535 zcmV+y0_go8iwFpu`RrZ*17u-zVJ>QOZ*Bl>Q^{`IFc7`_D-4}80yi!6sR&vi>9qlh zwkQIMOG`vsDamdc_}@!9maWA+5aOFR;^e=F$NTgNJ|8T-|MqCQ6Fo3$rT+#}rF&;(2f9{mW9vTlfKZ|r&y{tqaiFvj zL)il!Q@dtx^7@!ZKJ>QIT`#KEqd4J&ku*mX<>}o>`E^lAla!&>wQI`KE8Z-4 zk@%&THNO{uGh#@QWq<@tfYBs_BE<>l!l*;Bzzi)#Whn)%?r!5#`;mGnjYnYQFyhEY;bY9Qm>0ON)Mr(A*- zjOJ8kS(?q7Y{UHin6?9>m>?8;w_?okY-~ad)0mQ&t^Ti-A-(}rDJ9&%TVlB|4TQAZu><$KM-4jFqz95+jck;{jAIhd*Q4&`L?)h ZL7R=+jWO7a`*CyJ{0IMZTXL`j004KW02=@R From 341f38577f6877456f1bfacaf65eceefd2b066f8 Mon Sep 17 00:00:00 2001 From: Angela Chuang <6295984+angorayc@users.noreply.github.com> Date: Wed, 1 Jul 2020 23:46:26 +0100 Subject: [PATCH 12/34] fix export response (#70473) * fix export response * update unit tests --- .../generic_downloader/index.test.tsx | 22 ++- .../generic_downloader/translations.ts | 4 +- .../public/timelines/containers/api.test.ts | 142 ++++++++++++++++-- .../public/timelines/containers/api.ts | 9 +- 4 files changed, 156 insertions(+), 21 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/generic_downloader/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/generic_downloader/index.test.tsx index a70772911ba60..fb5dd915033b5 100644 --- a/x-pack/plugins/security_solution/public/common/components/generic_downloader/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/generic_downloader/index.test.tsx @@ -4,9 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { shallow } from 'enzyme'; +import { shallow, mount } from 'enzyme'; import React from 'react'; -import { GenericDownloaderComponent } from './index'; +import { GenericDownloaderComponent, ExportSelectedData } from './index'; +import { errorToToaster } from '../toasters'; + +jest.mock('../toasters', () => ({ + useStateToaster: jest.fn(() => [jest.fn(), jest.fn()]), + errorToToaster: jest.fn(), +})); describe('GenericDownloader', () => { test('renders correctly against snapshot', () => { @@ -19,4 +25,16 @@ describe('GenericDownloader', () => { ); expect(wrapper).toMatchSnapshot(); }); + + test('show toaster with correct error message if error occurrs', () => { + mount( + + ); + expect((errorToToaster as jest.Mock).mock.calls[0][0].title).toEqual('Failed to export data…'); + }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/generic_downloader/translations.ts b/x-pack/plugins/security_solution/public/common/components/generic_downloader/translations.ts index 867c908bbacd3..a87dce8c81c56 100644 --- a/x-pack/plugins/security_solution/public/common/components/generic_downloader/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/generic_downloader/translations.ts @@ -7,8 +7,8 @@ import { i18n } from '@kbn/i18n'; export const EXPORT_FAILURE = i18n.translate( - 'xpack.securitySolution.detectionEngine.rules.components.ruleDownloader.exportFailureTitle', + 'xpack.securitySolution.detectionEngine.rules.components.genericDownloader.exportFailureTitle', { - defaultMessage: 'Failed to export rules…', + defaultMessage: 'Failed to export data…', } ); diff --git a/x-pack/plugins/security_solution/public/timelines/containers/api.test.ts b/x-pack/plugins/security_solution/public/timelines/containers/api.test.ts index 8a2f91d7171f7..089a428f7dfaf 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/api.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/containers/api.test.ts @@ -7,10 +7,11 @@ import * as api from './api'; import { KibanaServices } from '../../common/lib/kibana'; import { TimelineType, TimelineStatus } from '../../../common/types/timeline'; import { TIMELINE_DRAFT_URL, TIMELINE_URL } from '../../../common/constants'; +import { ImportDataProps } from '../../alerts/containers/detection_engine/rules/types'; jest.mock('../../common/lib/kibana', () => { return { - KibanaServices: { get: jest.fn() }, + KibanaServices: { get: jest.fn(() => ({ http: { fetch: jest.fn() } })) }, }; }); @@ -173,6 +174,7 @@ describe('persistTimeline', () => { beforeAll(() => { jest.resetAllMocks(); + jest.resetModules(); (KibanaServices.get as jest.Mock).mockReturnValue({ http: { @@ -188,10 +190,6 @@ describe('persistTimeline', () => { }); }); - afterAll(() => { - jest.resetAllMocks(); - }); - test('it should create a draft timeline if given status is draft and timelineId is null', () => { expect(postMock).toHaveBeenCalledWith(TIMELINE_DRAFT_URL, { body: JSON.stringify({ @@ -334,6 +332,7 @@ describe('persistTimeline', () => { beforeAll(() => { jest.resetAllMocks(); + jest.resetModules(); (KibanaServices.get as jest.Mock).mockReturnValue({ http: { @@ -345,10 +344,6 @@ describe('persistTimeline', () => { api.persistTimeline({ timelineId, timeline: importTimeline, version }); }); - afterAll(() => { - jest.resetAllMocks(); - }); - test('it should update timeline', () => { expect(postMock.mock.calls[0][0]).toEqual(TIMELINE_URL); }); @@ -474,6 +469,7 @@ describe('persistTimeline', () => { beforeAll(() => { jest.resetAllMocks(); + jest.resetModules(); (KibanaServices.get as jest.Mock).mockReturnValue({ http: { @@ -485,10 +481,6 @@ describe('persistTimeline', () => { api.persistTimeline({ timelineId, timeline: inputTimeline, version }); }); - afterAll(() => { - jest.resetAllMocks(); - }); - test('it should update timeline', () => { expect(patchMock.mock.calls[0][0]).toEqual(TIMELINE_URL); }); @@ -506,3 +498,127 @@ describe('persistTimeline', () => { }); }); }); + +describe('importTimelines', () => { + const fileToImport = { fileToImport: {} } as ImportDataProps; + const fetchMock = jest.fn(); + + beforeAll(() => { + jest.resetAllMocks(); + jest.resetModules(); + + (KibanaServices.get as jest.Mock).mockReturnValue({ + http: { + fetch: fetchMock, + }, + }); + api.importTimelines(fileToImport); + }); + + test('should pass correct args to KibanaServices - url', () => { + expect(fetchMock.mock.calls[0][0]).toEqual('/api/timeline/_import'); + }); + + test('should pass correct args to KibanaServices - args', () => { + expect(JSON.stringify(fetchMock.mock.calls[0][1])).toEqual( + JSON.stringify({ + method: 'POST', + headers: { 'Content-Type': undefined }, + body: new FormData(), + signal: undefined, + }) + ); + }); +}); + +describe('exportSelectedTimeline', () => { + const ids = ['123', 'abc']; + const fetchMock = jest.fn(); + + beforeAll(() => { + jest.resetAllMocks(); + jest.resetModules(); + + (KibanaServices.get as jest.Mock).mockReturnValue({ + http: { + fetch: fetchMock, + }, + }); + api.exportSelectedTimeline({ + filename: 'timelines_export.ndjson', + ids, + signal: {} as AbortSignal, + }); + }); + + test('should pass correct args to KibanaServices', () => { + expect(fetchMock).toBeCalledWith('/api/timeline/_export', { + body: JSON.stringify({ ids }), + method: 'POST', + query: { file_name: 'timelines_export.ndjson' }, + signal: {}, + }); + }); +}); + +describe('getDraftTimeline', () => { + const timelineType = { timelineType: TimelineType.default }; + const getMock = jest.fn(); + + beforeAll(() => { + jest.resetAllMocks(); + jest.resetModules(); + + (KibanaServices.get as jest.Mock).mockReturnValue({ + http: { + get: getMock, + }, + }); + api.getDraftTimeline(timelineType); + }); + + test('should pass correct args to KibanaServices', () => { + expect(getMock).toBeCalledWith('/api/timeline/_draft', { + query: timelineType, + }); + }); +}); + +describe('cleanDraftTimeline', () => { + const postMock = jest.fn(); + + beforeEach(() => { + jest.resetAllMocks(); + jest.resetModules(); + + (KibanaServices.get as jest.Mock).mockReturnValue({ + http: { + post: postMock, + }, + }); + }); + + test('should pass correct args to KibanaServices - timeline', () => { + const args = { timelineType: TimelineType.default }; + + api.cleanDraftTimeline(args); + + expect(postMock).toBeCalledWith('/api/timeline/_draft', { + body: JSON.stringify(args), + }); + }); + + test('should pass correct args to KibanaServices - timeline template', () => { + const args = { + timelineType: TimelineType.template, + templateTimelineId: 'test-123', + templateTimelineVersion: 1, + }; + + api.cleanDraftTimeline(args); + + expect(postMock).toBeCalledWith('/api/timeline/_draft', { + body: JSON.stringify(args), + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/containers/api.ts b/x-pack/plugins/security_solution/public/timelines/containers/api.ts index fbd89268880db..ff252ea93039d 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/api.ts +++ b/x-pack/plugins/security_solution/public/timelines/containers/api.ts @@ -132,6 +132,7 @@ export const persistTimeline = async ({ export const importTimelines = async ({ fileToImport, + signal, }: ImportDataProps): Promise => { const formData = new FormData(); formData.append('file', fileToImport); @@ -140,24 +141,24 @@ export const importTimelines = async ({ method: 'POST', headers: { 'Content-Type': undefined }, body: formData, + signal, }); }; -export const exportSelectedTimeline: ExportSelectedData = async ({ +export const exportSelectedTimeline: ExportSelectedData = ({ filename = `timelines_export.ndjson`, ids = [], signal, }): Promise => { const body = ids.length > 0 ? JSON.stringify({ ids }) : undefined; - const response = await KibanaServices.get().http.fetch<{ body: Blob }>(`${TIMELINE_EXPORT_URL}`, { + return KibanaServices.get().http.fetch(`${TIMELINE_EXPORT_URL}`, { method: 'POST', body, query: { file_name: filename, }, + signal, }); - - return response.body; }; export const getDraftTimeline = async ({ From 515b5dc66c224fb566ae8b0ba7e11b4c69a7e408 Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper Date: Wed, 1 Jul 2020 18:07:54 -0500 Subject: [PATCH 13/34] [Metrics UI] Fix asynchronicity and error handling in Snapshot API (#70503) Co-authored-by: Elastic Machine --- .../infra/server/lib/snapshot/snapshot.ts | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/x-pack/plugins/infra/server/lib/snapshot/snapshot.ts b/x-pack/plugins/infra/server/lib/snapshot/snapshot.ts index 2868c63745eb1..9ca10d5e39da7 100644 --- a/x-pack/plugins/infra/server/lib/snapshot/snapshot.ts +++ b/x-pack/plugins/infra/server/lib/snapshot/snapshot.ts @@ -41,12 +41,13 @@ export class InfraSnapshot { // when they have both been completed. const timeRangeWithIntervalApplied = await createTimeRangeWithInterval(client, options); const optionsWithTimerange = { ...options, timerange: timeRangeWithIntervalApplied }; + const groupedNodesPromise = requestGroupedNodes(client, optionsWithTimerange); const nodeMetricsPromise = requestNodeMetrics(client, optionsWithTimerange); - - const groupedNodeBuckets = await groupedNodesPromise; - const nodeMetricBuckets = await nodeMetricsPromise; - + const [groupedNodeBuckets, nodeMetricBuckets] = await Promise.all([ + groupedNodesPromise, + nodeMetricsPromise, + ]); return { nodes: mergeNodeBuckets(groupedNodeBuckets, nodeMetricBuckets, options), interval: timeRangeWithIntervalApplied.interval, @@ -103,11 +104,12 @@ const requestGroupedNodes = async ( }, }, }; - - return await getAllCompositeData< - InfraSnapshotAggregationResponse, - InfraSnapshotNodeGroupByBucket - >(callClusterFactory(client), query, bucketSelector, handleAfterKey); + return getAllCompositeData( + callClusterFactory(client), + query, + bucketSelector, + handleAfterKey + ); }; const calculateIndexPatterBasedOnMetrics = (options: InfraSnapshotRequestOptions) => { @@ -161,10 +163,12 @@ const requestNodeMetrics = async ( }, }, }; - return await getAllCompositeData< - InfraSnapshotAggregationResponse, - InfraSnapshotNodeMetricsBucket - >(callClusterFactory(client), query, bucketSelector, handleAfterKey); + return getAllCompositeData( + callClusterFactory(client), + query, + bucketSelector, + handleAfterKey + ); }; // buckets can be InfraSnapshotNodeGroupByBucket[] or InfraSnapshotNodeMetricsBucket[] From 0f418bb7f0860db2d8e5e33aee5e40ff83b0bbdb Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 1 Jul 2020 16:10:01 -0700 Subject: [PATCH 14/34] skip suites blocking es snapshot promomotion (#70532) --- .../apis/management/index_management/templates.js | 3 ++- x-pack/test/functional/apps/index_management/home_page.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/x-pack/test/api_integration/apis/management/index_management/templates.js b/x-pack/test/api_integration/apis/management/index_management/templates.js index cd7f5fb209eca..003fb21b09ccc 100644 --- a/x-pack/test/api_integration/apis/management/index_management/templates.js +++ b/x-pack/test/api_integration/apis/management/index_management/templates.js @@ -24,7 +24,8 @@ export default function ({ getService }) { updateTemplate, } = registerHelpers({ supertest }); - describe('index templates', () => { + // blocking es snapshot promotion: https://github.com/elastic/kibana/issues/70532 + describe.skip('index templates', () => { after(() => Promise.all([cleanUpEsResources()])); describe('get all', () => { diff --git a/x-pack/test/functional/apps/index_management/home_page.ts b/x-pack/test/functional/apps/index_management/home_page.ts index 90bc3603c1613..b5b0197aad4b3 100644 --- a/x-pack/test/functional/apps/index_management/home_page.ts +++ b/x-pack/test/functional/apps/index_management/home_page.ts @@ -13,7 +13,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const log = getService('log'); const browser = getService('browser'); - describe('Home page', function () { + // blocking es snapshot promotion: https://github.com/elastic/kibana/issues/70532 + describe.skip('Home page', function () { before(async () => { await pageObjects.common.navigateToApp('indexManagement'); }); From 4f7da59a51859dc7a7dc795ff3112f5148544a66 Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Wed, 1 Jul 2020 16:14:21 -0700 Subject: [PATCH 15/34] [Ingest Manager] Rename data sources to package configs (#70259) * Rename `datasource` saved object to `package_config` (SO type `ingest-datasource` to `ingest-package-config`) and adjust mappings: - Remove unused `processors` field on input & stream levels - Remove unnecessary `enabled` field on package config & input levels - Rename `agent_stream` field to `compiled_stream` - Reorder other fields so that important fields are closer to top & similar fields are grouped together Also, - Remove 7.9.0 migrations as we are not supporting an upgrade path from experimental to beta release * Pluralize `ingest-package-configs` for consistency * Rename `Datasource`-related types to `PackageConfig`, update all references (does not include actual type definition changes yet) * Rename `Datasource` schemas and rest spec typings to `PackageConfig` (does not include actual schema changes yet) * Change `datasources` on agent config typings and schemas to `package_configs` and update all references * Add back `enabled` field on package config and input levels. They are needed for current & future UI features. Also: - Match types and schemas with saved object mappings (`agent_stream` to `compiled_stream`, removal of `processors`) - Set `namespace` to be a required property on agent config and package config types, add validation support for it on UI * Rename server-side datasource references in file names, variable names, and routes * Update spec file and schema file * Update doc wording * Rename all instances of datasource in file paths and variable names on client sides, and for Endpoint too * Minor copy adjustments, fix i18n check * Replace datasource references in tests and fixtures; remove unused `ingest/policies` es archiver data * Fix tests * Fix test field name * Fix test fixtures fields again * Fix i18n --- x-pack/plugins/ingest_manager/README.md | 2 +- .../common/constants/agent_config.ts | 2 +- .../ingest_manager/common/constants/index.ts | 2 +- .../{datasource.ts => package_config.ts} | 2 +- .../ingest_manager/common/constants/routes.ts | 16 +- .../common/openapi/spec_oas3.json | 64 +- .../ingest_manager/common/services/index.ts | 4 +- ...> package_configs_to_agent_inputs.test.ts} | 38 +- ....ts => package_configs_to_agent_inputs.ts} | 29 +- .../common/services/package_to_config.test.ts | 57 +- .../common/services/package_to_config.ts | 60 +- .../ingest_manager/common/services/routes.ts | 18 +- .../common/types/models/agent_config.ts | 18 +- .../common/types/models/datasource.ts | 70 - .../ingest_manager/common/types/models/epm.ts | 2 +- .../common/types/models/index.ts | 2 +- .../common/types/models/package_config.ts | 68 + .../common/types/rest_spec/datasource.ts | 59 - .../common/types/rest_spec/index.ts | 2 +- .../common/types/rest_spec/package_config.ts | 59 + .../ingest_manager/dev_docs/definitions.md | 21 +- .../dev_docs/schema/saved_objects.mml | 8 +- .../ingest_manager/constants/index.ts | 2 +- .../ingest_manager/constants/page_paths.ts | 20 +- .../ingest_manager/hooks/use_breadcrumbs.tsx | 18 +- .../hooks/use_request/datasource.ts | 62 - .../ingest_manager/hooks/use_request/index.ts | 2 +- .../hooks/use_request/package_config.ts | 62 + .../agent_config/components/config_form.tsx | 4 +- .../sections/agent_config/components/index.ts | 2 +- ...tsx => package_config_delete_provider.tsx} | 80 +- .../custom_configure_datasource.tsx | 62 - .../components/index.ts | 9 - .../create_datasource_page/constants.ts | 11 - .../components/custom_package_config.tsx | 61 + .../components/index.ts | 9 + .../components/layout.tsx | 28 +- .../package_config_input_config.tsx} | 46 +- .../package_config_input_panel.tsx} | 70 +- .../package_config_input_stream.tsx} | 46 +- .../package_config_input_var_field.tsx} | 4 +- .../index.tsx | 141 +- .../services/index.ts | 10 +- .../services/is_advanced_var.test.ts | 0 .../services/is_advanced_var.ts | 0 .../services/validate_package_config.ts} | 81 +- .../validate_package_config.ts.test.ts} | 76 +- .../step_configure_package.tsx} | 58 +- .../step_define_package_config.tsx} | 80 +- .../step_select_config.tsx | 10 +- .../step_select_package.tsx | 8 +- .../types.ts | 4 +- .../components/datasources/index.tsx | 20 - .../details_page/components/index.ts | 4 +- .../components/package_configs/index.tsx | 23 + .../no_package_configs.tsx} | 19 +- .../package_configs_table.tsx} | 175 +- .../agent_config/details_page/index.tsx | 23 +- .../index.tsx | 147 +- .../sections/agent_config/index.tsx | 12 +- .../list_page/components/create_config.tsx | 2 +- .../sections/agent_config/list_page/index.tsx | 9 +- .../ingest_manager/sections/epm/index.tsx | 6 +- .../sections/epm/screens/detail/content.tsx | 6 +- .../sections/epm/screens/detail/header.tsx | 4 +- ...es_panel.tsx => package_configs_panel.tsx} | 15 +- .../epm/screens/detail/settings_panel.tsx | 16 +- .../epm/screens/detail/side_nav_links.tsx | 13 +- ...es.tsx => agent_config_package_badges.tsx} | 20 +- .../config_selection.tsx | 4 +- .../agent_reassign_config_flyout/index.tsx | 4 +- .../components/configuration_section.tsx | 12 +- .../ingest_manager/services/index.ts | 6 +- .../ingest_manager/types/index.ts | 20 +- .../types/intra_app_route_state.ts | 12 +- x-pack/plugins/ingest_manager/public/index.ts | 10 +- .../plugins/ingest_manager/public/plugin.ts | 6 +- .../ingest_manager/server/constants/index.ts | 4 +- x-pack/plugins/ingest_manager/server/index.ts | 2 +- .../server/integration_tests/router.test.ts | 22 +- x-pack/plugins/ingest_manager/server/mocks.ts | 8 +- .../plugins/ingest_manager/server/plugin.ts | 22 +- .../server/routes/agent_config/handlers.ts | 52 +- .../server/routes/datasource/index.ts | 73 - .../ingest_manager/server/routes/index.ts | 2 +- .../handlers.test.ts} | 46 +- .../handlers.ts | 106 +- .../server/routes/package_config/index.ts | 73 + .../server/saved_objects/index.ts | 37 +- .../migrations/agent_config_v790.ts | 24 - .../migrations/datasources_v790.ts | 43 - .../server/services/agent_config.ts | 59 +- .../server/services/epm/agent/agent.ts | 6 +- .../server/services/epm/packages/remove.ts | 10 +- .../ingest_manager/server/services/index.ts | 2 +- ...asource.test.ts => package_config.test.ts} | 12 +- .../{datasource.ts => package_config.ts} | 141 +- .../ingest_manager/server/services/setup.ts | 39 +- .../ingest_manager/server/types/index.tsx | 10 +- .../server/types/models/agent_config.ts | 7 +- .../server/types/models/index.ts | 2 +- .../{datasource.ts => package_config.ts} | 12 +- .../server/types/rest_spec/datasource.ts | 33 - .../server/types/rest_spec/index.ts | 2 +- .../server/types/rest_spec/package_config.ts | 33 + .../common/endpoint/generate_data.ts | 4 +- .../common/endpoint/types.ts | 8 +- .../mock/endpoint/dependencies_start_mock.ts | 7 +- .../pages/endpoint_hosts/store/middleware.ts | 4 +- .../pages/endpoint_hosts/view/index.tsx | 6 +- .../policy/store/policy_details/middleware.ts | 8 +- .../policy/store/policy_list/index.test.ts | 28 +- .../policy/store/policy_list/middleware.ts | 16 +- .../policy_list/mock_policy_result_list.ts | 2 +- .../store/policy_list/services/ingest.test.ts | 30 +- .../store/policy_list/services/ingest.ts | 58 +- .../store/policy_list/test_mock_utils.ts | 6 +- .../public/management/pages/policy/types.ts | 12 +- ...ource.tsx => configure_package_config.tsx} | 26 +- .../pages/policy/view/policy_details.test.tsx | 20 +- .../pages/policy/view/policy_list.tsx | 8 +- .../security_solution/public/plugin.tsx | 7 +- .../endpoint/endpoint_app_context_services.ts | 4 +- .../server/endpoint/ingest_integration.ts | 28 +- .../server/endpoint/mocks.ts | 4 +- .../translations/translations/ja-JP.json | 167 +- .../translations/translations/zh-CN.json | 167 +- .../apis/ingest_manager/agent_config.ts | 2 +- .../es_archives/fleet/agents/data.json | 4 +- .../es_archives/fleet/agents/mappings.json | 8 +- .../es_archives/ingest/policies/data.json | 59 - .../es_archives/ingest/policies/mappings.json | 1545 ----------------- .../custom_rule_with_timeline/mappings.json | 6 +- .../apps/endpoint/policy_details.ts | 22 +- .../apps/endpoint/policy_list.ts | 28 +- .../page_objects/index.ts | 4 +- ...est_manager_create_package_config_page.ts} | 33 +- .../page_objects/policy_page.ts | 6 +- .../services/endpoint_policy.ts | 87 +- 139 files changed, 1990 insertions(+), 3573 deletions(-) rename x-pack/plugins/ingest_manager/common/constants/{datasource.ts => package_config.ts} (76%) rename x-pack/plugins/ingest_manager/common/services/{datasources_to_agent_inputs.test.ts => package_configs_to_agent_inputs.test.ts} (79%) rename x-pack/plugins/ingest_manager/common/services/{datasources_to_agent_inputs.ts => package_configs_to_agent_inputs.ts} (65%) delete mode 100644 x-pack/plugins/ingest_manager/common/types/models/datasource.ts create mode 100644 x-pack/plugins/ingest_manager/common/types/models/package_config.ts delete mode 100644 x-pack/plugins/ingest_manager/common/types/rest_spec/datasource.ts create mode 100644 x-pack/plugins/ingest_manager/common/types/rest_spec/package_config.ts delete mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/datasource.ts create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/package_config.ts rename x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/{datasource_delete_provider.tsx => package_config_delete_provider.tsx} (66%) delete mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/custom_configure_datasource.tsx delete mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/index.ts delete mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/constants.ts create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/custom_package_config.tsx create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/index.ts rename x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/{create_datasource_page => create_package_config_page}/components/layout.tsx (83%) rename x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/{create_datasource_page/components/datasource_input_config.tsx => create_package_config_page/components/package_config_input_config.tsx} (77%) rename x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/{create_datasource_page/components/datasource_input_panel.tsx => create_package_config_page/components/package_config_input_panel.tsx} (73%) rename x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/{create_datasource_page/components/datasource_input_stream_config.tsx => create_package_config_page/components/package_config_input_stream.tsx} (78%) rename x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/{create_datasource_page/components/datasource_input_var_field.tsx => create_package_config_page/components/package_config_input_var_field.tsx} (93%) rename x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/{create_datasource_page => create_package_config_page}/index.tsx (71%) rename x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/{create_datasource_page => create_package_config_page}/services/index.ts (65%) rename x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/{create_datasource_page => create_package_config_page}/services/is_advanced_var.test.ts (100%) rename x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/{create_datasource_page => create_package_config_page}/services/is_advanced_var.ts (100%) rename x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/{create_datasource_page/services/validate_datasource.ts => create_package_config_page/services/validate_package_config.ts} (69%) rename x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/{create_datasource_page/services/validate_datasource.test.ts => create_package_config_page/services/validate_package_config.ts.test.ts} (88%) rename x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/{create_datasource_page/step_configure_datasource.tsx => create_package_config_page/step_configure_package.tsx} (67%) rename x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/{create_datasource_page/step_define_datasource.tsx => create_package_config_page/step_define_package_config.tsx} (60%) rename x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/{create_datasource_page => create_package_config_page}/step_select_config.tsx (91%) rename x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/{create_datasource_page => create_package_config_page}/step_select_package.tsx (92%) rename x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/{create_datasource_page => create_package_config_page}/types.ts (59%) delete mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/index.tsx create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/package_configs/index.tsx rename x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/{datasources/no_datasources.tsx => package_configs/no_package_configs.tsx} (61%) rename x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/{datasources/datasources_table.tsx => package_configs/package_configs_table.tsx} (55%) rename x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/{edit_datasource_page => edit_package_config_page}/index.tsx (64%) rename x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/{data_sources_panel.tsx => package_configs_panel.tsx} (75%) rename x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/{agent_config_datasource_badges.tsx => agent_config_package_badges.tsx} (68%) delete mode 100644 x-pack/plugins/ingest_manager/server/routes/datasource/index.ts rename x-pack/plugins/ingest_manager/server/routes/{datasource/datasource_handlers.test.ts => package_config/handlers.test.ts} (85%) rename x-pack/plugins/ingest_manager/server/routes/{datasource => package_config}/handlers.ts (56%) create mode 100644 x-pack/plugins/ingest_manager/server/routes/package_config/index.ts delete mode 100644 x-pack/plugins/ingest_manager/server/saved_objects/migrations/agent_config_v790.ts delete mode 100644 x-pack/plugins/ingest_manager/server/saved_objects/migrations/datasources_v790.ts rename x-pack/plugins/ingest_manager/server/services/{datasource.test.ts => package_config.test.ts} (92%) rename x-pack/plugins/ingest_manager/server/services/{datasource.ts => package_config.ts} (67%) rename x-pack/plugins/ingest_manager/server/types/models/{datasource.ts => package_config.ts} (84%) delete mode 100644 x-pack/plugins/ingest_manager/server/types/rest_spec/datasource.ts create mode 100644 x-pack/plugins/ingest_manager/server/types/rest_spec/package_config.ts rename x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/{configure_datasource.tsx => configure_package_config.tsx} (79%) delete mode 100644 x-pack/test/functional/es_archives/ingest/policies/data.json delete mode 100644 x-pack/test/functional/es_archives/ingest/policies/mappings.json rename x-pack/test/security_solution_endpoint/page_objects/{ingest_manager_create_datasource_page.ts => ingest_manager_create_package_config_page.ts} (66%) diff --git a/x-pack/plugins/ingest_manager/README.md b/x-pack/plugins/ingest_manager/README.md index 50c42544b8bdc..eebafc76a5e00 100644 --- a/x-pack/plugins/ingest_manager/README.md +++ b/x-pack/plugins/ingest_manager/README.md @@ -3,7 +3,7 @@ ## Plugin - The plugin is disabled by default. See the TypeScript type for the [the available plugin configuration options](https://github.com/elastic/kibana/blob/master/x-pack/plugins/ingest_manager/common/types/index.ts#L9-L27) -- Setting `xpack.ingestManager.enabled=true` enables the plugin including the EPM and Fleet features. It also adds the `DATASOURCE_API_ROUTES` and `AGENT_CONFIG_API_ROUTES` values in [`common/constants/routes.ts`](./common/constants/routes.ts) +- Setting `xpack.ingestManager.enabled=true` enables the plugin including the EPM and Fleet features. It also adds the `PACKAGE_CONFIG_API_ROUTES` and `AGENT_CONFIG_API_ROUTES` values in [`common/constants/routes.ts`](./common/constants/routes.ts) - Adding `--xpack.ingestManager.epm.enabled=false` will disable the EPM API & UI - Adding `--xpack.ingestManager.fleet.enabled=false` will disable the Fleet API & UI - [code for adding the routes](https://github.com/elastic/kibana/blob/1f27d349533b1c2865c10c45b2cf705d7416fb36/x-pack/plugins/ingest_manager/server/plugin.ts#L115-L133) diff --git a/x-pack/plugins/ingest_manager/common/constants/agent_config.ts b/x-pack/plugins/ingest_manager/common/constants/agent_config.ts index 9bc1293799d3c..30ca92f5f32f3 100644 --- a/x-pack/plugins/ingest_manager/common/constants/agent_config.ts +++ b/x-pack/plugins/ingest_manager/common/constants/agent_config.ts @@ -12,7 +12,7 @@ export const DEFAULT_AGENT_CONFIG = { namespace: 'default', description: 'Default agent configuration created by Kibana', status: AgentConfigStatus.Active, - datasources: [], + package_configs: [], is_default: true, monitoring_enabled: ['logs', 'metrics'] as Array<'logs' | 'metrics'>, }; diff --git a/x-pack/plugins/ingest_manager/common/constants/index.ts b/x-pack/plugins/ingest_manager/common/constants/index.ts index 6a2e559bbbe4f..ed01fcdd316a3 100644 --- a/x-pack/plugins/ingest_manager/common/constants/index.ts +++ b/x-pack/plugins/ingest_manager/common/constants/index.ts @@ -8,7 +8,7 @@ export * from './routes'; export * from './agent'; export * from './agent_config'; -export * from './datasource'; +export * from './package_config'; export * from './epm'; export * from './output'; export * from './enrollment_api_key'; diff --git a/x-pack/plugins/ingest_manager/common/constants/datasource.ts b/x-pack/plugins/ingest_manager/common/constants/package_config.ts similarity index 76% rename from x-pack/plugins/ingest_manager/common/constants/datasource.ts rename to x-pack/plugins/ingest_manager/common/constants/package_config.ts index 08113cff53bda..e7d5ef67f7253 100644 --- a/x-pack/plugins/ingest_manager/common/constants/datasource.ts +++ b/x-pack/plugins/ingest_manager/common/constants/package_config.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export const DATASOURCE_SAVED_OBJECT_TYPE = 'ingest-datasources'; +export const PACKAGE_CONFIG_SAVED_OBJECT_TYPE = 'ingest-package-configs'; diff --git a/x-pack/plugins/ingest_manager/common/constants/routes.ts b/x-pack/plugins/ingest_manager/common/constants/routes.ts index 1fe29aa54f6f9..dad3cdce1a497 100644 --- a/x-pack/plugins/ingest_manager/common/constants/routes.ts +++ b/x-pack/plugins/ingest_manager/common/constants/routes.ts @@ -7,7 +7,7 @@ export const API_ROOT = `/api/ingest_manager`; export const EPM_API_ROOT = `${API_ROOT}/epm`; export const DATA_STREAM_API_ROOT = `${API_ROOT}/data_streams`; -export const DATASOURCE_API_ROOT = `${API_ROOT}/datasources`; +export const PACKAGE_CONFIG_API_ROOT = `${API_ROOT}/package_configs`; export const AGENT_CONFIG_API_ROOT = `${API_ROOT}/agent_configs`; export const FLEET_API_ROOT = `${API_ROOT}/fleet`; @@ -29,13 +29,13 @@ export const DATA_STREAM_API_ROUTES = { LIST_PATTERN: `${DATA_STREAM_API_ROOT}`, }; -// Datasource API routes -export const DATASOURCE_API_ROUTES = { - LIST_PATTERN: `${DATASOURCE_API_ROOT}`, - INFO_PATTERN: `${DATASOURCE_API_ROOT}/{datasourceId}`, - CREATE_PATTERN: `${DATASOURCE_API_ROOT}`, - UPDATE_PATTERN: `${DATASOURCE_API_ROOT}/{datasourceId}`, - DELETE_PATTERN: `${DATASOURCE_API_ROOT}/delete`, +// Package config API routes +export const PACKAGE_CONFIG_API_ROUTES = { + LIST_PATTERN: `${PACKAGE_CONFIG_API_ROOT}`, + INFO_PATTERN: `${PACKAGE_CONFIG_API_ROOT}/{packageConfigId}`, + CREATE_PATTERN: `${PACKAGE_CONFIG_API_ROOT}`, + UPDATE_PATTERN: `${PACKAGE_CONFIG_API_ROOT}/{packageConfigId}`, + DELETE_PATTERN: `${PACKAGE_CONFIG_API_ROOT}/delete`, }; // Agent config API routes diff --git a/x-pack/plugins/ingest_manager/common/openapi/spec_oas3.json b/x-pack/plugins/ingest_manager/common/openapi/spec_oas3.json index d17b4115e64ab..9617173bd0c7b 100644 --- a/x-pack/plugins/ingest_manager/common/openapi/spec_oas3.json +++ b/x-pack/plugins/ingest_manager/common/openapi/spec_oas3.json @@ -60,7 +60,7 @@ "namespace": "default", "description": "Default agent configuration created by Kibana", "status": "active", - "datasources": ["8a5679b0-8fbf-11ea-b2ce-01c4a6127154"], + "packageConfigs": ["8a5679b0-8fbf-11ea-b2ce-01c4a6127154"], "is_default": true, "monitoring_enabled": ["logs", "metrics"], "revision": 2, @@ -175,7 +175,7 @@ "namespace": "default", "description": "Default agent configuration created by Kibana", "status": "active", - "datasources": [ + "packageConfigs": [ { "id": "8a5679b0-8fbf-11ea-b2ce-01c4a6127154", "name": "system-1", @@ -750,7 +750,7 @@ "namespace": "UPDATED namespace", "updated_on": "Fri Feb 28 2020 16:22:31 GMT-0500 (Eastern Standard Time)", "updated_by": "elastic", - "datasources": [] + "packageConfigs": [] }, "success": true } @@ -922,9 +922,9 @@ }, "parameters": [] }, - "/datasources": { + "/packageConfigs": { "get": { - "summary": "Datasources - List", + "summary": "PackageConfigs - List", "tags": [], "responses": { "200": { @@ -937,7 +937,7 @@ "items": { "type": "array", "items": { - "$ref": "#/components/schemas/Datasource" + "$ref": "#/components/schemas/PackageConfig" } }, "total": { @@ -1166,14 +1166,14 @@ } } }, - "operationId": "get-datasources", + "operationId": "get-packageConfigs", "security": [], "parameters": [] }, "parameters": [], "post": { - "summary": "Datasources - Create", - "operationId": "post-datasources", + "summary": "PackageConfigs - Create", + "operationId": "post-packageConfigs", "responses": { "200": { "description": "OK" @@ -1183,7 +1183,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/NewDatasource" + "$ref": "#/components/schemas/NewPackageConfig" }, "examples": { "example-1": { @@ -1237,9 +1237,9 @@ ] } }, - "/datasources/{datasourceId}": { + "/packageConfigs/{packageConfigId}": { "get": { - "summary": "Datasources - Info", + "summary": "PackageConfigs - Info", "tags": [], "responses": { "200": { @@ -1250,7 +1250,7 @@ "type": "object", "properties": { "item": { - "$ref": "#/components/schemas/Datasource" + "$ref": "#/components/schemas/PackageConfig" }, "success": { "type": "boolean" @@ -1262,21 +1262,21 @@ } } }, - "operationId": "get-datasources-datasourceId" + "operationId": "get-packageConfigs-packageConfigId" }, "parameters": [ { "schema": { "type": "string" }, - "name": "datasourceId", + "name": "packageConfigId", "in": "path", "required": true } ], "put": { - "summary": "Datasources - Update", - "operationId": "put-datasources-datasourceId", + "summary": "PackageConfigs - Update", + "operationId": "put-packageConfigs-packageConfigId", "responses": { "200": { "description": "OK", @@ -1286,7 +1286,7 @@ "type": "object", "properties": { "item": { - "$ref": "#/components/schemas/Datasource" + "$ref": "#/components/schemas/PackageConfig" }, "sucess": { "type": "boolean" @@ -1724,9 +1724,7 @@ "license": "basic", "description": "This is the Elastic Endpoint package.", "type": "solution", - "categories": [ - "security" - ], + "categories": ["security"], "release": "beta", "requirement": { "kibana": { @@ -1826,10 +1824,10 @@ "path": "telemetry" } ], - "datasources": [ + "packageConfigs": [ { "name": "endpoint", - "title": "Endpoint data source", + "title": "Endpoint package config", "description": "Interact with the endpoint.", "inputs": null, "multiple": false @@ -2121,7 +2119,7 @@ } }, { - "description": "The log package should be used to create data sources for all type of logs for which an package doesn't exist yet.\n", + "description": "The log package should be used to create package configs for all type of logs for which an package doesn't exist yet.\n", "download": "/epr/log/log-0.9.0.tar.gz", "icons": [ { @@ -2782,7 +2780,7 @@ }, "actions": [ { - "data": "{\"config\":{\"id\":\"ae556400-5e39-11ea-8b49-f9747e466f7b\",\"outputs\":{\"default\":{\"type\":\"elasticsearch\",\"hosts\":[\"http://localhost:9200\"],\"api_key\":\"\",\"api_token\":\"6ckkp3ABz7e_XRqr3LM8:gQuDfUNSRgmY0iziYqP9Hw\"}},\"datasources\":[]}}", + "data": "{\"config\":{\"id\":\"ae556400-5e39-11ea-8b49-f9747e466f7b\",\"outputs\":{\"default\":{\"type\":\"elasticsearch\",\"hosts\":[\"http://localhost:9200\"],\"api_key\":\"\",\"api_token\":\"6ckkp3ABz7e_XRqr3LM8:gQuDfUNSRgmY0iziYqP9Hw\"}},\"packageConfigs\":[]}}", "created_at": "2020-03-04T20:02:56.149Z", "id": "6a95c00a-d76d-4931-97c3-0bf935272d7d", "type": "CONFIG_CHANGE" @@ -2967,7 +2965,7 @@ "api_key": "Z-XkgHIBvwtjzIKtSCTh:AejRqdKpQx6z-6dqSI1LHg" } }, - "datasources": [ + "packageConfigs": [ { "id": "33d6bd70-a5e0-11ea-a587-5f886c8a849f", "name": "system-1", @@ -3690,7 +3688,7 @@ "type": "string", "enum": ["active", "inactive"] }, - "datasources": { + "packageConfigs": { "oneOf": [ { "items": { @@ -3699,7 +3697,7 @@ }, { "items": { - "$ref": "#/components/schemas/Datasource" + "$ref": "#/components/schemas/PackageConfig" } } ], @@ -3723,8 +3721,8 @@ } ] }, - "Datasource": { - "title": "Datasource", + "PackageConfig": { + "title": "PackageConfig", "allOf": [ { "type": "object", @@ -3743,7 +3741,7 @@ "required": ["id", "revision"] }, { - "$ref": "#/components/schemas/NewDatasource" + "$ref": "#/components/schemas/NewPackageConfig" } ], "x-examples": { @@ -3765,8 +3763,8 @@ } } }, - "NewDatasource": { - "title": "NewDatasource", + "NewPackageConfig": { + "title": "NewPackageConfig", "type": "object", "x-examples": { "example-1": { diff --git a/x-pack/plugins/ingest_manager/common/services/index.ts b/x-pack/plugins/ingest_manager/common/services/index.ts index e53d97972fa2f..a0db7c20747e2 100644 --- a/x-pack/plugins/ingest_manager/common/services/index.ts +++ b/x-pack/plugins/ingest_manager/common/services/index.ts @@ -6,8 +6,8 @@ import * as AgentStatusKueryHelper from './agent_status'; export * from './routes'; -export { packageToConfigDatasourceInputs, packageToConfigDatasource } from './package_to_config'; -export { storedDatasourcesToAgentInputs } from './datasources_to_agent_inputs'; +export { packageToPackageConfigInputs, packageToPackageConfig } from './package_to_config'; +export { storedPackageConfigsToAgentInputs } from './package_configs_to_agent_inputs'; export { configToYaml } from './config_to_yaml'; export { AgentStatusKueryHelper }; export { decodeCloudId } from './decode_cloud_id'; diff --git a/x-pack/plugins/ingest_manager/common/services/datasources_to_agent_inputs.test.ts b/x-pack/plugins/ingest_manager/common/services/package_configs_to_agent_inputs.test.ts similarity index 79% rename from x-pack/plugins/ingest_manager/common/services/datasources_to_agent_inputs.test.ts rename to x-pack/plugins/ingest_manager/common/services/package_configs_to_agent_inputs.test.ts index 538951ff10399..a4d87f54b0915 100644 --- a/x-pack/plugins/ingest_manager/common/services/datasources_to_agent_inputs.test.ts +++ b/x-pack/plugins/ingest_manager/common/services/package_configs_to_agent_inputs.test.ts @@ -3,13 +3,13 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { Datasource, DatasourceInput } from '../types'; -import { storedDatasourcesToAgentInputs } from './datasources_to_agent_inputs'; +import { PackageConfig, PackageConfigInput } from '../types'; +import { storedPackageConfigsToAgentInputs } from './package_configs_to_agent_inputs'; -describe('Ingest Manager - storedDatasourcesToAgentInputs', () => { - const mockDatasource: Datasource = { +describe('Ingest Manager - storedPackageConfigsToAgentInputs', () => { + const mockPackageConfig: PackageConfig = { id: 'some-uuid', - name: 'mock-datasource', + name: 'mock-package-config', description: '', created_at: '', created_by: '', @@ -23,7 +23,7 @@ describe('Ingest Manager - storedDatasourcesToAgentInputs', () => { revision: 1, }; - const mockInput: DatasourceInput = { + const mockInput: PackageConfigInput = { type: 'test-logs', enabled: true, vars: { @@ -44,7 +44,7 @@ describe('Ingest Manager - storedDatasourcesToAgentInputs', () => { fooVar: { value: 'foo-value' }, fooVar2: { value: [1, 2] }, }, - agent_stream: { + compiled_stream: { fooKey: 'fooValue1', fooKey2: ['fooValue2'], }, @@ -74,13 +74,13 @@ describe('Ingest Manager - storedDatasourcesToAgentInputs', () => { ], }; - it('returns no inputs for datasource with no inputs, or only disabled inputs', () => { - expect(storedDatasourcesToAgentInputs([mockDatasource])).toEqual([]); + it('returns no inputs for package config with no inputs, or only disabled inputs', () => { + expect(storedPackageConfigsToAgentInputs([mockPackageConfig])).toEqual([]); expect( - storedDatasourcesToAgentInputs([ + storedPackageConfigsToAgentInputs([ { - ...mockDatasource, + ...mockPackageConfig, package: { name: 'mock-package', title: 'Mock package', @@ -91,9 +91,9 @@ describe('Ingest Manager - storedDatasourcesToAgentInputs', () => { ).toEqual([]); expect( - storedDatasourcesToAgentInputs([ + storedPackageConfigsToAgentInputs([ { - ...mockDatasource, + ...mockPackageConfig, inputs: [{ ...mockInput, enabled: false }], }, ]) @@ -102,9 +102,9 @@ describe('Ingest Manager - storedDatasourcesToAgentInputs', () => { it('returns agent inputs', () => { expect( - storedDatasourcesToAgentInputs([ + storedPackageConfigsToAgentInputs([ { - ...mockDatasource, + ...mockPackageConfig, package: { name: 'mock-package', title: 'Mock package', @@ -116,7 +116,7 @@ describe('Ingest Manager - storedDatasourcesToAgentInputs', () => { ).toEqual([ { id: 'some-uuid', - name: 'mock-datasource', + name: 'mock-package-config', type: 'test-logs', dataset: { namespace: 'default' }, use_output: 'default', @@ -144,9 +144,9 @@ describe('Ingest Manager - storedDatasourcesToAgentInputs', () => { it('returns agent inputs without disabled streams', () => { expect( - storedDatasourcesToAgentInputs([ + storedPackageConfigsToAgentInputs([ { - ...mockDatasource, + ...mockPackageConfig, inputs: [ { ...mockInput, @@ -158,7 +158,7 @@ describe('Ingest Manager - storedDatasourcesToAgentInputs', () => { ).toEqual([ { id: 'some-uuid', - name: 'mock-datasource', + name: 'mock-package-config', type: 'test-logs', dataset: { namespace: 'default' }, use_output: 'default', diff --git a/x-pack/plugins/ingest_manager/common/services/datasources_to_agent_inputs.ts b/x-pack/plugins/ingest_manager/common/services/package_configs_to_agent_inputs.ts similarity index 65% rename from x-pack/plugins/ingest_manager/common/services/datasources_to_agent_inputs.ts rename to x-pack/plugins/ingest_manager/common/services/package_configs_to_agent_inputs.ts index c6c5d784396db..64ba6b8a52b57 100644 --- a/x-pack/plugins/ingest_manager/common/services/datasources_to_agent_inputs.ts +++ b/x-pack/plugins/ingest_manager/common/services/package_configs_to_agent_inputs.ts @@ -3,29 +3,29 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { Datasource, FullAgentConfigInput, FullAgentConfigInputStream } from '../types'; +import { PackageConfig, FullAgentConfigInput, FullAgentConfigInputStream } from '../types'; import { DEFAULT_OUTPUT } from '../constants'; -export const storedDatasourcesToAgentInputs = ( - datasources: Datasource[] +export const storedPackageConfigsToAgentInputs = ( + packageConfigs: PackageConfig[] ): FullAgentConfigInput[] => { const fullInputs: FullAgentConfigInput[] = []; - datasources.forEach((datasource) => { - if (!datasource.enabled || !datasource.inputs || !datasource.inputs.length) { + packageConfigs.forEach((packageConfig) => { + if (!packageConfig.enabled || !packageConfig.inputs || !packageConfig.inputs.length) { return; } - datasource.inputs.forEach((input) => { + packageConfig.inputs.forEach((input) => { if (!input.enabled) { return; } const fullInput: FullAgentConfigInput = { - id: datasource.id || datasource.name, - name: datasource.name, + id: packageConfig.id || packageConfig.name, + name: packageConfig.name, type: input.type, dataset: { - namespace: datasource.namespace || 'default', + namespace: packageConfig.namespace || 'default', }, use_output: DEFAULT_OUTPUT.name, ...Object.entries(input.config || {}).reduce((acc, [key, { value }]) => { @@ -38,24 +38,21 @@ export const storedDatasourcesToAgentInputs = ( const fullStream: FullAgentConfigInputStream = { id: stream.id, dataset: stream.dataset, - ...stream.agent_stream, + ...stream.compiled_stream, ...Object.entries(stream.config || {}).reduce((acc, [key, { value }]) => { acc[key] = value; return acc; }, {} as { [k: string]: any }), }; - if (stream.processors) { - fullStream.processors = stream.processors; - } return fullStream; }), }; - if (datasource.package) { + if (packageConfig.package) { fullInput.meta = { package: { - name: datasource.package.name, - version: datasource.package.version, + name: packageConfig.package.name, + version: packageConfig.package.version, }, }; } diff --git a/x-pack/plugins/ingest_manager/common/services/package_to_config.test.ts b/x-pack/plugins/ingest_manager/common/services/package_to_config.test.ts index 7739fdc3a3b61..e0cd32df1535e 100644 --- a/x-pack/plugins/ingest_manager/common/services/package_to_config.test.ts +++ b/x-pack/plugins/ingest_manager/common/services/package_to_config.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { PackageInfo, InstallationStatus } from '../types'; -import { packageToConfigDatasource, packageToConfigDatasourceInputs } from './package_to_config'; +import { packageToPackageConfig, packageToPackageConfigInputs } from './package_to_config'; describe('Ingest Manager - packageToConfig', () => { const mockPackage: PackageInfo = { @@ -31,15 +31,15 @@ describe('Ingest Manager - packageToConfig', () => { status: InstallationStatus.notInstalled, }; - describe('packageToConfigDatasourceInputs', () => { - it('returns empty array for packages with no datasources', () => { - expect(packageToConfigDatasourceInputs(mockPackage)).toEqual([]); - expect(packageToConfigDatasourceInputs({ ...mockPackage, config_templates: [] })).toEqual([]); + describe('packageToPackageConfigInputs', () => { + it('returns empty array for packages with no config templates', () => { + expect(packageToPackageConfigInputs(mockPackage)).toEqual([]); + expect(packageToPackageConfigInputs({ ...mockPackage, config_templates: [] })).toEqual([]); }); - it('returns empty array for packages a datasource but no inputs', () => { + it('returns empty array for packages with a config template but no inputs', () => { expect( - packageToConfigDatasourceInputs(({ + packageToPackageConfigInputs(({ ...mockPackage, config_templates: [{ inputs: [] }], } as unknown) as PackageInfo) @@ -48,13 +48,13 @@ describe('Ingest Manager - packageToConfig', () => { it('returns inputs with no streams for packages with no streams', () => { expect( - packageToConfigDatasourceInputs(({ + packageToPackageConfigInputs(({ ...mockPackage, config_templates: [{ inputs: [{ type: 'foo' }] }], } as unknown) as PackageInfo) ).toEqual([{ type: 'foo', enabled: true, streams: [] }]); expect( - packageToConfigDatasourceInputs(({ + packageToPackageConfigInputs(({ ...mockPackage, config_templates: [{ inputs: [{ type: 'foo' }, { type: 'bar' }] }], } as unknown) as PackageInfo) @@ -66,7 +66,7 @@ describe('Ingest Manager - packageToConfig', () => { it('returns inputs with streams for packages with streams', () => { expect( - packageToConfigDatasourceInputs(({ + packageToPackageConfigInputs(({ ...mockPackage, datasets: [ { type: 'logs', name: 'foo', streams: [{ input: 'foo' }] }, @@ -98,7 +98,7 @@ describe('Ingest Manager - packageToConfig', () => { it('returns inputs with streams configurations for packages with stream vars', () => { expect( - packageToConfigDatasourceInputs(({ + packageToPackageConfigInputs(({ ...mockPackage, datasets: [ { @@ -169,7 +169,7 @@ describe('Ingest Manager - packageToConfig', () => { it('returns inputs with streams configurations for packages with stream and input vars', () => { expect( - packageToConfigDatasourceInputs(({ + packageToPackageConfigInputs(({ ...mockPackage, datasets: [ { @@ -313,10 +313,11 @@ describe('Ingest Manager - packageToConfig', () => { }); }); - describe('packageToConfigDatasource', () => { - it('returns datasource with default name', () => { - expect(packageToConfigDatasource(mockPackage, '1', '2')).toEqual({ + describe('packageToPackageConfig', () => { + it('returns package config with default name', () => { + expect(packageToPackageConfig(mockPackage, '1', '2')).toEqual({ config_id: '1', + namespace: '', enabled: true, inputs: [], name: 'mock-package-1', @@ -328,12 +329,13 @@ describe('Ingest Manager - packageToConfig', () => { }, }); }); - it('returns datasource with custom name', () => { - expect(packageToConfigDatasource(mockPackage, '1', '2', 'ds-1')).toEqual({ + it('returns package config with custom name', () => { + expect(packageToPackageConfig(mockPackage, '1', '2', 'default', 'pkgConfig-1')).toEqual({ config_id: '1', + namespace: 'default', enabled: true, inputs: [], - name: 'ds-1', + name: 'pkgConfig-1', output_id: '2', package: { name: 'mock-package', @@ -342,21 +344,21 @@ describe('Ingest Manager - packageToConfig', () => { }, }); }); - it('returns datasource with namespace and description', () => { + it('returns package config with namespace and description', () => { expect( - packageToConfigDatasource( + packageToPackageConfig( mockPackage, '1', '2', - 'ds-1', 'mock-namespace', + 'pkgConfig-1', 'Test description' ) ).toEqual({ config_id: '1', enabled: true, inputs: [], - name: 'ds-1', + name: 'pkgConfig-1', namespace: 'mock-namespace', description: 'Test description', output_id: '2', @@ -367,17 +369,20 @@ describe('Ingest Manager - packageToConfig', () => { }, }); }); - it('returns datasource with inputs', () => { - const mockPackageWithDatasources = ({ + it('returns package config with inputs', () => { + const mockPackageWithConfigTemplates = ({ ...mockPackage, config_templates: [{ inputs: [{ type: 'foo' }] }], } as unknown) as PackageInfo; - expect(packageToConfigDatasource(mockPackageWithDatasources, '1', '2', 'ds-1')).toEqual({ + expect( + packageToPackageConfig(mockPackageWithConfigTemplates, '1', '2', 'default', 'pkgConfig-1') + ).toEqual({ config_id: '1', + namespace: 'default', enabled: true, inputs: [{ type: 'foo', enabled: true, streams: [] }], - name: 'ds-1', + name: 'pkgConfig-1', output_id: '2', package: { name: 'mock-package', diff --git a/x-pack/plugins/ingest_manager/common/services/package_to_config.ts b/x-pack/plugins/ingest_manager/common/services/package_to_config.ts index 1817077e97f9c..5957267c7304c 100644 --- a/x-pack/plugins/ingest_manager/common/services/package_to_config.ts +++ b/x-pack/plugins/ingest_manager/common/services/package_to_config.ts @@ -8,12 +8,12 @@ import { RegistryConfigTemplate, RegistryVarsEntry, RegistryStream, - Datasource, - DatasourceConfigRecord, - DatasourceConfigRecordEntry, - DatasourceInput, - DatasourceInputStream, - NewDatasource, + PackageConfig, + PackageConfigConfigRecord, + PackageConfigConfigRecordEntry, + PackageConfigInput, + PackageConfigInputStream, + NewPackageConfig, } from '../types'; const getStreamsForInputType = ( @@ -40,27 +40,27 @@ const getStreamsForInputType = ( }; /* - * This service creates a datasource inputs definition from defaults provided in package info + * This service creates a package config inputs definition from defaults provided in package info */ -export const packageToConfigDatasourceInputs = (packageInfo: PackageInfo): Datasource['inputs'] => { - const inputs: Datasource['inputs'] = []; +export const packageToPackageConfigInputs = (packageInfo: PackageInfo): PackageConfig['inputs'] => { + const inputs: PackageConfig['inputs'] = []; - // Assume package will only ever ship one datasource for now - const packageDatasource: RegistryConfigTemplate | null = + // Assume package will only ever ship one package config template for now + const packageConfigTemplate: RegistryConfigTemplate | null = packageInfo.config_templates && packageInfo.config_templates[0] ? packageInfo.config_templates[0] : null; - // Create datasource input property - if (packageDatasource?.inputs?.length) { - // Map each package datasource input to agent config datasource input - packageDatasource.inputs.forEach((packageInput) => { + // Create package config input property + if (packageConfigTemplate?.inputs?.length) { + // Map each package package config input to agent config package config input + packageConfigTemplate.inputs.forEach((packageInput) => { // Reduces registry var def into config object entry const varsReducer = ( - configObject: DatasourceConfigRecord, + configObject: PackageConfigConfigRecord, registryVar: RegistryVarsEntry - ): DatasourceConfigRecord => { - const configEntry: DatasourceConfigRecordEntry = { + ): PackageConfigConfigRecord => { + const configEntry: PackageConfigConfigRecordEntry = { value: !registryVar.default && registryVar.multi ? [] : registryVar.default, }; if (registryVar.type) { @@ -70,12 +70,12 @@ export const packageToConfigDatasourceInputs = (packageInfo: PackageInfo): Datas return configObject; }; - // Map each package input stream into datasource input stream - const streams: DatasourceInputStream[] = getStreamsForInputType( + // Map each package input stream into package config input stream + const streams: PackageConfigInputStream[] = getStreamsForInputType( packageInput.type, packageInfo ).map((packageStream) => { - const stream: DatasourceInputStream = { + const stream: PackageConfigInputStream = { id: `${packageInput.type}-${packageStream.dataset.name}`, enabled: packageStream.enabled === false ? false : true, dataset: { @@ -89,7 +89,7 @@ export const packageToConfigDatasourceInputs = (packageInfo: PackageInfo): Datas return stream; }); - const input: DatasourceInput = { + const input: PackageConfigInput = { type: packageInput.type, enabled: streams.length ? !!streams.find((stream) => stream.enabled) : true, streams, @@ -107,23 +107,23 @@ export const packageToConfigDatasourceInputs = (packageInfo: PackageInfo): Datas }; /** - * Builds a `NewDatasource` structure based on a package + * Builds a `NewPackageConfig` structure based on a package * * @param packageInfo * @param configId * @param outputId - * @param datasourceName + * @param packageConfigName */ -export const packageToConfigDatasource = ( +export const packageToPackageConfig = ( packageInfo: PackageInfo, configId: string, outputId: string, - datasourceName?: string, - namespace?: string, + namespace: string = '', + packageConfigName?: string, description?: string -): NewDatasource => { +): NewPackageConfig => { return { - name: datasourceName || `${packageInfo.name}-1`, + name: packageConfigName || `${packageInfo.name}-1`, namespace, description, package: { @@ -134,6 +134,6 @@ export const packageToConfigDatasource = ( enabled: true, config_id: configId, output_id: outputId, - inputs: packageToConfigDatasourceInputs(packageInfo), + inputs: packageToPackageConfigInputs(packageInfo), }; }; diff --git a/x-pack/plugins/ingest_manager/common/services/routes.ts b/x-pack/plugins/ingest_manager/common/services/routes.ts index 8136abe1a42d4..463a18887174c 100644 --- a/x-pack/plugins/ingest_manager/common/services/routes.ts +++ b/x-pack/plugins/ingest_manager/common/services/routes.ts @@ -6,7 +6,7 @@ import { EPM_API_ROOT, EPM_API_ROUTES, - DATASOURCE_API_ROUTES, + PACKAGE_CONFIG_API_ROUTES, AGENT_CONFIG_API_ROUTES, DATA_STREAM_API_ROUTES, FLEET_SETUP_API_ROUTES, @@ -44,25 +44,25 @@ export const epmRouteService = { }, }; -export const datasourceRouteService = { +export const packageConfigRouteService = { getListPath: () => { - return DATASOURCE_API_ROUTES.LIST_PATTERN; + return PACKAGE_CONFIG_API_ROUTES.LIST_PATTERN; }, - getInfoPath: (datasourceId: string) => { - return DATASOURCE_API_ROUTES.INFO_PATTERN.replace('{datasourceId}', datasourceId); + getInfoPath: (packageConfigId: string) => { + return PACKAGE_CONFIG_API_ROUTES.INFO_PATTERN.replace('{packageConfigId}', packageConfigId); }, getCreatePath: () => { - return DATASOURCE_API_ROUTES.CREATE_PATTERN; + return PACKAGE_CONFIG_API_ROUTES.CREATE_PATTERN; }, - getUpdatePath: (datasourceId: string) => { - return DATASOURCE_API_ROUTES.UPDATE_PATTERN.replace('{datasourceId}', datasourceId); + getUpdatePath: (packageConfigId: string) => { + return PACKAGE_CONFIG_API_ROUTES.UPDATE_PATTERN.replace('{packageConfigId}', packageConfigId); }, getDeletePath: () => { - return DATASOURCE_API_ROUTES.DELETE_PATTERN; + return PACKAGE_CONFIG_API_ROUTES.DELETE_PATTERN; }, }; diff --git a/x-pack/plugins/ingest_manager/common/types/models/agent_config.ts b/x-pack/plugins/ingest_manager/common/types/models/agent_config.ts index 0d7dc13af7a30..a6040742e45fc 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/agent_config.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/agent_config.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { Datasource, DatasourcePackage, DatasourceInputStream } from './datasource'; +import { PackageConfig, PackageConfigPackage } from './package_config'; import { Output } from './output'; export enum AgentConfigStatus { @@ -13,7 +13,7 @@ export enum AgentConfigStatus { export interface NewAgentConfig { name: string; - namespace?: string; + namespace: string; description?: string; is_default?: boolean; monitoring_enabled?: Array<'logs' | 'metrics'>; @@ -22,7 +22,7 @@ export interface NewAgentConfig { export interface AgentConfig extends NewAgentConfig { id: string; status: AgentConfigStatus; - datasources: string[] | Datasource[]; + package_configs: string[] | PackageConfig[]; updated_at: string; updated_by: string; revision: number; @@ -30,10 +30,14 @@ export interface AgentConfig extends NewAgentConfig { export type AgentConfigSOAttributes = Omit; -export type FullAgentConfigInputStream = Pick & { - dataset: { name: string }; +export interface FullAgentConfigInputStream { + id: string; + dataset: { + name: string; + type: string; + }; [key: string]: any; -}; +} export interface FullAgentConfigInput { id: string; @@ -42,7 +46,7 @@ export interface FullAgentConfigInput { dataset: { namespace: string }; use_output: string; meta?: { - package?: Pick; + package?: Pick; [key: string]: unknown; }; streams: FullAgentConfigInputStream[]; diff --git a/x-pack/plugins/ingest_manager/common/types/models/datasource.ts b/x-pack/plugins/ingest_manager/common/types/models/datasource.ts deleted file mode 100644 index aae65bb003995..0000000000000 --- a/x-pack/plugins/ingest_manager/common/types/models/datasource.ts +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export interface DatasourcePackage { - name: string; - title: string; - version: string; -} - -export interface DatasourceConfigRecordEntry { - type?: string; - value?: any; -} - -export type DatasourceConfigRecord = Record; - -export interface NewDatasourceInputStream { - id: string; - enabled: boolean; - dataset: { - name: string; - type: string; - }; - processors?: string[]; - config?: DatasourceConfigRecord; - vars?: DatasourceConfigRecord; -} - -export interface DatasourceInputStream extends NewDatasourceInputStream { - agent_stream?: any; -} - -export interface NewDatasourceInput { - type: string; - enabled: boolean; - processors?: string[]; - config?: DatasourceConfigRecord; - vars?: DatasourceConfigRecord; - streams: NewDatasourceInputStream[]; -} - -export interface DatasourceInput extends Omit { - streams: DatasourceInputStream[]; -} - -export interface NewDatasource { - name: string; - description?: string; - namespace?: string; - config_id: string; - enabled: boolean; - package?: DatasourcePackage; - output_id: string; - inputs: NewDatasourceInput[]; -} - -export interface Datasource extends Omit { - id: string; - inputs: DatasourceInput[]; - revision: number; - updated_at: string; - updated_by: string; - created_at: string; - created_by: string; -} - -export type DatasourceSOAttributes = Omit; diff --git a/x-pack/plugins/ingest_manager/common/types/models/epm.ts b/x-pack/plugins/ingest_manager/common/types/models/epm.ts index b001b994187e1..5b68cd2beeed4 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/epm.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/epm.ts @@ -19,7 +19,7 @@ export enum InstallStatus { uninstalling = 'uninstalling', } -export type DetailViewPanelName = 'overview' | 'data-sources' | 'settings'; +export type DetailViewPanelName = 'overview' | 'usages' | 'settings'; export type ServiceName = 'kibana' | 'elasticsearch'; export type AssetType = KibanaAssetType | ElasticsearchAssetType | AgentAssetType; diff --git a/x-pack/plugins/ingest_manager/common/types/models/index.ts b/x-pack/plugins/ingest_manager/common/types/models/index.ts index 2310fdd54a719..8ad716a4ba768 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/index.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/index.ts @@ -6,7 +6,7 @@ export * from './agent'; export * from './agent_config'; -export * from './datasource'; +export * from './package_config'; export * from './data_stream'; export * from './output'; export * from './epm'; diff --git a/x-pack/plugins/ingest_manager/common/types/models/package_config.ts b/x-pack/plugins/ingest_manager/common/types/models/package_config.ts new file mode 100644 index 0000000000000..e9595bab0174e --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/types/models/package_config.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface PackageConfigPackage { + name: string; + title: string; + version: string; +} + +export interface PackageConfigConfigRecordEntry { + type?: string; + value?: any; +} + +export type PackageConfigConfigRecord = Record; + +export interface NewPackageConfigInputStream { + id: string; + enabled: boolean; + dataset: { + name: string; + type: string; + }; + vars?: PackageConfigConfigRecord; + config?: PackageConfigConfigRecord; +} + +export interface PackageConfigInputStream extends NewPackageConfigInputStream { + compiled_stream?: any; +} + +export interface NewPackageConfigInput { + type: string; + enabled: boolean; + vars?: PackageConfigConfigRecord; + config?: PackageConfigConfigRecord; + streams: NewPackageConfigInputStream[]; +} + +export interface PackageConfigInput extends Omit { + streams: PackageConfigInputStream[]; +} + +export interface NewPackageConfig { + name: string; + description?: string; + namespace: string; + enabled: boolean; + config_id: string; + output_id: string; + package?: PackageConfigPackage; + inputs: NewPackageConfigInput[]; +} + +export interface PackageConfig extends Omit { + id: string; + inputs: PackageConfigInput[]; + revision: number; + updated_at: string; + updated_by: string; + created_at: string; + created_by: string; +} + +export type PackageConfigSOAttributes = Omit; diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/datasource.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/datasource.ts deleted file mode 100644 index 61f1f15d49259..0000000000000 --- a/x-pack/plugins/ingest_manager/common/types/rest_spec/datasource.ts +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { Datasource, NewDatasource } from '../models'; - -export interface GetDatasourcesRequest { - query: { - page: number; - perPage: number; - kuery?: string; - }; -} - -export interface GetDatasourcesResponse { - items: Datasource[]; - total: number; - page: number; - perPage: number; - success: boolean; -} - -export interface GetOneDatasourceRequest { - params: { - datasourceId: string; - }; -} - -export interface GetOneDatasourceResponse { - item: Datasource; - success: boolean; -} - -export interface CreateDatasourceRequest { - body: NewDatasource; -} - -export interface CreateDatasourceResponse { - item: Datasource; - success: boolean; -} - -export type UpdateDatasourceRequest = GetOneDatasourceRequest & { - body: NewDatasource; -}; - -export type UpdateDatasourceResponse = CreateDatasourceResponse; - -export interface DeleteDatasourcesRequest { - body: { - datasourceIds: string[]; - }; -} - -export type DeleteDatasourcesResponse = Array<{ - id: string; - success: boolean; -}>; diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/index.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/index.ts index 294e10aabe4ef..c40940fdbb623 100644 --- a/x-pack/plugins/ingest_manager/common/types/rest_spec/index.ts +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ export * from './common'; -export * from './datasource'; +export * from './package_config'; export * from './data_stream'; export * from './agent'; export * from './agent_config'; diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/package_config.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/package_config.ts new file mode 100644 index 0000000000000..4b8abbde47d5b --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/package_config.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; + * you may not use this file except in compliance with the Elastic License. + */ +import { PackageConfig, NewPackageConfig } from '../models'; + +export interface GetPackageConfigsRequest { + query: { + page: number; + perPage: number; + kuery?: string; + }; +} + +export interface GetPackageConfigsResponse { + items: PackageConfig[]; + total: number; + page: number; + perPage: number; + success: boolean; +} + +export interface GetOnePackageConfigRequest { + params: { + packageConfigId: string; + }; +} + +export interface GetOnePackageConfigResponse { + item: PackageConfig; + success: boolean; +} + +export interface CreatePackageConfigRequest { + body: NewPackageConfig; +} + +export interface CreatePackageConfigResponse { + item: PackageConfig; + success: boolean; +} + +export type UpdatePackageConfigRequest = GetOnePackageConfigRequest & { + body: NewPackageConfig; +}; + +export type UpdatePackageConfigResponse = CreatePackageConfigResponse; + +export interface DeletePackageConfigsRequest { + body: { + packageConfigIds: string[]; + }; +} + +export type DeletePackageConfigsResponse = Array<{ + id: string; + success: boolean; +}>; diff --git a/x-pack/plugins/ingest_manager/dev_docs/definitions.md b/x-pack/plugins/ingest_manager/dev_docs/definitions.md index 0d9e285ab80d3..a33d95f3afa38 100644 --- a/x-pack/plugins/ingest_manager/dev_docs/definitions.md +++ b/x-pack/plugins/ingest_manager/dev_docs/definitions.md @@ -5,15 +5,14 @@ Overall documentation of Ingest Management is now maintained in the `elastic/sta This section is to define terms used across ingest management. -## Data Source +## Package Config -A data source is a definition on how to collect data from a service, for example `nginx`. A data source contains +A package config is a definition on how to collect data from a service, for example `nginx`. A package config contains definitions for one or multiple inputs and each input can contain one or multiple streams. -With the example of the nginx Data Source, it contains to inputs: `logs` and `nginx/metrics`. Logs and metrics are collected +With the example of the nginx Package Config, it contains to inputs: `logs` and `nginx/metrics`. Logs and metrics are collected differently. The `logs` input contains two streams, `access` and `error`, the `nginx/metrics` input contains the stubstatus stream. - ## Data Stream Data Streams are a [new concept](https://github.com/elastic/elasticsearch/issues/53100) in Elasticsearch which simplify @@ -23,7 +22,6 @@ ingesting data and the setup of Elasticsearch. A single, unified agent that users can deploy to hosts or containers. It controls which data is collected from the host or containers and where the data is sent. It will run Beats, Endpoint or other monitoring programs as needed. It can operate standalone or pull a configuration policy from Fleet. - ## Elastic Package Registry The Elastic Package Registry (EPR) is a service which runs under [https://epr.elastic.co]. It serves the packages through its API. @@ -42,15 +40,15 @@ the index strategy is sent to Data Streams. ## Input -An input is the configuration unit in an Agent Config that defines the options on how to collect data from -an endpoint. This could be username / password which are need to authenticate with a service or a host url +An input is the configuration unit in an Agent Config that defines the options on how to collect data from +an endpoint. This could be username / password which are need to authenticate with a service or a host url as an example. -An input is part of a Data Source and contains streams. +An input is part of a Package Config and contains streams. ## Integration -An integration is a package with the type integration. An integration package has at least 1 data source +An integration is a package with the type integration. An integration package has at least 1 package config and usually collects data from / about a service. ## Namespace @@ -59,13 +57,12 @@ A user-specified string that will be used to part of the index name in Elasticse ## Package -A package contains all the assets for the Elastic Stack. A more detailed definition of a +A package contains all the assets for the Elastic Stack. A more detailed definition of a package can be found under https://github.com/elastic/package-registry. -Besides the assets, a package contains the data source definitions with its inputs and streams. +Besides the assets, a package contains the package config definitions with its inputs and streams. ## Stream A stream is a configuration unit in the Elastic Agent config. A stream is part of an input and defines how the data fetched by this input should be processed and which Data Stream to send it to. - diff --git a/x-pack/plugins/ingest_manager/dev_docs/schema/saved_objects.mml b/x-pack/plugins/ingest_manager/dev_docs/schema/saved_objects.mml index 373bd10ad6628..d157bf32fa66b 100644 --- a/x-pack/plugins/ingest_manager/dev_docs/schema/saved_objects.mml +++ b/x-pack/plugins/ingest_manager/dev_docs/schema/saved_objects.mml @@ -1,11 +1,11 @@ classDiagram - agent_configs "1" -- "*" datasources + agent_configs "1" -- "*" package_configs agent_configs "1" -- "*" enrollment_api_keys agent_configs "1" -- "*" agents : is used agent_configs "*" -- "*" outputs agents "1" -- "*" agent_events agents "1" -- "*" agent_events - package "1" -- "*" datasources + package "1" -- "*" package_configs class package { installed @@ -37,14 +37,14 @@ classDiagram } class agent_configs { - datasources // datasource ids + package_configs // package_config ids name namespace description status } - class datasources { + class package_configs { name namespace config_id diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/index.ts index 2936eea21805d..d31d66d889c96 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/index.ts @@ -11,7 +11,7 @@ export { AGENT_EVENT_SAVED_OBJECT_TYPE, AGENT_SAVED_OBJECT_TYPE, ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, - DATASOURCE_SAVED_OBJECT_TYPE, + PACKAGE_CONFIG_SAVED_OBJECT_TYPE, } from '../../../../common'; export * from './page_paths'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/page_paths.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/page_paths.ts index 5ef7f45faec48..9881d5e40d8ab 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/page_paths.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/page_paths.ts @@ -18,9 +18,9 @@ export type StaticPage = export type DynamicPage = | 'integration_details' | 'configuration_details' - | 'add_datasource_from_configuration' - | 'add_datasource_from_integration' - | 'edit_datasource' + | 'add_integration_from_configuration' + | 'add_integration_to_configuration' + | 'edit_integration' | 'fleet_agent_list' | 'fleet_agent_details'; @@ -44,9 +44,9 @@ export const PAGE_ROUTING_PATHS = { configurations_list: '/configs', configuration_details: '/configs/:configId/:tabId?', configuration_details_settings: '/configs/:configId/settings', - add_datasource_from_configuration: '/configs/:configId/add-datasource', - add_datasource_from_integration: '/integrations/:pkgkey/add-datasource', - edit_datasource: '/configs/:configId/edit-datasource/:datasourceId', + add_integration_from_configuration: '/configs/:configId/add-integration', + add_integration_to_configuration: '/integrations/:pkgkey/add-integration', + edit_integration: '/configs/:configId/edit-integration/:packageConfigId', fleet: '/fleet', fleet_agent_list: '/fleet/agents', fleet_agent_details: '/fleet/agents/:agentId/:tabId?', @@ -71,10 +71,10 @@ export const pagePathGetters: { configurations: () => '/configs', configurations_list: () => '/configs', configuration_details: ({ configId, tabId }) => `/configs/${configId}${tabId ? `/${tabId}` : ''}`, - add_datasource_from_configuration: ({ configId }) => `/configs/${configId}/add-datasource`, - add_datasource_from_integration: ({ pkgkey }) => `/integrations/${pkgkey}/add-datasource`, - edit_datasource: ({ configId, datasourceId }) => - `/configs/${configId}/edit-datasource/${datasourceId}`, + add_integration_from_configuration: ({ configId }) => `/configs/${configId}/add-integration`, + add_integration_to_configuration: ({ pkgkey }) => `/integrations/${pkgkey}/add-integration`, + edit_integration: ({ configId, packageConfigId }) => + `/configs/${configId}/edit-integration/${packageConfigId}`, fleet: () => '/fleet', fleet_agent_list: ({ kuery }) => `/fleet/agents${kuery ? `?kuery=${kuery}` : ''}`, fleet_agent_details: ({ agentId, tabId }) => diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_breadcrumbs.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_breadcrumbs.tsx index 0c858cd2b305d..2b92987963ef6 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_breadcrumbs.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_breadcrumbs.tsx @@ -98,7 +98,7 @@ const breadcrumbGetters: { }, { text: configName }, ], - add_datasource_from_configuration: ({ configName, configId }) => [ + add_integration_from_configuration: ({ configName, configId }) => [ BASE_BREADCRUMB, { href: pagePathGetters.configurations(), @@ -111,12 +111,12 @@ const breadcrumbGetters: { text: configName, }, { - text: i18n.translate('xpack.ingestManager.breadcrumbs.addDatasourcePageTitle', { - defaultMessage: 'Add data source', + text: i18n.translate('xpack.ingestManager.breadcrumbs.addPackageConfigPageTitle', { + defaultMessage: 'Add integration', }), }, ], - add_datasource_from_integration: ({ pkgTitle, pkgkey }) => [ + add_integration_to_configuration: ({ pkgTitle, pkgkey }) => [ BASE_BREADCRUMB, { href: pagePathGetters.integrations(), @@ -129,12 +129,12 @@ const breadcrumbGetters: { text: pkgTitle, }, { - text: i18n.translate('xpack.ingestManager.breadcrumbs.addDatasourcePageTitle', { - defaultMessage: 'Add data source', + text: i18n.translate('xpack.ingestManager.breadcrumbs.addPackageConfigPageTitle', { + defaultMessage: 'Add integration', }), }, ], - edit_datasource: ({ configName, configId }) => [ + edit_integration: ({ configName, configId }) => [ BASE_BREADCRUMB, { href: pagePathGetters.configurations(), @@ -147,8 +147,8 @@ const breadcrumbGetters: { text: configName, }, { - text: i18n.translate('xpack.ingestManager.breadcrumbs.editDatasourcePageTitle', { - defaultMessage: 'Edit data source', + text: i18n.translate('xpack.ingestManager.breadcrumbs.editPackageConfigPageTitle', { + defaultMessage: 'Edit integration', }), }, ], diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/datasource.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/datasource.ts deleted file mode 100644 index e2fc190e158f9..0000000000000 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/datasource.ts +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { sendRequest, useRequest } from './use_request'; -import { datasourceRouteService } from '../../services'; -import { - CreateDatasourceRequest, - CreateDatasourceResponse, - UpdateDatasourceRequest, - UpdateDatasourceResponse, -} from '../../types'; -import { - DeleteDatasourcesRequest, - DeleteDatasourcesResponse, - GetDatasourcesRequest, - GetDatasourcesResponse, - GetOneDatasourceResponse, -} from '../../../../../common/types/rest_spec'; - -export const sendCreateDatasource = (body: CreateDatasourceRequest['body']) => { - return sendRequest({ - path: datasourceRouteService.getCreatePath(), - method: 'post', - body: JSON.stringify(body), - }); -}; - -export const sendUpdateDatasource = ( - datasourceId: string, - body: UpdateDatasourceRequest['body'] -) => { - return sendRequest({ - path: datasourceRouteService.getUpdatePath(datasourceId), - method: 'put', - body: JSON.stringify(body), - }); -}; - -export const sendDeleteDatasource = (body: DeleteDatasourcesRequest['body']) => { - return sendRequest({ - path: datasourceRouteService.getDeletePath(), - method: 'post', - body: JSON.stringify(body), - }); -}; - -export function useGetDatasources(query: GetDatasourcesRequest['query']) { - return useRequest({ - method: 'get', - path: datasourceRouteService.getListPath(), - query, - }); -} - -export const sendGetOneDatasource = (datasourceId: string) => { - return sendRequest({ - path: datasourceRouteService.getInfoPath(datasourceId), - method: 'get', - }); -}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/index.ts index 8aec20d15c888..0d703925f2cf3 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/index.ts @@ -5,7 +5,7 @@ */ export { setHttpClient, sendRequest, useRequest } from './use_request'; export * from './agent_config'; -export * from './datasource'; +export * from './package_config'; export * from './data_stream'; export * from './agents'; export * from './enrollment_api_keys'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/package_config.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/package_config.ts new file mode 100644 index 0000000000000..aba950123ead3 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/package_config.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { sendRequest, useRequest } from './use_request'; +import { packageConfigRouteService } from '../../services'; +import { + CreatePackageConfigRequest, + CreatePackageConfigResponse, + UpdatePackageConfigRequest, + UpdatePackageConfigResponse, +} from '../../types'; +import { + DeletePackageConfigsRequest, + DeletePackageConfigsResponse, + GetPackageConfigsRequest, + GetPackageConfigsResponse, + GetOnePackageConfigResponse, +} from '../../../../../common/types/rest_spec'; + +export const sendCreatePackageConfig = (body: CreatePackageConfigRequest['body']) => { + return sendRequest({ + path: packageConfigRouteService.getCreatePath(), + method: 'post', + body: JSON.stringify(body), + }); +}; + +export const sendUpdatePackageConfig = ( + packageConfigId: string, + body: UpdatePackageConfigRequest['body'] +) => { + return sendRequest({ + path: packageConfigRouteService.getUpdatePath(packageConfigId), + method: 'put', + body: JSON.stringify(body), + }); +}; + +export const sendDeletePackageConfig = (body: DeletePackageConfigsRequest['body']) => { + return sendRequest({ + path: packageConfigRouteService.getDeletePath(), + method: 'post', + body: JSON.stringify(body), + }); +}; + +export function useGetPackageConfigs(query: GetPackageConfigsRequest['query']) { + return useRequest({ + method: 'get', + path: packageConfigRouteService.getListPath(), + query, + }); +} + +export const sendGetOnePackageConfig = (packageConfigId: string) => { + return sendRequest({ + path: packageConfigRouteService.getInfoPath(packageConfigId), + method: 'get', + }); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/config_form.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/config_form.tsx index 73ddd567c515b..ad04e78bdcd11 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/config_form.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/config_form.tsx @@ -174,7 +174,7 @@ export const AgentConfigForm: React.FunctionComponent = ({ description={ } > @@ -336,7 +336,7 @@ export const AgentConfigForm: React.FunctionComponent = ({ 'xpack.ingestManager.agentConfigForm.systemMonitoringTooltipText', { defaultMessage: - 'Enable this option to bootstrap your configuration with a data source that collects system metrics and information.', + 'Enable this option to bootstrap your configuration with an integration that collects system metrics and information.', } )} position="right" diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/index.ts index f3ec15e0f477d..3794a1f1afd26 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/index.ts @@ -5,7 +5,7 @@ */ export { AgentConfigForm, agentConfigFormValidation } from './config_form'; export { AgentConfigDeleteProvider } from './config_delete_provider'; -export { DatasourceDeleteProvider } from './datasource_delete_provider'; +export { PackageConfigDeleteProvider } from './package_config_delete_provider'; export { LinkedAgentCount } from './linked_agent_count'; export { ConfirmDeployConfigModal } from './confirm_deploy_modal'; export { DangerEuiContextMenuItem } from './danger_eui_context_menu_item'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/datasource_delete_provider.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/package_config_delete_provider.tsx similarity index 66% rename from x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/datasource_delete_provider.tsx rename to x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/package_config_delete_provider.tsx index 86186f7f0a6dd..3421b70715c35 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/datasource_delete_provider.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/package_config_delete_provider.tsx @@ -8,23 +8,23 @@ import React, { Fragment, useMemo, useRef, useState } from 'react'; import { EuiCallOut, EuiConfirmModal, EuiOverlayMask, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { useCore, sendRequest, sendDeleteDatasource, useConfig } from '../../../hooks'; +import { useCore, sendRequest, sendDeletePackageConfig, useConfig } from '../../../hooks'; import { AGENT_API_ROUTES, AGENT_SAVED_OBJECT_TYPE } from '../../../constants'; import { AgentConfig } from '../../../types'; interface Props { agentConfig: AgentConfig; - children: (deleteDatasourcePrompt: DeleteAgentConfigDatasourcePrompt) => React.ReactElement; + children: (deletePackageConfigsPrompt: DeletePackageConfigsPrompt) => React.ReactElement; } -export type DeleteAgentConfigDatasourcePrompt = ( - datasourcesToDelete: string[], +export type DeletePackageConfigsPrompt = ( + packageConfigsToDelete: string[], onSuccess?: OnSuccessCallback ) => void; -type OnSuccessCallback = (datasourcesDeleted: string[]) => void; +type OnSuccessCallback = (packageConfigsDeleted: string[]) => void; -export const DatasourceDeleteProvider: React.FunctionComponent = ({ +export const PackageConfigDeleteProvider: React.FunctionComponent = ({ agentConfig, children, }) => { @@ -32,7 +32,7 @@ export const DatasourceDeleteProvider: React.FunctionComponent = ({ const { fleet: { enabled: isFleetEnabled }, } = useConfig(); - const [datasources, setDatasources] = useState([]); + const [packageConfigs, setPackageConfigs] = useState([]); const [isModalOpen, setIsModalOpen] = useState(false); const [isLoadingAgentsCount, setIsLoadingAgentsCount] = useState(false); const [agentsCount, setAgentsCount] = useState(0); @@ -60,13 +60,13 @@ export const DatasourceDeleteProvider: React.FunctionComponent = ({ [agentConfig.id, isFleetEnabled, isLoadingAgentsCount] ); - const deleteDatasourcesPrompt = useMemo( - (): DeleteAgentConfigDatasourcePrompt => (datasourcesToDelete, onSuccess = () => undefined) => { - if (!Array.isArray(datasourcesToDelete) || datasourcesToDelete.length === 0) { - throw new Error('No datasources specified for deletion'); + const deletePackageConfigsPrompt = useMemo( + (): DeletePackageConfigsPrompt => (packageConfigsToDelete, onSuccess = () => undefined) => { + if (!Array.isArray(packageConfigsToDelete) || packageConfigsToDelete.length === 0) { + throw new Error('No package configs specified for deletion'); } setIsModalOpen(true); - setDatasources(datasourcesToDelete); + setPackageConfigs(packageConfigsToDelete); fetchAgentsCount(); onSuccessCallback.current = onSuccess; }, @@ -75,7 +75,7 @@ export const DatasourceDeleteProvider: React.FunctionComponent = ({ const closeModal = useMemo( () => () => { - setDatasources([]); + setPackageConfigs([]); setIsLoading(false); setIsLoadingAgentsCount(false); setIsModalOpen(false); @@ -83,12 +83,12 @@ export const DatasourceDeleteProvider: React.FunctionComponent = ({ [] ); - const deleteDatasources = useMemo( + const deletePackageConfigs = useMemo( () => async () => { setIsLoading(true); try { - const { data } = await sendDeleteDatasource({ datasourceIds: datasources }); + const { data } = await sendDeletePackageConfig({ packageConfigIds: packageConfigs }); const successfulResults = data?.filter((result) => result.success) || []; const failedResults = data?.filter((result) => !result.success) || []; @@ -96,16 +96,16 @@ export const DatasourceDeleteProvider: React.FunctionComponent = ({ const hasMultipleSuccesses = successfulResults.length > 1; const successMessage = hasMultipleSuccesses ? i18n.translate( - 'xpack.ingestManager.deleteDatasource.successMultipleNotificationTitle', + 'xpack.ingestManager.deletePackageConfig.successMultipleNotificationTitle', { - defaultMessage: 'Deleted {count} data sources', + defaultMessage: 'Deleted {count} integrations', values: { count: successfulResults.length }, } ) : i18n.translate( - 'xpack.ingestManager.deleteDatasource.successSingleNotificationTitle', + 'xpack.ingestManager.deletePackageConfig.successSingleNotificationTitle', { - defaultMessage: "Deleted data source '{id}'", + defaultMessage: "Deleted integration '{id}'", values: { id: successfulResults[0].id }, } ); @@ -116,16 +116,16 @@ export const DatasourceDeleteProvider: React.FunctionComponent = ({ const hasMultipleFailures = failedResults.length > 1; const failureMessage = hasMultipleFailures ? i18n.translate( - 'xpack.ingestManager.deleteDatasource.failureMultipleNotificationTitle', + 'xpack.ingestManager.deletePackageConfig.failureMultipleNotificationTitle', { - defaultMessage: 'Error deleting {count} data sources', + defaultMessage: 'Error deleting {count} integrations', values: { count: failedResults.length }, } ) : i18n.translate( - 'xpack.ingestManager.deleteDatasource.failureSingleNotificationTitle', + 'xpack.ingestManager.deletePackageConfig.failureSingleNotificationTitle', { - defaultMessage: "Error deleting data source '{id}'", + defaultMessage: "Error deleting integration '{id}'", values: { id: failedResults[0].id }, } ); @@ -137,14 +137,14 @@ export const DatasourceDeleteProvider: React.FunctionComponent = ({ } } catch (e) { notifications.toasts.addDanger( - i18n.translate('xpack.ingestManager.deleteDatasource.fatalErrorNotificationTitle', { - defaultMessage: 'Error deleting data source', + i18n.translate('xpack.ingestManager.deletePackageConfig.fatalErrorNotificationTitle', { + defaultMessage: 'Error deleting integration', }) ); } closeModal(); }, - [closeModal, datasources, notifications.toasts] + [closeModal, packageConfigs, notifications.toasts] ); const renderModal = () => { @@ -157,31 +157,31 @@ export const DatasourceDeleteProvider: React.FunctionComponent = ({ } onCancel={closeModal} - onConfirm={deleteDatasources} + onConfirm={deletePackageConfigs} cancelButtonText={ } confirmButtonText={ isLoading || isLoadingAgentsCount ? ( ) : ( ) @@ -191,7 +191,7 @@ export const DatasourceDeleteProvider: React.FunctionComponent = ({ > {isLoadingAgentsCount ? ( ) : agentsCount ? ( @@ -200,14 +200,14 @@ export const DatasourceDeleteProvider: React.FunctionComponent = ({ color="danger" title={ } > {agentConfig.name}, @@ -219,7 +219,7 @@ export const DatasourceDeleteProvider: React.FunctionComponent = ({ ) : null} {!isLoadingAgentsCount && ( )} @@ -230,7 +230,7 @@ export const DatasourceDeleteProvider: React.FunctionComponent = ({ return ( - {children(deleteDatasourcesPrompt)} + {children(deletePackageConfigsPrompt)} {renderModal()} ); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/custom_configure_datasource.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/custom_configure_datasource.tsx deleted file mode 100644 index 4263feb7cd8c7..0000000000000 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/custom_configure_datasource.tsx +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import React from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiEmptyPrompt, EuiText } from '@elastic/eui'; -import { NewDatasource } from '../../../../types'; -import { CreateDatasourceFrom } from '../types'; - -export interface CustomConfigureDatasourceProps { - packageName: string; - from: CreateDatasourceFrom; - datasource: NewDatasource; - datasourceId?: string; -} - -/** - * Custom content type that external plugins can provide to Ingest's - * Datasource configuration. - */ -export type CustomConfigureDatasourceContent = React.FC; - -type AllowedDatasourceKey = 'endpoint'; -const ConfigureDatasourceMapping: { - [key: string]: CustomConfigureDatasourceContent; -} = {}; - -/** - * Plugins can call this function from the start lifecycle to - * register a custom component in the Ingest Datasource configuration. - */ -export function registerDatasource( - key: AllowedDatasourceKey, - value: CustomConfigureDatasourceContent -) { - ConfigureDatasourceMapping[key] = value; -} - -const EmptyConfigureDatasource: CustomConfigureDatasourceContent = () => ( - -

- -

- - } - /> -); - -export const CustomConfigureDatasource = (props: CustomConfigureDatasourceProps) => { - const ConfigureDatasourceContent = - ConfigureDatasourceMapping[props.packageName] || EmptyConfigureDatasource; - return ; -}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/index.ts deleted file mode 100644 index 42848cc0f5e41..0000000000000 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -export { CreateDatasourcePageLayout } from './layout'; -export { DatasourceInputPanel } from './datasource_input_panel'; -export { DatasourceInputVarField } from './datasource_input_var_field'; -export { CustomConfigureDatasource } from './custom_configure_datasource'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/constants.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/constants.ts deleted file mode 100644 index 49223a8eb4531..0000000000000 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/constants.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -export const WeightedCreateDatasourceSteps = [ - 'selectConfig', - 'selectPackage', - 'configure', - 'review', -]; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/custom_package_config.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/custom_package_config.tsx new file mode 100644 index 0000000000000..98ab507c1453d --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/custom_package_config.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiEmptyPrompt, EuiText } from '@elastic/eui'; +import { NewPackageConfig } from '../../../../types'; +import { CreatePackageConfigFrom } from '../types'; + +export interface CustomConfigurePackageConfigProps { + packageName: string; + from: CreatePackageConfigFrom; + packageConfig: NewPackageConfig; + packageConfigId?: string; +} + +/** + * Custom content type that external plugins can provide to Ingest's + * package config UI. + */ +export type CustomConfigurePackageConfigContent = React.FC; + +type AllowedPackageKey = 'endpoint'; +const PackageConfigMapping: { + [key: string]: CustomConfigurePackageConfigContent; +} = {}; + +/** + * Plugins can call this function from the start lifecycle to + * register a custom component in the Ingest package config. + */ +export function registerPackageConfigComponent( + key: AllowedPackageKey, + value: CustomConfigurePackageConfigContent +) { + PackageConfigMapping[key] = value; +} + +const EmptyPackageConfig: CustomConfigurePackageConfigContent = () => ( + +

+ +

+ + } + /> +); + +export const CustomPackageConfig = (props: CustomConfigurePackageConfigProps) => { + const CustomPackageConfigContent = PackageConfigMapping[props.packageName] || EmptyPackageConfig; + return ; +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/index.ts new file mode 100644 index 0000000000000..e8d2ef329b25e --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/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; + * you may not use this file except in compliance with the Elastic License. + */ +export { CreatePackageConfigPageLayout } from './layout'; +export { PackageConfigInputPanel } from './package_config_input_panel'; +export { PackageConfigInputVarField } from './package_config_input_var_field'; +export { CustomPackageConfig } from './custom_package_config'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/layout.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/layout.tsx similarity index 83% rename from x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/layout.tsx rename to x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/layout.tsx index 6f23c0ce60850..e0f40f1b15375 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/layout.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/layout.tsx @@ -18,10 +18,10 @@ import { import { WithHeaderLayout } from '../../../../layouts'; import { AgentConfig, PackageInfo } from '../../../../types'; import { PackageIcon } from '../../../../components/package_icon'; -import { CreateDatasourceFrom } from '../types'; +import { CreatePackageConfigFrom } from '../types'; -export const CreateDatasourcePageLayout: React.FunctionComponent<{ - from: CreateDatasourceFrom; +export const CreatePackageConfigPageLayout: React.FunctionComponent<{ + from: CreatePackageConfigFrom; cancelUrl: string; onCancel?: React.ReactEventHandler; agentConfig?: AgentConfig; @@ -49,7 +49,7 @@ export const CreateDatasourcePageLayout: React.FunctionComponent<{ data-test-subj={`${dataTestSubj}_cancelBackLink`} > @@ -59,13 +59,13 @@ export const CreateDatasourcePageLayout: React.FunctionComponent<{

{from === 'edit' ? ( ) : ( )}

@@ -76,17 +76,17 @@ export const CreateDatasourcePageLayout: React.FunctionComponent<{ {from === 'edit' ? ( ) : from === 'config' ? ( ) : ( )} @@ -102,7 +102,7 @@ export const CreateDatasourcePageLayout: React.FunctionComponent<{ @@ -115,7 +115,7 @@ export const CreateDatasourcePageLayout: React.FunctionComponent<{ diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_config.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/package_config_input_config.tsx similarity index 77% rename from x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_config.tsx rename to x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/package_config_input_config.tsx index 6eed7e74d6bc6..85c0f2134d8dc 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_config.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/package_config_input_config.tsx @@ -15,20 +15,24 @@ import { EuiTitle, EuiIconTip, } from '@elastic/eui'; -import { DatasourceInput, RegistryVarsEntry } from '../../../../types'; -import { isAdvancedVar, DatasourceConfigValidationResults, validationHasErrors } from '../services'; -import { DatasourceInputVarField } from './datasource_input_var_field'; +import { PackageConfigInput, RegistryVarsEntry } from '../../../../types'; +import { + isAdvancedVar, + PackageConfigConfigValidationResults, + validationHasErrors, +} from '../services'; +import { PackageConfigInputVarField } from './package_config_input_var_field'; -export const DatasourceInputConfig: React.FunctionComponent<{ +export const PackageConfigInputConfig: React.FunctionComponent<{ packageInputVars?: RegistryVarsEntry[]; - datasourceInput: DatasourceInput; - updateDatasourceInput: (updatedInput: Partial) => void; - inputVarsValidationResults: DatasourceConfigValidationResults; + packageConfigInput: PackageConfigInput; + updatePackageConfigInput: (updatedInput: Partial) => void; + inputVarsValidationResults: PackageConfigConfigValidationResults; forceShowErrors?: boolean; }> = ({ packageInputVars, - datasourceInput, - updateDatasourceInput, + packageConfigInput, + updatePackageConfigInput, inputVarsValidationResults, forceShowErrors, }) => { @@ -60,7 +64,7 @@ export const DatasourceInputConfig: React.FunctionComponent<{

@@ -71,7 +75,7 @@ export const DatasourceInputConfig: React.FunctionComponent<{ } @@ -87,7 +91,7 @@ export const DatasourceInputConfig: React.FunctionComponent<{

@@ -97,16 +101,16 @@ export const DatasourceInputConfig: React.FunctionComponent<{ {requiredVars.map((varDef) => { const { name: varName, type: varType } = varDef; - const value = datasourceInput.vars![varName].value; + const value = packageConfigInput.vars![varName].value; return ( - { - updateDatasourceInput({ + updatePackageConfigInput({ vars: { - ...datasourceInput.vars, + ...packageConfigInput.vars, [varName]: { type: varType, value: newValue, @@ -132,7 +136,7 @@ export const DatasourceInputConfig: React.FunctionComponent<{ flush="left" > @@ -141,16 +145,16 @@ export const DatasourceInputConfig: React.FunctionComponent<{ {isShowingAdvanced ? advancedVars.map((varDef) => { const { name: varName, type: varType } = varDef; - const value = datasourceInput.vars![varName].value; + const value = packageConfigInput.vars![varName].value; return ( - { - updateDatasourceInput({ + updatePackageConfigInput({ vars: { - ...datasourceInput.vars, + ...packageConfigInput.vars, [varName]: { type: varType, value: newValue, diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_panel.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/package_config_input_panel.tsx similarity index 73% rename from x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_panel.tsx rename to x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/package_config_input_panel.tsx index 6f6fa5aaa7f3e..f9c9dcd469b25 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_panel.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/package_config_input_panel.tsx @@ -20,14 +20,14 @@ import { EuiIconTip, } from '@elastic/eui'; import { - DatasourceInput, - DatasourceInputStream, + PackageConfigInput, + PackageConfigInputStream, RegistryInput, RegistryStream, } from '../../../../types'; -import { DatasourceInputValidationResults, validationHasErrors } from '../services'; -import { DatasourceInputConfig } from './datasource_input_config'; -import { DatasourceInputStreamConfig } from './datasource_input_stream_config'; +import { PackageConfigInputValidationResults, validationHasErrors } from '../services'; +import { PackageConfigInputConfig } from './package_config_input_config'; +import { PackageConfigInputStreamConfig } from './package_config_input_stream'; const FlushHorizontalRule = styled(EuiHorizontalRule)` margin-left: -${(props) => props.theme.eui.paddingSizes.m}; @@ -35,18 +35,18 @@ const FlushHorizontalRule = styled(EuiHorizontalRule)` width: auto; `; -export const DatasourceInputPanel: React.FunctionComponent<{ +export const PackageConfigInputPanel: React.FunctionComponent<{ packageInput: RegistryInput; packageInputStreams: Array; - datasourceInput: DatasourceInput; - updateDatasourceInput: (updatedInput: Partial) => void; - inputValidationResults: DatasourceInputValidationResults; + packageConfigInput: PackageConfigInput; + updatePackageConfigInput: (updatedInput: Partial) => void; + inputValidationResults: PackageConfigInputValidationResults; forceShowErrors?: boolean; }> = ({ packageInput, packageInputStreams, - datasourceInput, - updateDatasourceInput, + packageConfigInput, + updatePackageConfigInput, inputValidationResults, forceShowErrors, }) => { @@ -78,7 +78,7 @@ export const DatasourceInputPanel: React.FunctionComponent<{ } @@ -90,12 +90,12 @@ export const DatasourceInputPanel: React.FunctionComponent<{ ) : null} } - checked={datasourceInput.enabled} + checked={packageConfigInput.enabled} onChange={(e) => { const enabled = e.target.checked; - updateDatasourceInput({ + updatePackageConfigInput({ enabled, - streams: datasourceInput.streams.map((stream) => ({ + streams: packageConfigInput.streams.map((stream) => ({ ...stream, enabled, })), @@ -108,13 +108,13 @@ export const DatasourceInputPanel: React.FunctionComponent<{ - {datasourceInput.streams.filter((stream) => stream.enabled).length} + {packageConfigInput.streams.filter((stream) => stream.enabled).length} ), @@ -131,7 +131,7 @@ export const DatasourceInputPanel: React.FunctionComponent<{ aria-label={ isShowingStreams ? i18n.translate( - 'xpack.ingestManager.createDatasource.stepConfigure.hideStreamsAriaLabel', + 'xpack.ingestManager.createPackageConfig.stepConfigure.hideStreamsAriaLabel', { defaultMessage: 'Hide {type} streams', values: { @@ -140,7 +140,7 @@ export const DatasourceInputPanel: React.FunctionComponent<{ } ) : i18n.translate( - 'xpack.ingestManager.createDatasource.stepConfigure.showStreamsAriaLabel', + 'xpack.ingestManager.createPackageConfig.stepConfigure.showStreamsAriaLabel', { defaultMessage: 'Show {type} streams', values: { @@ -161,10 +161,10 @@ export const DatasourceInputPanel: React.FunctionComponent<{ {/* Input level configuration */} {isShowingStreams && packageInput.vars && packageInput.vars.length ? ( - @@ -176,42 +176,44 @@ export const DatasourceInputPanel: React.FunctionComponent<{ {isShowingStreams ? ( {packageInputStreams.map((packageInputStream) => { - const datasourceInputStream = datasourceInput.streams.find( + const packageConfigInputStream = packageConfigInput.streams.find( (stream) => stream.dataset.name === packageInputStream.dataset.name ); - return datasourceInputStream ? ( + return packageConfigInputStream ? ( - ) => { - const indexOfUpdatedStream = datasourceInput.streams.findIndex( + packageConfigInputStream={packageConfigInputStream} + updatePackageConfigInputStream={( + updatedStream: Partial + ) => { + const indexOfUpdatedStream = packageConfigInput.streams.findIndex( (stream) => stream.dataset.name === packageInputStream.dataset.name ); - const newStreams = [...datasourceInput.streams]; + const newStreams = [...packageConfigInput.streams]; newStreams[indexOfUpdatedStream] = { ...newStreams[indexOfUpdatedStream], ...updatedStream, }; - const updatedInput: Partial = { + const updatedInput: Partial = { streams: newStreams, }; // Update input enabled state if needed - if (!datasourceInput.enabled && updatedStream.enabled) { + if (!packageConfigInput.enabled && updatedStream.enabled) { updatedInput.enabled = true; } else if ( - datasourceInput.enabled && + packageConfigInput.enabled && !newStreams.find((stream) => stream.enabled) ) { updatedInput.enabled = false; } - updateDatasourceInput(updatedInput); + updatePackageConfigInput(updatedInput); }} inputStreamValidationResults={ - inputValidationResults.streams![datasourceInputStream.id] + inputValidationResults.streams![packageConfigInputStream.id] } forceShowErrors={forceShowErrors} /> diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_stream_config.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/package_config_input_stream.tsx similarity index 78% rename from x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_stream_config.tsx rename to x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/package_config_input_stream.tsx index f697ef736ef70..52a4748fe14c7 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_stream_config.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/package_config_input_stream.tsx @@ -16,20 +16,24 @@ import { EuiTextColor, EuiIconTip, } from '@elastic/eui'; -import { DatasourceInputStream, RegistryStream, RegistryVarsEntry } from '../../../../types'; -import { isAdvancedVar, DatasourceConfigValidationResults, validationHasErrors } from '../services'; -import { DatasourceInputVarField } from './datasource_input_var_field'; +import { PackageConfigInputStream, RegistryStream, RegistryVarsEntry } from '../../../../types'; +import { + isAdvancedVar, + PackageConfigConfigValidationResults, + validationHasErrors, +} from '../services'; +import { PackageConfigInputVarField } from './package_config_input_var_field'; -export const DatasourceInputStreamConfig: React.FunctionComponent<{ +export const PackageConfigInputStreamConfig: React.FunctionComponent<{ packageInputStream: RegistryStream; - datasourceInputStream: DatasourceInputStream; - updateDatasourceInputStream: (updatedStream: Partial) => void; - inputStreamValidationResults: DatasourceConfigValidationResults; + packageConfigInputStream: PackageConfigInputStream; + updatePackageConfigInputStream: (updatedStream: Partial) => void; + inputStreamValidationResults: PackageConfigConfigValidationResults; forceShowErrors?: boolean; }> = ({ packageInputStream, - datasourceInputStream, - updateDatasourceInputStream, + packageConfigInputStream, + updatePackageConfigInputStream, inputStreamValidationResults, forceShowErrors, }) => { @@ -68,7 +72,7 @@ export const DatasourceInputStreamConfig: React.FunctionComponent<{ } @@ -80,10 +84,10 @@ export const DatasourceInputStreamConfig: React.FunctionComponent<{ ) : null} } - checked={datasourceInputStream.enabled} + checked={packageConfigInputStream.enabled} onChange={(e) => { const enabled = e.target.checked; - updateDatasourceInputStream({ + updatePackageConfigInputStream({ enabled, }); }} @@ -101,16 +105,16 @@ export const DatasourceInputStreamConfig: React.FunctionComponent<{ {requiredVars.map((varDef) => { const { name: varName, type: varType } = varDef; - const value = datasourceInputStream.vars![varName].value; + const value = packageConfigInputStream.vars![varName].value; return ( - { - updateDatasourceInputStream({ + updatePackageConfigInputStream({ vars: { - ...datasourceInputStream.vars, + ...packageConfigInputStream.vars, [varName]: { type: varType, value: newValue, @@ -136,7 +140,7 @@ export const DatasourceInputStreamConfig: React.FunctionComponent<{ flush="left" > @@ -145,16 +149,16 @@ export const DatasourceInputStreamConfig: React.FunctionComponent<{ {isShowingAdvanced ? advancedVars.map((varDef) => { const { name: varName, type: varType } = varDef; - const value = datasourceInputStream.vars![varName].value; + const value = packageConfigInputStream.vars![varName].value; return ( - { - updateDatasourceInputStream({ + updatePackageConfigInputStream({ vars: { - ...datasourceInputStream.vars, + ...packageConfigInputStream.vars, [varName]: { type: varType, value: newValue, diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_var_field.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/package_config_input_var_field.tsx similarity index 93% rename from x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_var_field.tsx rename to x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/package_config_input_var_field.tsx index f5f21f685f180..8868e00ecc1f1 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_var_field.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/package_config_input_var_field.tsx @@ -12,7 +12,7 @@ import { RegistryVarsEntry } from '../../../../types'; import 'brace/mode/yaml'; import 'brace/theme/textmate'; -export const DatasourceInputVarField: React.FunctionComponent<{ +export const PackageConfigInputVarField: React.FunctionComponent<{ varDef: RegistryVarsEntry; value: any; onChange: (newValue: any) => void; @@ -78,7 +78,7 @@ export const DatasourceInputVarField: React.FunctionComponent<{ !required ? ( diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/index.tsx similarity index 71% rename from x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/index.tsx rename to x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/index.tsx index 18c4f2f82ac01..a81fb232ceaa0 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/index.tsx @@ -20,28 +20,32 @@ import { EuiStepProps } from '@elastic/eui/src/components/steps/step'; import { AgentConfig, PackageInfo, - NewDatasource, - CreateDatasourceRouteState, + NewPackageConfig, + CreatePackageConfigRouteState, } from '../../../types'; import { useLink, useBreadcrumbs, - sendCreateDatasource, + sendCreatePackageConfig, useCore, useConfig, sendGetAgentStatus, } from '../../../hooks'; import { ConfirmDeployConfigModal } from '../components'; -import { CreateDatasourcePageLayout } from './components'; -import { CreateDatasourceFrom, DatasourceFormState } from './types'; -import { DatasourceValidationResults, validateDatasource, validationHasErrors } from './services'; +import { CreatePackageConfigPageLayout } from './components'; +import { CreatePackageConfigFrom, PackageConfigFormState } from './types'; +import { + PackageConfigValidationResults, + validatePackageConfig, + validationHasErrors, +} from './services'; import { StepSelectPackage } from './step_select_package'; import { StepSelectConfig } from './step_select_config'; -import { StepConfigureDatasource } from './step_configure_datasource'; -import { StepDefineDatasource } from './step_define_datasource'; +import { StepConfigurePackage } from './step_configure_package'; +import { StepDefinePackageConfig } from './step_define_package_config'; import { useIntraAppState } from '../../../hooks/use_intra_app_state'; -export const CreateDatasourcePage: React.FunctionComponent = () => { +export const CreatePackageConfigPage: React.FunctionComponent = () => { const { notifications, chrome: { getIsNavDrawerLocked$ }, @@ -56,8 +60,8 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { } = useRouteMatch(); const { getHref, getPath } = useLink(); const history = useHistory(); - const routeState = useIntraAppState(); - const from: CreateDatasourceFrom = configId ? 'config' : 'package'; + const routeState = useIntraAppState(); + const from: CreatePackageConfigFrom = configId ? 'config' : 'package'; const [isNavDrawerLocked, setIsNavDrawerLocked] = useState(false); useEffect(() => { @@ -90,21 +94,22 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { }, [agentConfigId, isFleetEnabled]); const [agentCount, setAgentCount] = useState(0); - // New datasource state - const [datasource, setDatasource] = useState({ + // New package config state + const [packageConfig, setPackageConfig] = useState({ name: '', description: '', + namespace: '', config_id: '', enabled: true, output_id: '', // TODO: Blank for now as we only support default output inputs: [], }); - // Datasource validation state - const [validationResults, setValidationResults] = useState(); + // Package config validation state + const [validationResults, setValidationResults] = useState(); // Form state - const [formState, setFormState] = useState('INVALID'); + const [formState, setFormState] = useState('INVALID'); // Update package info method const updatePackageInfo = useCallback( @@ -146,33 +151,36 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { const hasErrors = validationResults ? validationHasErrors(validationResults) : false; - // Update datasource method - const updateDatasource = (updatedFields: Partial) => { - const newDatasource = { - ...datasource, + // Update package config method + const updatePackageConfig = (updatedFields: Partial) => { + const newPackageConfig = { + ...packageConfig, ...updatedFields, }; - setDatasource(newDatasource); + setPackageConfig(newPackageConfig); // eslint-disable-next-line no-console - console.debug('Datasource updated', newDatasource); - const newValidationResults = updateDatasourceValidation(newDatasource); - const hasPackage = newDatasource.package; + console.debug('Package config updated', newPackageConfig); + const newValidationResults = updatePackageConfigValidation(newPackageConfig); + const hasPackage = newPackageConfig.package; const hasValidationErrors = newValidationResults ? validationHasErrors(newValidationResults) : false; - const hasAgentConfig = newDatasource.config_id && newDatasource.config_id !== ''; + const hasAgentConfig = newPackageConfig.config_id && newPackageConfig.config_id !== ''; if (hasPackage && hasAgentConfig && !hasValidationErrors) { setFormState('VALID'); } }; - const updateDatasourceValidation = (newDatasource?: NewDatasource) => { + const updatePackageConfigValidation = (newPackageConfig?: NewPackageConfig) => { if (packageInfo) { - const newValidationResult = validateDatasource(newDatasource || datasource, packageInfo); + const newValidationResult = validatePackageConfig( + newPackageConfig || packageConfig, + packageInfo + ); setValidationResults(newValidationResult); // eslint-disable-next-line no-console - console.debug('Datasource validation results', newValidationResult); + console.debug('Package config validation results', newValidationResult); return newValidationResult; } @@ -198,10 +206,10 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { [routeState, navigateToApp] ); - // Save datasource - const saveDatasource = async () => { + // Save package config + const savePackageConfig = async () => { setFormState('LOADING'); - const result = await sendCreateDatasource(datasource); + const result = await sendCreatePackageConfig(packageConfig); setFormState('SUBMITTED'); return result; }; @@ -215,7 +223,7 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { setFormState('CONFIRM'); return; } - const { error, data } = await saveDatasource(); + const { error, data } = await savePackageConfig(); if (!error) { if (routeState && routeState.onSaveNavigateTo) { navigateToApp( @@ -228,22 +236,22 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { } notifications.toasts.addSuccess({ - title: i18n.translate('xpack.ingestManager.createDatasource.addedNotificationTitle', { - defaultMessage: `Successfully added '{datasourceName}'`, + title: i18n.translate('xpack.ingestManager.createPackageConfig.addedNotificationTitle', { + defaultMessage: `Successfully added '{packageConfigName}'`, values: { - datasourceName: datasource.name, + packageConfigName: packageConfig.name, }, }), text: agentCount && agentConfig - ? i18n.translate('xpack.ingestManager.createDatasource.addedNotificationMessage', { + ? i18n.translate('xpack.ingestManager.createPackageConfig.addedNotificationMessage', { defaultMessage: `Fleet will deploy updates to all agents that use the '{agentConfigName}' configuration`, values: { agentConfigName: agentConfig.name, }, }) : undefined, - 'data-test-subj': 'datasourceCreateSuccessToast', + 'data-test-subj': 'packageConfigCreateSuccessToast', }); } else { notifications.toasts.addError(error, { @@ -288,45 +296,54 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { const steps: EuiStepProps[] = [ from === 'package' ? { - title: i18n.translate('xpack.ingestManager.createDatasource.stepSelectAgentConfigTitle', { - defaultMessage: 'Select an agent configuration', - }), + title: i18n.translate( + 'xpack.ingestManager.createPackageConfig.stepSelectAgentConfigTitle', + { + defaultMessage: 'Select an agent configuration', + } + ), children: stepSelectConfig, } : { - title: i18n.translate('xpack.ingestManager.createDatasource.stepSelectPackageTitle', { + title: i18n.translate('xpack.ingestManager.createPackageConfig.stepSelectPackageTitle', { defaultMessage: 'Select an integration', }), children: stepSelectPackage, }, { - title: i18n.translate('xpack.ingestManager.createDatasource.stepDefineDatasourceTitle', { - defaultMessage: 'Define your data source', - }), + title: i18n.translate( + 'xpack.ingestManager.createPackageConfig.stepDefinePackageConfigTitle', + { + defaultMessage: 'Define your integration', + } + ), status: !packageInfo || !agentConfig ? 'disabled' : undefined, children: agentConfig && packageInfo ? ( - ) : null, }, { - title: i18n.translate('xpack.ingestManager.createDatasource.stepConfgiureDatasourceTitle', { - defaultMessage: 'Select the data you want to collect', - }), + title: i18n.translate( + 'xpack.ingestManager.createPackageConfig.stepConfigurePackageConfigTitle', + { + defaultMessage: 'Select the data you want to collect', + } + ), status: !packageInfo || !agentConfig ? 'disabled' : undefined, 'data-test-subj': 'dataCollectionSetupStep', children: agentConfig && packageInfo ? ( - @@ -335,7 +352,7 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { ]; return ( - + {formState === 'CONFIRM' && agentConfig && ( { color="ghost" href={cancelUrl} onClick={cancelClickHandler} - data-test-subj="createDatasourceCancelButton" + data-test-subj="createPackageConfigCancelButton" > @@ -389,17 +406,17 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { iconType="save" color="primary" fill - data-test-subj="createDatasourceSaveButton" + data-test-subj="createPackageConfigSaveButton" > - + ); }; @@ -407,7 +424,7 @@ const ConfigurationBreadcrumb: React.FunctionComponent<{ configName: string; configId: string; }> = ({ configName, configId }) => { - useBreadcrumbs('add_datasource_from_configuration', { configName, configId }); + useBreadcrumbs('add_integration_from_configuration', { configName, configId }); return null; }; @@ -415,6 +432,6 @@ const IntegrationBreadcrumb: React.FunctionComponent<{ pkgTitle: string; pkgkey: string; }> = ({ pkgTitle, pkgkey }) => { - useBreadcrumbs('add_datasource_from_integration', { pkgTitle, pkgkey }); + useBreadcrumbs('add_integration_to_configuration', { pkgTitle, pkgkey }); return null; }; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/services/index.ts similarity index 65% rename from x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/index.ts rename to x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/services/index.ts index d99f0712db3c3..6cfb1c74bd661 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/services/index.ts @@ -5,9 +5,9 @@ */ export { isAdvancedVar } from './is_advanced_var'; export { - DatasourceValidationResults, - DatasourceConfigValidationResults, - DatasourceInputValidationResults, - validateDatasource, + PackageConfigValidationResults, + PackageConfigConfigValidationResults, + PackageConfigInputValidationResults, + validatePackageConfig, validationHasErrors, -} from './validate_datasource'; +} from './validate_package_config'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/is_advanced_var.test.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/services/is_advanced_var.test.ts similarity index 100% rename from x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/is_advanced_var.test.ts rename to x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/services/is_advanced_var.test.ts diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/is_advanced_var.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/services/is_advanced_var.ts similarity index 100% rename from x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/is_advanced_var.ts rename to x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/services/is_advanced_var.ts diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/validate_datasource.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/services/validate_package_config.ts similarity index 69% rename from x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/validate_datasource.ts rename to x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/services/validate_package_config.ts index 30dca4a5fbf81..cd301747c3f53 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/validate_datasource.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/services/validate_package_config.ts @@ -7,10 +7,10 @@ import { i18n } from '@kbn/i18n'; import { safeLoad } from 'js-yaml'; import { getFlattenedObject } from '../../../../services'; import { - NewDatasource, - DatasourceInput, - DatasourceInputStream, - DatasourceConfigRecordEntry, + NewPackageConfig, + PackageConfigInput, + PackageConfigInputStream, + PackageConfigConfigRecordEntry, PackageInfo, RegistryInput, RegistryStream, @@ -21,42 +21,52 @@ type Errors = string[] | null; type ValidationEntry = Record; -export interface DatasourceConfigValidationResults { +export interface PackageConfigConfigValidationResults { vars?: ValidationEntry; } -export type DatasourceInputValidationResults = DatasourceConfigValidationResults & { - streams?: Record; +export type PackageConfigInputValidationResults = PackageConfigConfigValidationResults & { + streams?: Record; }; -export interface DatasourceValidationResults { +export interface PackageConfigValidationResults { name: Errors; description: Errors; - inputs: Record | null; + namespace: Errors; + inputs: Record | null; } /* - * Returns validation information for a given datasource configuration and package info - * Note: this method assumes that `datasource` is correctly structured for the given package + * Returns validation information for a given package config and package info + * Note: this method assumes that `packageConfig` is correctly structured for the given package */ -export const validateDatasource = ( - datasource: NewDatasource, +export const validatePackageConfig = ( + packageConfig: NewPackageConfig, packageInfo: PackageInfo -): DatasourceValidationResults => { - const validationResults: DatasourceValidationResults = { +): PackageConfigValidationResults => { + const validationResults: PackageConfigValidationResults = { name: null, description: null, + namespace: null, inputs: {}, }; - if (!datasource.name.trim()) { + if (!packageConfig.name.trim()) { validationResults.name = [ - i18n.translate('xpack.ingestManager.datasourceValidation.nameRequiredErrorMessage', { + i18n.translate('xpack.ingestManager.packageConfigValidation.nameRequiredErrorMessage', { defaultMessage: 'Name is required', }), ]; } + if (!packageConfig.namespace.trim()) { + validationResults.namespace = [ + i18n.translate('xpack.ingestManager.packageConfigValidation.namespaceRequiredErrorMessage', { + defaultMessage: 'Namespace is required', + }), + ]; + } + if ( !packageInfo.config_templates || packageInfo.config_templates.length === 0 || @@ -83,13 +93,13 @@ export const validateDatasource = ( return datasets; }, {} as Record); - // Validate each datasource input with either its own config fields or streams - datasource.inputs.forEach((input) => { + // Validate each package config input with either its own config fields or streams + packageConfig.inputs.forEach((input) => { if (!input.vars && !input.streams) { return; } - const inputValidationResults: DatasourceInputValidationResults = { + const inputValidationResults: PackageConfigInputValidationResults = { vars: undefined, streams: {}, }; @@ -107,7 +117,7 @@ export const validateDatasource = ( if (inputConfigs.length) { inputValidationResults.vars = inputConfigs.reduce((results, [name, configEntry]) => { results[name] = input.enabled - ? validateDatasourceConfig(configEntry, inputVarsByName[name]) + ? validatePackageConfigConfig(configEntry, inputVarsByName[name]) : null; return results; }, {} as ValidationEntry); @@ -118,7 +128,7 @@ export const validateDatasource = ( // Validate each input stream with config fields if (input.streams.length) { input.streams.forEach((stream) => { - const streamValidationResults: DatasourceConfigValidationResults = {}; + const streamValidationResults: PackageConfigConfigValidationResults = {}; // Validate stream-level config fields if (stream.vars) { @@ -136,7 +146,7 @@ export const validateDatasource = ( (results, [name, configEntry]) => { results[name] = input.enabled && stream.enabled - ? validateDatasourceConfig(configEntry, streamVarsByName[name]) + ? validatePackageConfigConfig(configEntry, streamVarsByName[name]) : null; return results; }, @@ -161,8 +171,8 @@ export const validateDatasource = ( return validationResults; }; -const validateDatasourceConfig = ( - configEntry: DatasourceConfigRecordEntry, +const validatePackageConfigConfig = ( + configEntry: PackageConfigConfigRecordEntry, varDef: RegistryVarsEntry ): string[] | null => { const errors = []; @@ -176,7 +186,7 @@ const validateDatasourceConfig = ( if (varDef.required) { if (parsedValue === undefined || (typeof parsedValue === 'string' && !parsedValue)) { errors.push( - i18n.translate('xpack.ingestManager.datasourceValidation.requiredErrorMessage', { + i18n.translate('xpack.ingestManager.packageConfigValidation.requiredErrorMessage', { defaultMessage: '{fieldName} is required', values: { fieldName: varDef.title || varDef.name, @@ -191,9 +201,12 @@ const validateDatasourceConfig = ( parsedValue = safeLoad(value); } catch (e) { errors.push( - i18n.translate('xpack.ingestManager.datasourceValidation.invalidYamlFormatErrorMessage', { - defaultMessage: 'Invalid YAML format', - }) + i18n.translate( + 'xpack.ingestManager.packageConfigValidation.invalidYamlFormatErrorMessage', + { + defaultMessage: 'Invalid YAML format', + } + ) ); } } @@ -201,7 +214,7 @@ const validateDatasourceConfig = ( if (varDef.multi) { if (parsedValue && !Array.isArray(parsedValue)) { errors.push( - i18n.translate('xpack.ingestManager.datasourceValidation.invalidArrayErrorMessage', { + i18n.translate('xpack.ingestManager.packageConfigValidation.invalidArrayErrorMessage', { defaultMessage: 'Invalid format', }) ); @@ -211,7 +224,7 @@ const validateDatasourceConfig = ( (!parsedValue || (Array.isArray(parsedValue) && parsedValue.length === 0)) ) { errors.push( - i18n.translate('xpack.ingestManager.datasourceValidation.requiredErrorMessage', { + i18n.translate('xpack.ingestManager.packageConfigValidation.requiredErrorMessage', { defaultMessage: '{fieldName} is required', values: { fieldName: varDef.title || varDef.name, @@ -226,9 +239,9 @@ const validateDatasourceConfig = ( export const validationHasErrors = ( validationResults: - | DatasourceValidationResults - | DatasourceInputValidationResults - | DatasourceConfigValidationResults + | PackageConfigValidationResults + | PackageConfigInputValidationResults + | PackageConfigConfigValidationResults ) => { const flattenedValidation = getFlattenedObject(validationResults); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/validate_datasource.test.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/services/validate_package_config.ts.test.ts similarity index 88% rename from x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/validate_datasource.test.ts rename to x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/services/validate_package_config.ts.test.ts index 64facf01d474a..41d46f03dca23 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/validate_datasource.test.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/services/validate_package_config.ts.test.ts @@ -6,12 +6,12 @@ import { PackageInfo, InstallationStatus, - NewDatasource, + NewPackageConfig, RegistryConfigTemplate, } from '../../../../types'; -import { validateDatasource, validationHasErrors } from './validate_datasource'; +import { validatePackageConfig, validationHasErrors } from './validate_package_config'; -describe('Ingest Manager - validateDatasource()', () => { +describe('Ingest Manager - validatePackageConfig()', () => { const mockPackage = ({ name: 'mock-package', title: 'Mock package', @@ -92,9 +92,9 @@ describe('Ingest Manager - validateDatasource()', () => { ], config_templates: [ { - name: 'datasource1', - title: 'Datasource 1', - description: 'test datasource', + name: 'pkgConfig1', + title: 'Package config 1', + description: 'test package config', inputs: [ { type: 'foo', @@ -141,8 +141,9 @@ describe('Ingest Manager - validateDatasource()', () => { ], } as unknown) as PackageInfo; - const validDatasource: NewDatasource = { - name: 'datasource1-1', + const validPackageConfig: NewPackageConfig = { + name: 'pkgConfig1-1', + namespace: 'default', config_id: 'test-config', enabled: true, output_id: 'test-output', @@ -225,8 +226,8 @@ describe('Ingest Manager - validateDatasource()', () => { ], }; - const invalidDatasource: NewDatasource = { - ...validDatasource, + const invalidPackageConfig: NewPackageConfig = { + ...validPackageConfig, name: '', inputs: [ { @@ -315,6 +316,7 @@ describe('Ingest Manager - validateDatasource()', () => { const noErrorsValidationResults = { name: null, description: null, + namespace: null, inputs: { foo: { vars: { @@ -348,14 +350,17 @@ describe('Ingest Manager - validateDatasource()', () => { }, }; - it('returns no errors for valid datasource configuration', () => { - expect(validateDatasource(validDatasource, mockPackage)).toEqual(noErrorsValidationResults); + it('returns no errors for valid package config', () => { + expect(validatePackageConfig(validPackageConfig, mockPackage)).toEqual( + noErrorsValidationResults + ); }); - it('returns errors for invalid datasource configuration', () => { - expect(validateDatasource(invalidDatasource, mockPackage)).toEqual({ + it('returns errors for invalid package config', () => { + expect(validatePackageConfig(invalidPackageConfig, mockPackage)).toEqual({ name: ['Name is required'], description: null, + namespace: null, inputs: { foo: { vars: { @@ -392,14 +397,17 @@ describe('Ingest Manager - validateDatasource()', () => { }); it('returns no errors for disabled inputs', () => { - const disabledInputs = invalidDatasource.inputs.map((input) => ({ ...input, enabled: false })); - expect(validateDatasource({ ...validDatasource, inputs: disabledInputs }, mockPackage)).toEqual( - noErrorsValidationResults - ); + const disabledInputs = invalidPackageConfig.inputs.map((input) => ({ + ...input, + enabled: false, + })); + expect( + validatePackageConfig({ ...validPackageConfig, inputs: disabledInputs }, mockPackage) + ).toEqual(noErrorsValidationResults); }); - it('returns only datasource and input-level errors for disabled streams', () => { - const inputsWithDisabledStreams = invalidDatasource.inputs.map((input) => + it('returns only package config and input-level errors for disabled streams', () => { + const inputsWithDisabledStreams = invalidPackageConfig.inputs.map((input) => input.streams ? { ...input, @@ -408,10 +416,14 @@ describe('Ingest Manager - validateDatasource()', () => { : input ); expect( - validateDatasource({ ...invalidDatasource, inputs: inputsWithDisabledStreams }, mockPackage) + validatePackageConfig( + { ...invalidPackageConfig, inputs: inputsWithDisabledStreams }, + mockPackage + ) ).toEqual({ name: ['Name is required'], description: null, + namespace: null, inputs: { foo: { vars: { @@ -449,48 +461,52 @@ describe('Ingest Manager - validateDatasource()', () => { }); }); - it('returns no errors for packages with no datasources', () => { + it('returns no errors for packages with no package configs', () => { expect( - validateDatasource(validDatasource, { + validatePackageConfig(validPackageConfig, { ...mockPackage, config_templates: undefined, }) ).toEqual({ name: null, description: null, + namespace: null, inputs: null, }); expect( - validateDatasource(validDatasource, { + validatePackageConfig(validPackageConfig, { ...mockPackage, config_templates: [], }) ).toEqual({ name: null, description: null, + namespace: null, inputs: null, }); }); it('returns no errors for packages with no inputs', () => { expect( - validateDatasource(validDatasource, { + validatePackageConfig(validPackageConfig, { ...mockPackage, config_templates: [{} as RegistryConfigTemplate], }) ).toEqual({ name: null, description: null, + namespace: null, inputs: null, }); expect( - validateDatasource(validDatasource, { + validatePackageConfig(validPackageConfig, { ...mockPackage, config_templates: [({ inputs: [] } as unknown) as RegistryConfigTemplate], }) ).toEqual({ name: null, description: null, + namespace: null, inputs: null, }); }); @@ -537,11 +553,12 @@ describe('Ingest Manager - validationHasErrors()', () => { ).toBe(false); }); - it('returns true for datasource validation results with errors', () => { + it('returns true for package config validation results with errors', () => { expect( validationHasErrors({ name: ['name error'], description: null, + namespace: null, inputs: { input1: { vars: { foo: null, bar: null }, @@ -554,6 +571,7 @@ describe('Ingest Manager - validationHasErrors()', () => { validationHasErrors({ name: null, description: null, + namespace: null, inputs: { input1: { vars: { foo: ['foo error'], bar: null }, @@ -566,6 +584,7 @@ describe('Ingest Manager - validationHasErrors()', () => { validationHasErrors({ name: null, description: null, + namespace: null, inputs: { input1: { vars: { foo: null, bar: null }, @@ -576,11 +595,12 @@ describe('Ingest Manager - validationHasErrors()', () => { ).toBe(true); }); - it('returns false for datasource validation results with no errors', () => { + it('returns false for package config validation results with no errors', () => { expect( validationHasErrors({ name: null, description: null, + namespace: null, inputs: { input1: { vars: { foo: null, bar: null }, diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_configure_datasource.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_configure_package.tsx similarity index 67% rename from x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_configure_datasource.tsx rename to x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_configure_package.tsx index c319dbc08b2c6..eecd204a5e307 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_configure_datasource.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_configure_package.tsx @@ -7,11 +7,11 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiPanel, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiCallOut } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { PackageInfo, RegistryStream, NewDatasource, DatasourceInput } from '../../../types'; +import { PackageInfo, RegistryStream, NewPackageConfig, PackageConfigInput } from '../../../types'; import { Loading } from '../../../components'; -import { DatasourceValidationResults, validationHasErrors } from './services'; -import { DatasourceInputPanel, CustomConfigureDatasource } from './components'; -import { CreateDatasourceFrom } from './types'; +import { PackageConfigValidationResults, validationHasErrors } from './services'; +import { PackageConfigInputPanel, CustomPackageConfig } from './components'; +import { CreatePackageConfigFrom } from './types'; const findStreamsForInputType = ( inputType: string, @@ -35,27 +35,27 @@ const findStreamsForInputType = ( return streams; }; -export const StepConfigureDatasource: React.FunctionComponent<{ - from?: CreateDatasourceFrom; +export const StepConfigurePackage: React.FunctionComponent<{ + from?: CreatePackageConfigFrom; packageInfo: PackageInfo; - datasource: NewDatasource; - datasourceId?: string; - updateDatasource: (fields: Partial) => void; - validationResults: DatasourceValidationResults; + packageConfig: NewPackageConfig; + packageConfigId?: string; + updatePackageConfig: (fields: Partial) => void; + validationResults: PackageConfigValidationResults; submitAttempted: boolean; }> = ({ from = 'config', packageInfo, - datasource, - datasourceId, - updateDatasource, + packageConfig, + packageConfigId, + updatePackageConfig, validationResults, submitAttempted, }) => { const hasErrors = validationResults ? validationHasErrors(validationResults) : false; // Configure inputs (and their streams) - // Assume packages only export one datasource for now + // Assume packages only export one config template for now const renderConfigureInputs = () => packageInfo.config_templates && packageInfo.config_templates[0] && @@ -63,30 +63,30 @@ export const StepConfigureDatasource: React.FunctionComponent<{ packageInfo.config_templates[0].inputs.length ? ( {packageInfo.config_templates[0].inputs.map((packageInput) => { - const datasourceInput = datasource.inputs.find( + const packageConfigInput = packageConfig.inputs.find( (input) => input.type === packageInput.type ); const packageInputStreams = findStreamsForInputType(packageInput.type, packageInfo); - return datasourceInput ? ( + return packageConfigInput ? ( - ) => { - const indexOfUpdatedInput = datasource.inputs.findIndex( + packageConfigInput={packageConfigInput} + updatePackageConfigInput={(updatedInput: Partial) => { + const indexOfUpdatedInput = packageConfig.inputs.findIndex( (input) => input.type === packageInput.type ); - const newInputs = [...datasource.inputs]; + const newInputs = [...packageConfig.inputs]; newInputs[indexOfUpdatedInput] = { ...newInputs[indexOfUpdatedInput], ...updatedInput, }; - updateDatasource({ + updatePackageConfig({ inputs: newInputs, }); }} - inputValidationResults={validationResults!.inputs![datasourceInput.type]} + inputValidationResults={validationResults!.inputs![packageConfigInput.type]} forceShowErrors={submitAttempted} /> @@ -95,11 +95,11 @@ export const StepConfigureDatasource: React.FunctionComponent<{ ) : ( - ); @@ -112,16 +112,16 @@ export const StepConfigureDatasource: React.FunctionComponent<{

diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_define_datasource.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_define_package_config.tsx similarity index 60% rename from x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_define_datasource.tsx rename to x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_define_package_config.tsx index 2651615b458f7..b2ffe62104eb1 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_define_datasource.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_define_package_config.tsx @@ -15,37 +15,37 @@ import { EuiText, EuiComboBox, } from '@elastic/eui'; -import { AgentConfig, PackageInfo, Datasource, NewDatasource } from '../../../types'; -import { packageToConfigDatasourceInputs } from '../../../services'; +import { AgentConfig, PackageInfo, PackageConfig, NewPackageConfig } from '../../../types'; +import { packageToPackageConfigInputs } from '../../../services'; import { Loading } from '../../../components'; -import { DatasourceValidationResults } from './services'; +import { PackageConfigValidationResults } from './services'; -export const StepDefineDatasource: React.FunctionComponent<{ +export const StepDefinePackageConfig: React.FunctionComponent<{ agentConfig: AgentConfig; packageInfo: PackageInfo; - datasource: NewDatasource; - updateDatasource: (fields: Partial) => void; - validationResults: DatasourceValidationResults; -}> = ({ agentConfig, packageInfo, datasource, updateDatasource, validationResults }) => { + packageConfig: NewPackageConfig; + updatePackageConfig: (fields: Partial) => void; + validationResults: PackageConfigValidationResults; +}> = ({ agentConfig, packageInfo, packageConfig, updatePackageConfig, validationResults }) => { // Form show/hide states const [isShowingAdvancedDefine, setIsShowingAdvancedDefine] = useState(false); - // Update datasource's package and config info + // Update package config's package and config info useEffect(() => { - const dsPackage = datasource.package; - const currentPkgKey = dsPackage ? `${dsPackage.name}-${dsPackage.version}` : ''; + const pkg = packageConfig.package; + const currentPkgKey = pkg ? `${pkg.name}-${pkg.version}` : ''; const pkgKey = `${packageInfo.name}-${packageInfo.version}`; - // If package has changed, create shell datasource with input&stream values based on package info + // If package has changed, create shell package config with input&stream values based on package info if (currentPkgKey !== pkgKey) { - // Existing datasources on the agent config using the package name, retrieve highest number appended to datasource name + // Existing package configs on the agent config using the package name, retrieve highest number appended to package config name const dsPackageNamePattern = new RegExp(`${packageInfo.name}-(\\d+)`); - const dsWithMatchingNames = (agentConfig.datasources as Datasource[]) + const dsWithMatchingNames = (agentConfig.package_configs as PackageConfig[]) .filter((ds) => Boolean(ds.name.match(dsPackageNamePattern))) .map((ds) => parseInt(ds.name.match(dsPackageNamePattern)![1], 10)) .sort(); - updateDatasource({ + updatePackageConfig({ name: `${packageInfo.name}-${ dsWithMatchingNames.length ? dsWithMatchingNames[dsWithMatchingNames.length - 1] + 1 : 1 }`, @@ -54,18 +54,24 @@ export const StepDefineDatasource: React.FunctionComponent<{ title: packageInfo.title, version: packageInfo.version, }, - inputs: packageToConfigDatasourceInputs(packageInfo), + inputs: packageToPackageConfigInputs(packageInfo), }); } - // If agent config has changed, update datasource's config ID and namespace - if (datasource.config_id !== agentConfig.id) { - updateDatasource({ + // If agent config has changed, update package config's config ID and namespace + if (packageConfig.config_id !== agentConfig.id) { + updatePackageConfig({ config_id: agentConfig.id, namespace: agentConfig.namespace, }); } - }, [datasource.package, datasource.config_id, agentConfig, packageInfo, updateDatasource]); + }, [ + packageConfig.package, + packageConfig.config_id, + agentConfig, + packageInfo, + updatePackageConfig, + ]); return validationResults ? ( <> @@ -76,19 +82,19 @@ export const StepDefineDatasource: React.FunctionComponent<{ error={validationResults.name} label={ } > - updateDatasource({ + updatePackageConfig({ name: e.target.value, }) } - data-test-subj="datasourceNameInput" + data-test-subj="packageConfigNameInput" />
@@ -96,14 +102,14 @@ export const StepDefineDatasource: React.FunctionComponent<{ } labelAppend={ @@ -112,9 +118,9 @@ export const StepDefineDatasource: React.FunctionComponent<{ error={validationResults.description} > - updateDatasource({ + updatePackageConfig({ description: e.target.value, }) } @@ -130,20 +136,22 @@ export const StepDefineDatasource: React.FunctionComponent<{ onClick={() => setIsShowingAdvancedDefine(!isShowingAdvancedDefine)} > {/* Todo: Populate list of existing namespaces */} - {isShowingAdvancedDefine ? ( + {isShowingAdvancedDefine || !!validationResults.namespace ? ( } @@ -151,14 +159,16 @@ export const StepDefineDatasource: React.FunctionComponent<{ { - updateDatasource({ + updatePackageConfig({ namespace: newNamespace, }); }} onChange={(newNamespaces: Array<{ label: string }>) => { - updateDatasource({ + updatePackageConfig({ namespace: newNamespaces.length ? newNamespaces[0].label : '', }); }} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_select_config.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_select_config.tsx similarity index 91% rename from x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_select_config.tsx rename to x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_select_config.tsx index 5f556a46e518d..70668c2856f98 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_select_config.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_select_config.tsx @@ -76,7 +76,7 @@ export const StepSelectConfig: React.FunctionComponent<{ } @@ -91,7 +91,7 @@ export const StepSelectConfig: React.FunctionComponent<{ } @@ -127,7 +127,7 @@ export const StepSelectConfig: React.FunctionComponent<{ } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_select_package.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_select_package.tsx similarity index 92% rename from x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_select_package.tsx rename to x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_select_package.tsx index 12f5bf9eec1d0..e4f4c976688b1 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_select_package.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/step_select_package.tsx @@ -70,7 +70,7 @@ export const StepSelectPackage: React.FunctionComponent<{ } @@ -85,7 +85,7 @@ export const StepSelectPackage: React.FunctionComponent<{ } @@ -124,7 +124,7 @@ export const StepSelectPackage: React.FunctionComponent<{ }} searchProps={{ placeholder: i18n.translate( - 'xpack.ingestManager.createDatasource.stepSelectPackage.filterPackagesInputPlaceholder', + 'xpack.ingestManager.createPackageConfig.stepSelectPackage.filterPackagesInputPlaceholder', { defaultMessage: 'Search for integrations', } @@ -155,7 +155,7 @@ export const StepSelectPackage: React.FunctionComponent<{ } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/types.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/types.ts similarity index 59% rename from x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/types.ts rename to x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/types.ts index 10b30a5696d83..5386ff17fe96b 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/types.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/types.ts @@ -4,5 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export type CreateDatasourceFrom = 'package' | 'config' | 'edit'; -export type DatasourceFormState = 'VALID' | 'INVALID' | 'CONFIRM' | 'LOADING' | 'SUBMITTED'; +export type CreatePackageConfigFrom = 'package' | 'config' | 'edit'; +export type PackageConfigFormState = 'VALID' | 'INVALID' | 'CONFIRM' | 'LOADING' | 'SUBMITTED'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/index.tsx deleted file mode 100644 index 346ccde45f3f0..0000000000000 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/index.tsx +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { memo } from 'react'; -import { AgentConfig, Datasource } from '../../../../../../../../common/types/models'; -import { NoDatasources } from './no_datasources'; -import { DatasourcesTable } from './datasources_table'; - -export const ConfigDatasourcesView = memo<{ config: AgentConfig }>(({ config }) => { - if (config.datasources.length === 0) { - return ; - } - - return ( - - ); -}); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/index.ts index ee2eb9f9dbba6..e53206c68c443 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/index.ts @@ -3,6 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -export { DatasourcesTable } from './datasources/datasources_table'; -export { ConfigDatasourcesView } from './datasources'; +export { PackageConfigsTable } from './package_configs/package_configs_table'; +export { ConfigPackageConfigsView } from './package_configs'; export { ConfigSettingsView } from './settings'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/package_configs/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/package_configs/index.tsx new file mode 100644 index 0000000000000..3aef297e8d22e --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/package_configs/index.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { memo } from 'react'; +import { AgentConfig, PackageConfig } from '../../../../../types'; +import { NoPackageConfigs } from './no_package_configs'; +import { PackageConfigsTable } from './package_configs_table'; + +export const ConfigPackageConfigsView = memo<{ config: AgentConfig }>(({ config }) => { + if (config.package_configs.length === 0) { + return ; + } + + return ( + + ); +}); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/no_datasources.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/package_configs/no_package_configs.tsx similarity index 61% rename from x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/no_datasources.tsx rename to x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/package_configs/no_package_configs.tsx index f2c204d955a0b..ad75c70783698 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/no_datasources.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/package_configs/no_package_configs.tsx @@ -3,13 +3,12 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import React, { memo } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; -import React, { memo } from 'react'; import { useCapabilities, useLink } from '../../../../../hooks'; -export const NoDatasources = memo<{ configId: string }>(({ configId }) => { +export const NoPackageConfigs = memo<{ configId: string }>(({ configId }) => { const { getHref } = useLink(); const hasWriteCapabilities = useCapabilities().write; @@ -19,26 +18,26 @@ export const NoDatasources = memo<{ configId: string }>(({ configId }) => { title={

} body={ } actions={ } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/datasources_table.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/package_configs/package_configs_table.tsx similarity index 55% rename from x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/datasources_table.tsx rename to x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/package_configs/package_configs_table.tsx index caf0c149c0199..19243084f6821 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/datasources_table.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/package_configs/package_configs_table.tsx @@ -16,13 +16,13 @@ import { EuiFlexGroup, EuiFlexItem, } from '@elastic/eui'; -import { AgentConfig, Datasource } from '../../../../../types'; +import { AgentConfig, PackageConfig } from '../../../../../types'; import { PackageIcon, ContextMenuActions } from '../../../../../components'; -import { DatasourceDeleteProvider, DangerEuiContextMenuItem } from '../../../components'; +import { PackageConfigDeleteProvider, DangerEuiContextMenuItem } from '../../../components'; import { useCapabilities, useLink } from '../../../../../hooks'; import { useConfigRefresh } from '../../hooks'; -interface InMemoryDatasource extends Datasource { +interface InMemoryPackageConfig extends PackageConfig { streams: { total: number; enabled: number }; inputTypes: string[]; packageName?: string; @@ -31,11 +31,11 @@ interface InMemoryDatasource extends Datasource { } interface Props { - datasources: Datasource[]; + packageConfigs: PackageConfig[]; config: AgentConfig; // Pass through props to InMemoryTable - loading?: EuiInMemoryTableProps['loading']; - message?: EuiInMemoryTableProps['message']; + loading?: EuiInMemoryTableProps['loading']; + message?: EuiInMemoryTableProps['message']; } interface FilterOption { @@ -46,8 +46,8 @@ interface FilterOption { const stringSortAscending = (a: string, b: string): number => a.localeCompare(b); const toFilterOption = (value: string): FilterOption => ({ name: value, value }); -export const DatasourcesTable: React.FunctionComponent = ({ - datasources: originalDatasources, +export const PackageConfigsTable: React.FunctionComponent = ({ + packageConfigs: originalPackageConfigs, config, ...rest }) => { @@ -55,75 +55,80 @@ export const DatasourcesTable: React.FunctionComponent = ({ const hasWriteCapabilities = useCapabilities().write; const refreshConfig = useConfigRefresh(); - // With the datasources provided on input, generate the list of datasources + // With the package configs provided on input, generate the list of package configs // used in the InMemoryTable (flattens some values for search) as well as // the list of options that will be used in the filters dropdowns - const [datasources, namespaces, inputTypes] = useMemo((): [ - InMemoryDatasource[], + const [packageConfigs, namespaces, inputTypes] = useMemo((): [ + InMemoryPackageConfig[], FilterOption[], FilterOption[] ] => { const namespacesValues: string[] = []; const inputTypesValues: string[] = []; - const mappedDatasources = originalDatasources.map((datasource) => { - if (datasource.namespace && !namespacesValues.includes(datasource.namespace)) { - namespacesValues.push(datasource.namespace); - } + const mappedPackageConfigs = originalPackageConfigs.map( + (packageConfig) => { + if (packageConfig.namespace && !namespacesValues.includes(packageConfig.namespace)) { + namespacesValues.push(packageConfig.namespace); + } - const dsInputTypes: string[] = []; - const streams = datasource.inputs.reduce( - (streamSummary, input) => { - if (!inputTypesValues.includes(input.type)) { - inputTypesValues.push(input.type); - } - if (!dsInputTypes.includes(input.type)) { - dsInputTypes.push(input.type); - } + const dsInputTypes: string[] = []; + const streams = packageConfig.inputs.reduce( + (streamSummary, input) => { + if (!inputTypesValues.includes(input.type)) { + inputTypesValues.push(input.type); + } + if (!dsInputTypes.includes(input.type)) { + dsInputTypes.push(input.type); + } - streamSummary.total += input.streams.length; - streamSummary.enabled += input.enabled - ? input.streams.filter((stream) => stream.enabled).length - : 0; + streamSummary.total += input.streams.length; + streamSummary.enabled += input.enabled + ? input.streams.filter((stream) => stream.enabled).length + : 0; - return streamSummary; - }, - { total: 0, enabled: 0 } - ); + return streamSummary; + }, + { total: 0, enabled: 0 } + ); - dsInputTypes.sort(stringSortAscending); + dsInputTypes.sort(stringSortAscending); - return { - ...datasource, - streams, - inputTypes: dsInputTypes, - packageName: datasource.package?.name ?? '', - packageTitle: datasource.package?.title ?? '', - packageVersion: datasource.package?.version ?? '', - }; - }); + return { + ...packageConfig, + streams, + inputTypes: dsInputTypes, + packageName: packageConfig.package?.name ?? '', + packageTitle: packageConfig.package?.title ?? '', + packageVersion: packageConfig.package?.version ?? '', + }; + } + ); namespacesValues.sort(stringSortAscending); inputTypesValues.sort(stringSortAscending); return [ - mappedDatasources, + mappedPackageConfigs, namespacesValues.map(toFilterOption), inputTypesValues.map(toFilterOption), ]; - }, [originalDatasources]); + }, [originalPackageConfigs]); const columns = useMemo( - (): EuiInMemoryTableProps['columns'] => [ + (): EuiInMemoryTableProps['columns'] => [ { field: 'name', - name: i18n.translate('xpack.ingestManager.configDetails.datasourcesTable.nameColumnTitle', { - defaultMessage: 'Data source', - }), + name: i18n.translate( + 'xpack.ingestManager.configDetails.packageConfigsTable.nameColumnTitle', + { + defaultMessage: 'Name', + } + ), }, { field: 'description', name: i18n.translate( - 'xpack.ingestManager.configDetails.datasourcesTable.descriptionColumnTitle', + 'xpack.ingestManager.configDetails.packageConfigsTable.descriptionColumnTitle', { defaultMessage: 'Description', } @@ -133,19 +138,19 @@ export const DatasourcesTable: React.FunctionComponent = ({ { field: 'packageTitle', name: i18n.translate( - 'xpack.ingestManager.configDetails.datasourcesTable.packageNameColumnTitle', + 'xpack.ingestManager.configDetails.packageConfigsTable.packageNameColumnTitle', { defaultMessage: 'Integration', } ), - render(packageTitle: string, datasource: InMemoryDatasource) { + render(packageTitle: string, packageConfig: InMemoryPackageConfig) { return ( - {datasource.package && ( + {packageConfig.package && ( @@ -159,24 +164,24 @@ export const DatasourcesTable: React.FunctionComponent = ({ { field: 'namespace', name: i18n.translate( - 'xpack.ingestManager.configDetails.datasourcesTable.namespaceColumnTitle', + 'xpack.ingestManager.configDetails.packageConfigsTable.namespaceColumnTitle', { defaultMessage: 'Namespace', } ), - render: (namespace: InMemoryDatasource['namespace']) => { + render: (namespace: InMemoryPackageConfig['namespace']) => { return namespace ? {namespace} : ''; }, }, { field: 'streams', name: i18n.translate( - 'xpack.ingestManager.configDetails.datasourcesTable.streamsCountColumnTitle', + 'xpack.ingestManager.configDetails.packageConfigsTable.streamsCountColumnTitle', { defaultMessage: 'Streams', } ), - render: (streams: InMemoryDatasource['streams']) => { + render: (streams: InMemoryPackageConfig['streams']) => { return ( <> {streams.enabled} @@ -187,67 +192,67 @@ export const DatasourcesTable: React.FunctionComponent = ({ }, { name: i18n.translate( - 'xpack.ingestManager.configDetails.datasourcesTable.actionsColumnTitle', + 'xpack.ingestManager.configDetails.packageConfigsTable.actionsColumnTitle', { defaultMessage: 'Actions', } ), actions: [ { - render: (datasource: InMemoryDatasource) => ( + render: (packageConfig: InMemoryPackageConfig) => ( {}} - // key="datasourceView" + // key="packageConfigView" // > // // , , - // FIXME: implement Copy datasource action - // {}} key="datasourceCopy"> + // FIXME: implement Copy package config action + // {}} key="packageConfigCopy"> // // , - - {(deleteDatasourcePrompt) => { + + {(deletePackageConfigsPrompt) => { return ( { - deleteDatasourcePrompt([datasource.id], refreshConfig); + deletePackageConfigsPrompt([packageConfig.id], refreshConfig); }} > ); }} - , + , ]} /> ), @@ -259,9 +264,9 @@ export const DatasourcesTable: React.FunctionComponent = ({ ); return ( - + itemId="id" - items={datasources} + items={packageConfigs} columns={columns} sorting={{ sort: { @@ -273,14 +278,14 @@ export const DatasourcesTable: React.FunctionComponent = ({ search={{ toolsRight: [ , ], diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/index.tsx index eaa161d57bbe4..4ae16eb91e582 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/index.tsx @@ -28,7 +28,7 @@ import { Loading } from '../../../components'; import { WithHeaderLayout } from '../../../layouts'; import { ConfigRefreshContext, useGetAgentStatus, AgentStatusRefreshContext } from './hooks'; import { LinkedAgentCount, AgentConfigActionMenu } from '../components'; -import { ConfigDatasourcesView, ConfigSettingsView } from './components'; +import { ConfigPackageConfigsView, ConfigSettingsView } from './components'; import { useIntraAppState } from '../../../hooks/use_intra_app_state'; const Divider = styled.div` @@ -120,13 +120,16 @@ export const AgentConfigDetailsPage: React.FunctionComponent = () => { }, { isDivider: true }, { - label: i18n.translate('xpack.ingestManager.configDetails.summary.datasources', { - defaultMessage: 'Data sources', + label: i18n.translate('xpack.ingestManager.configDetails.summary.package_configs', { + defaultMessage: 'Integrations', }), content: ( ), @@ -204,12 +207,12 @@ export const AgentConfigDetailsPage: React.FunctionComponent = () => { const headerTabs = useMemo(() => { return [ { - id: 'datasources', - name: i18n.translate('xpack.ingestManager.configDetails.subTabs.datasourcesTabText', { - defaultMessage: 'Data sources', + id: 'integrations', + name: i18n.translate('xpack.ingestManager.configDetails.subTabs.packageConfigsTabText', { + defaultMessage: 'Integrations', }), - href: getHref('configuration_details', { configId, tabId: 'datasources' }), - isSelected: tabId === '' || tabId === 'datasources', + href: getHref('configuration_details', { configId, tabId: 'integrations' }), + isSelected: tabId === '' || tabId === 'integrations', }, { id: 'settings', @@ -292,7 +295,7 @@ const AgentConfigDetailsContent: React.FunctionComponent<{ agentConfig: AgentCon { - return ; + return ; }} /> diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/edit_datasource_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/edit_package_config_page/index.tsx similarity index 64% rename from x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/edit_datasource_page/index.tsx rename to x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/edit_package_config_page/index.tsx index af39cb87f18c9..7fbcdbb9738cb 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/edit_datasource_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/edit_package_config_page/index.tsx @@ -16,31 +16,34 @@ import { EuiFlexItem, EuiSpacer, } from '@elastic/eui'; -import { AgentConfig, PackageInfo, NewDatasource } from '../../../types'; +import { AgentConfig, PackageInfo, NewPackageConfig } from '../../../types'; import { useLink, useBreadcrumbs, useCore, useConfig, - sendUpdateDatasource, + sendUpdatePackageConfig, sendGetAgentStatus, sendGetOneAgentConfig, - sendGetOneDatasource, + sendGetOnePackageConfig, sendGetPackageInfoByKey, } from '../../../hooks'; import { Loading, Error } from '../../../components'; import { ConfirmDeployConfigModal } from '../components'; -import { CreateDatasourcePageLayout } from '../create_datasource_page/components'; +import { CreatePackageConfigPageLayout } from '../create_package_config_page/components'; import { - DatasourceValidationResults, - validateDatasource, + PackageConfigValidationResults, + validatePackageConfig, validationHasErrors, -} from '../create_datasource_page/services'; -import { DatasourceFormState, CreateDatasourceFrom } from '../create_datasource_page/types'; -import { StepConfigureDatasource } from '../create_datasource_page/step_configure_datasource'; -import { StepDefineDatasource } from '../create_datasource_page/step_define_datasource'; +} from '../create_package_config_page/services'; +import { + PackageConfigFormState, + CreatePackageConfigFrom, +} from '../create_package_config_page/types'; +import { StepConfigurePackage } from '../create_package_config_page/step_configure_package'; +import { StepDefinePackageConfig } from '../create_package_config_page/step_define_package_config'; -export const EditDatasourcePage: React.FunctionComponent = () => { +export const EditPackageConfigPage: React.FunctionComponent = () => { const { notifications, chrome: { getIsNavDrawerLocked$ }, @@ -50,7 +53,7 @@ export const EditDatasourcePage: React.FunctionComponent = () => { fleet: { enabled: isFleetEnabled }, } = useConfig(); const { - params: { configId, datasourceId }, + params: { configId, packageConfigId }, } = useRouteMatch(); const history = useHistory(); const { getHref, getPath } = useLink(); @@ -64,34 +67,35 @@ export const EditDatasourcePage: React.FunctionComponent = () => { return () => subscription.unsubscribe(); }); - // Agent config, package info, and datasource states + // Agent config, package info, and package config states const [isLoadingData, setIsLoadingData] = useState(true); const [loadingError, setLoadingError] = useState(); const [agentConfig, setAgentConfig] = useState(); const [packageInfo, setPackageInfo] = useState(); - const [datasource, setDatasource] = useState({ + const [packageConfig, setPackageConfig] = useState({ name: '', description: '', + namespace: '', config_id: '', enabled: true, output_id: '', inputs: [], }); - // Retrieve agent config, package, and datasource info + // Retrieve agent config, package, and package config info useEffect(() => { const getData = async () => { setIsLoadingData(true); setLoadingError(undefined); try { - const [{ data: agentConfigData }, { data: datasourceData }] = await Promise.all([ + const [{ data: agentConfigData }, { data: packageConfigData }] = await Promise.all([ sendGetOneAgentConfig(configId), - sendGetOneDatasource(datasourceId), + sendGetOnePackageConfig(packageConfigId), ]); if (agentConfigData?.item) { setAgentConfig(agentConfigData.item); } - if (datasourceData?.item) { + if (packageConfigData?.item) { const { id, revision, @@ -100,30 +104,30 @@ export const EditDatasourcePage: React.FunctionComponent = () => { created_at, updated_by, updated_at, - ...restOfDatasource - } = datasourceData.item; - // Remove `agent_stream` from all stream info, we assign this after saving - const newDatasource = { - ...restOfDatasource, + ...restOfPackageConfig + } = packageConfigData.item; + // Remove `compiled_stream` from all stream info, we assign this after saving + const newPackageConfig = { + ...restOfPackageConfig, inputs: inputs.map((input) => { const { streams, ...restOfInput } = input; return { ...restOfInput, streams: streams.map((stream) => { - const { agent_stream, ...restOfStream } = stream; + const { compiled_stream, ...restOfStream } = stream; return restOfStream; }), }; }), }; - setDatasource(newDatasource); - if (datasourceData.item.package) { + setPackageConfig(newPackageConfig); + if (packageConfigData.item.package) { const { data: packageData } = await sendGetPackageInfoByKey( - `${datasourceData.item.package.name}-${datasourceData.item.package.version}` + `${packageConfigData.item.package.name}-${packageConfigData.item.package.version}` ); if (packageData?.response) { setPackageInfo(packageData.response); - setValidationResults(validateDatasource(newDatasource, packageData.response)); + setValidationResults(validatePackageConfig(newPackageConfig, packageData.response)); setFormState('VALID'); } } @@ -134,7 +138,7 @@ export const EditDatasourcePage: React.FunctionComponent = () => { setIsLoadingData(false); }; getData(); - }, [configId, datasourceId]); + }, [configId, packageConfigId]); // Retrieve agent count const [agentCount, setAgentCount] = useState(0); @@ -151,21 +155,21 @@ export const EditDatasourcePage: React.FunctionComponent = () => { } }, [configId, isFleetEnabled]); - // Datasource validation state - const [validationResults, setValidationResults] = useState(); + // Package config validation state + const [validationResults, setValidationResults] = useState(); const hasErrors = validationResults ? validationHasErrors(validationResults) : false; - // Update datasource method - const updateDatasource = (updatedFields: Partial) => { - const newDatasource = { - ...datasource, + // Update package config method + const updatePackageConfig = (updatedFields: Partial) => { + const newPackageConfig = { + ...packageConfig, ...updatedFields, }; - setDatasource(newDatasource); + setPackageConfig(newPackageConfig); // eslint-disable-next-line no-console - console.debug('Datasource updated', newDatasource); - const newValidationResults = updateDatasourceValidation(newDatasource); + console.debug('Package config updated', newPackageConfig); + const newValidationResults = updatePackageConfigValidation(newPackageConfig); const hasValidationErrors = newValidationResults ? validationHasErrors(newValidationResults) : false; @@ -174,12 +178,15 @@ export const EditDatasourcePage: React.FunctionComponent = () => { } }; - const updateDatasourceValidation = (newDatasource?: NewDatasource) => { + const updatePackageConfigValidation = (newPackageConfig?: NewPackageConfig) => { if (packageInfo) { - const newValidationResult = validateDatasource(newDatasource || datasource, packageInfo); + const newValidationResult = validatePackageConfig( + newPackageConfig || packageConfig, + packageInfo + ); setValidationResults(newValidationResult); // eslint-disable-next-line no-console - console.debug('Datasource validation results', newValidationResult); + console.debug('Package config validation results', newValidationResult); return newValidationResult; } @@ -188,11 +195,11 @@ export const EditDatasourcePage: React.FunctionComponent = () => { // Cancel url const cancelUrl = getHref('configuration_details', { configId }); - // Save datasource - const [formState, setFormState] = useState('INVALID'); - const saveDatasource = async () => { + // Save package config + const [formState, setFormState] = useState('INVALID'); + const savePackageConfig = async () => { setFormState('LOADING'); - const result = await sendUpdateDatasource(datasourceId, datasource); + const result = await sendUpdatePackageConfig(packageConfigId, packageConfig); setFormState('SUBMITTED'); return result; }; @@ -206,19 +213,19 @@ export const EditDatasourcePage: React.FunctionComponent = () => { setFormState('CONFIRM'); return; } - const { error } = await saveDatasource(); + const { error } = await savePackageConfig(); if (!error) { history.push(getPath('configuration_details', { configId })); notifications.toasts.addSuccess({ - title: i18n.translate('xpack.ingestManager.editDatasource.updatedNotificationTitle', { - defaultMessage: `Successfully updated '{datasourceName}'`, + title: i18n.translate('xpack.ingestManager.editPackageConfig.updatedNotificationTitle', { + defaultMessage: `Successfully updated '{packageConfigName}'`, values: { - datasourceName: datasource.name, + packageConfigName: packageConfig.name, }, }), text: agentCount && agentConfig - ? i18n.translate('xpack.ingestManager.editDatasource.updatedNotificationMessage', { + ? i18n.translate('xpack.ingestManager.editPackageConfig.updatedNotificationMessage', { defaultMessage: `Fleet will deploy updates to all agents that use the '{agentConfigName}' configuration`, values: { agentConfigName: agentConfig.name, @@ -235,28 +242,28 @@ export const EditDatasourcePage: React.FunctionComponent = () => { }; const layoutProps = { - from: 'edit' as CreateDatasourceFrom, + from: 'edit' as CreatePackageConfigFrom, cancelUrl, agentConfig, packageInfo, }; return ( - + {isLoadingData ? ( ) : loadingError || !agentConfig || !packageInfo ? ( } error={ loadingError || - i18n.translate('xpack.ingestManager.editDatasource.errorLoadingDataMessage', { - defaultMessage: 'There was an error loading this data source information', + i18n.translate('xpack.ingestManager.editPackageConfig.errorLoadingDataMessage', { + defaultMessage: 'There was an error loading this intergration information', }) } /> @@ -275,35 +282,35 @@ export const EditDatasourcePage: React.FunctionComponent = () => { steps={[ { title: i18n.translate( - 'xpack.ingestManager.editDatasource.stepDefineDatasourceTitle', + 'xpack.ingestManager.editPackageConfig.stepDefinePackageConfigTitle', { - defaultMessage: 'Define your data source', + defaultMessage: 'Define your integration', } ), children: ( - ), }, { title: i18n.translate( - 'xpack.ingestManager.editDatasource.stepConfgiureDatasourceTitle', + 'xpack.ingestManager.editPackageConfig.stepConfigurePackageConfigTitle', { defaultMessage: 'Select the data you want to collect', } ), children: ( - @@ -326,7 +333,7 @@ export const EditDatasourcePage: React.FunctionComponent = () => { @@ -341,8 +348,8 @@ export const EditDatasourcePage: React.FunctionComponent = () => { fill > @@ -350,7 +357,7 @@ export const EditDatasourcePage: React.FunctionComponent = () => { )} - + ); }; @@ -358,6 +365,6 @@ const Breadcrumb: React.FunctionComponent<{ configName: string; configId: string configName, configId, }) => { - useBreadcrumbs('edit_datasource', { configName, configId }); + useBreadcrumbs('edit_integration', { configName, configId }); return null; }; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/index.tsx index 74fa67078f741..727ef23347251 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/index.tsx @@ -9,8 +9,8 @@ import { PAGE_ROUTING_PATHS } from '../../constants'; import { useBreadcrumbs } from '../../hooks'; import { AgentConfigListPage } from './list_page'; import { AgentConfigDetailsPage } from './details_page'; -import { CreateDatasourcePage } from './create_datasource_page'; -import { EditDatasourcePage } from './edit_datasource_page'; +import { CreatePackageConfigPage } from './create_package_config_page'; +import { EditPackageConfigPage } from './edit_package_config_page'; export const AgentConfigApp: React.FunctionComponent = () => { useBreadcrumbs('configurations'); @@ -18,11 +18,11 @@ export const AgentConfigApp: React.FunctionComponent = () => { return ( - - + + - - + + diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/components/create_config.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/components/create_config.tsx index f746fadc4b0a3..d1abd88adba86 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/components/create_config.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/components/create_config.tsx @@ -64,7 +64,7 @@ export const CreateAgentConfigFlyout: React.FunctionComponent = ({ onClos diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx index 8b1ff0988d443..0a9daf0038aab 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx @@ -176,12 +176,13 @@ export const AgentConfigListPage: React.FunctionComponent<{}> = () => { ), }, { - field: 'datasources', - name: i18n.translate('xpack.ingestManager.agentConfigList.datasourcesCountColumnTitle', { - defaultMessage: 'Data sources', + field: 'package_configs', + name: i18n.translate('xpack.ingestManager.agentConfigList.packageConfigsCountColumnTitle', { + defaultMessage: 'Integrations', }), dataType: 'number', - render: (datasources: AgentConfig['datasources']) => (datasources ? datasources.length : 0), + render: (packageConfigs: AgentConfig['package_configs']) => + packageConfigs ? packageConfigs.length : 0, }, { name: i18n.translate('xpack.ingestManager.agentConfigList.actionsColumnTitle', { diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/index.tsx index ca1a8df534044..cb0664143bb34 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/index.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { HashRouter as Router, Switch, Route } from 'react-router-dom'; import { PAGE_ROUTING_PATHS } from '../../constants'; import { useConfig, useBreadcrumbs } from '../../hooks'; -import { CreateDatasourcePage } from '../agent_config/create_datasource_page'; +import { CreatePackageConfigPage } from '../agent_config/create_package_config_page'; import { EPMHomePage } from './screens/home'; import { Detail } from './screens/detail'; @@ -19,8 +19,8 @@ export const EPMApp: React.FunctionComponent = () => { return epm.enabled ? ( - - + + diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/content.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/content.tsx index 4c14e2556de09..c9a8cabdf414b 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/content.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/content.tsx @@ -13,7 +13,7 @@ import { AssetsFacetGroup } from '../../components/assets_facet_group'; import { CenterColumn, LeftColumn, RightColumn } from './layout'; import { OverviewPanel } from './overview_panel'; import { SideNavLinks } from './side_nav_links'; -import { DataSourcesPanel } from './data_sources_panel'; +import { PackageConfigsPanel } from './package_configs_panel'; import { SettingsPanel } from './settings_panel'; type ContentProps = PackageInfo & Pick & { hasIconPanel: boolean }; @@ -62,8 +62,8 @@ export function ContentPanel(props: ContentPanelProps) { latestVersion={latestVersion} /> ); - case 'data-sources': - return ; + case 'usages': + return ; case 'overview': default: return ; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/header.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/header.tsx index db046d18ccebc..875a8f5c5c127 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/header.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/header.tsx @@ -70,10 +70,10 @@ export function Header(props: HeaderProps) { { +export const PackageConfigsPanel = ({ name, version }: PackageConfigsPanelProps) => { const { getPath } = useLink(); const getPackageInstallStatus = useGetPackageInstallStatus(); const packageInstallStatus = getPackageInstallStatus(name); @@ -23,11 +22,5 @@ export const DataSourcesPanel = ({ name, version }: DataSourcesPanelProps) => { // this happens if they arrive with a direct url or they uninstall while on this tab if (packageInstallStatus.status !== InstallStatus.installed) return ; - return ( - - - Data Sources - - - ); + return null; }; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/settings_panel.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/settings_panel.tsx index 986b946131e33..125289ce3ee8d 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/settings_panel.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/settings_panel.tsx @@ -10,8 +10,8 @@ import { EuiTitle, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; import { EuiSpacer } from '@elastic/eui'; import styled from 'styled-components'; import { InstallStatus, PackageInfo } from '../../../../types'; -import { useGetDatasources } from '../../../../hooks'; -import { DATASOURCE_SAVED_OBJECT_TYPE } from '../../../../constants'; +import { useGetPackageConfigs } from '../../../../hooks'; +import { PACKAGE_CONFIG_SAVED_OBJECT_TYPE } from '../../../../constants'; import { useGetPackageInstallStatus } from '../../hooks'; import { InstallationButton } from './installation_button'; import { UpdateIcon } from '../../components/icons'; @@ -46,13 +46,13 @@ export const SettingsPanel = ( ) => { const { name, title, removable, latestVersion, version } = props; const getPackageInstallStatus = useGetPackageInstallStatus(); - const { data: datasourcesData } = useGetDatasources({ + const { data: packageConfigsData } = useGetPackageConfigs({ perPage: 0, page: 1, - kuery: `${DATASOURCE_SAVED_OBJECT_TYPE}.package.name:${props.name}`, + kuery: `${PACKAGE_CONFIG_SAVED_OBJECT_TYPE}.package.name:${props.name}`, }); const { status: installationStatus, version: installedVersion } = getPackageInstallStatus(name); - const packageHasDatasources = !!datasourcesData?.total; + const packageHasUsages = !!packageConfigsData?.total; const updateAvailable = installedVersion && installedVersion < latestVersion ? true : false; const isViewingOldPackage = version < latestVersion; // hide install/remove options if the user has version of the package is installed @@ -185,16 +185,16 @@ export const SettingsPanel = (

- {packageHasDatasources && removable === true && ( + {packageHasUsages && removable === true && (

= { overview: i18n.translate('xpack.ingestManager.epm.packageDetailsNav.overviewLinkText', { defaultMessage: 'Overview', }), - 'data-sources': i18n.translate('xpack.ingestManager.epm.packageDetailsNav.datasourcesLinkText', { - defaultMessage: 'Data sources', + usages: i18n.translate('xpack.ingestManager.epm.packageDetailsNav.packageConfigsLinkText', { + defaultMessage: 'Usages', }), settings: i18n.translate('xpack.ingestManager.epm.packageDetailsNav.settingsLinkText', { defaultMessage: 'Settings', @@ -43,12 +43,9 @@ export function SideNavLinks({ name, version, active }: NavLinkProps) { ? p.theme.eui.euiFontWeightSemiBold : p.theme.eui.euiFontWeightRegular}; `; - // Don't display Data Sources tab as we haven't implemented this yet - // FIXME: Restore when we implement data sources page - if ( - panel === 'data-sources' && - (true || packageInstallStatus.status !== InstallStatus.installed) - ) + // Don't display usages tab as we haven't implemented this yet + // FIXME: Restore when we implement usages page + if (panel === 'usages' && (true || packageInstallStatus.status !== InstallStatus.installed)) return null; return ( diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_config_datasource_badges.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_config_package_badges.tsx similarity index 68% rename from x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_config_datasource_badges.tsx rename to x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_config_package_badges.tsx index 30bc9dc701427..fcdb5ff02e7a4 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_config_datasource_badges.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_config_package_badges.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiSpacer, EuiText, EuiFlexGroup, EuiFlexItem, EuiBadge } from '@elastic/eui'; -import { Datasource } from '../../../types'; +import { PackageConfig } from '../../../types'; import { useGetOneAgentConfig } from '../../../hooks'; import { PackageIcon } from '../../../components/package_icon'; @@ -14,7 +14,7 @@ interface Props { agentConfigId: string; } -export const AgentConfigDatasourceBadges: React.FunctionComponent = ({ agentConfigId }) => { +export const AgentConfigPackageBadges: React.FunctionComponent = ({ agentConfigId }) => { const agentConfigRequest = useGetOneAgentConfig(agentConfigId); const agentConfig = agentConfigRequest.data ? agentConfigRequest.data.item : null; @@ -26,16 +26,16 @@ export const AgentConfigDatasourceBadges: React.FunctionComponent = ({ ag {agentConfig.datasources.length}, + count: agentConfig.package_configs.length, + countValue: {agentConfig.package_configs.length}, }} /> - {(agentConfig.datasources as Datasource[]).map((datasource, idx) => { - if (!datasource.package) { + {(agentConfig.package_configs as PackageConfig[]).map((packageConfig, idx) => { + if (!packageConfig.package) { return null; } return ( @@ -43,13 +43,13 @@ export const AgentConfigDatasourceBadges: React.FunctionComponent = ({ ag - {datasource.package.title} + {packageConfig.package.title} ); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/config_selection.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/config_selection.tsx index 6e7427c6ab55e..8cd337586d1bc 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/config_selection.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/config_selection.tsx @@ -10,7 +10,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiSelect, EuiSpacer, EuiText, EuiButtonEmpty } from '@elastic/eui'; import { AgentConfig } from '../../../../types'; import { useGetEnrollmentAPIKeys } from '../../../../hooks'; -import { AgentConfigDatasourceBadges } from '../agent_config_datasource_badges'; +import { AgentConfigPackageBadges } from '../agent_config_package_badges'; interface Props { agentConfigs: AgentConfig[]; @@ -83,7 +83,7 @@ export const EnrollmentStepAgentConfig: React.FC = ({ agentConfigs, onKey /> {selectedState.agentConfigId && ( - + )} void; @@ -113,7 +113,7 @@ export const AgentReassignConfigFlyout: React.FunctionComponent = ({ onCl {selectedAgentConfigId && ( - + )} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/configuration_section.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/configuration_section.tsx index 68364f9acbbf9..ed4b3fc8e6a5d 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/configuration_section.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/configuration_section.tsx @@ -15,7 +15,7 @@ import { } from '@elastic/eui'; import { OverviewPanel } from './overview_panel'; import { OverviewStats } from './overview_stats'; -import { useLink, useGetDatasources } from '../../../hooks'; +import { useLink, useGetPackageConfigs } from '../../../hooks'; import { AgentConfig } from '../../../types'; import { Loading } from '../../fleet/components'; @@ -23,7 +23,7 @@ export const OverviewConfigurationSection: React.FC<{ agentConfigs: AgentConfig[ agentConfigs, }) => { const { getHref } = useLink(); - const datasourcesRequest = useGetDatasources({ + const packageConfigsRequest = useGetPackageConfigs({ page: 1, perPage: 10000, }); @@ -48,7 +48,7 @@ export const OverviewConfigurationSection: React.FC<{ agentConfigs: AgentConfig[ - {datasourcesRequest.isLoading ? ( + {packageConfigsRequest.isLoading ? ( ) : ( <> @@ -63,12 +63,12 @@ export const OverviewConfigurationSection: React.FC<{ agentConfigs: AgentConfig[ - + )} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/services/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/services/index.ts index ece7aef2c247f..5dc9026aebdee 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/services/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/services/index.ts @@ -8,7 +8,7 @@ export { getFlattenedObject } from '../../../../../../../src/core/public'; export { agentConfigRouteService, - datasourceRouteService, + packageConfigRouteService, dataStreamRouteService, fleetSetupRouteService, agentRouteService, @@ -18,8 +18,8 @@ export { outputRoutesService, settingsRoutesService, appRoutesService, - packageToConfigDatasourceInputs, - storedDatasourcesToAgentInputs, + packageToPackageConfigInputs, + storedPackageConfigsToAgentInputs, configToYaml, AgentStatusKueryHelper, } from '../../../../common'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts index 90315f38fd476..43ec2f6d1a74d 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts @@ -13,11 +13,11 @@ export { NewAgentConfig, AgentEvent, EnrollmentAPIKey, - Datasource, - NewDatasource, - DatasourceInput, - DatasourceInputStream, - DatasourceConfigRecordEntry, + PackageConfig, + NewPackageConfig, + PackageConfigInput, + PackageConfigInputStream, + PackageConfigConfigRecordEntry, Output, DataStream, // API schema - misc setup, status @@ -35,11 +35,11 @@ export { CopyAgentConfigResponse, DeleteAgentConfigRequest, DeleteAgentConfigResponse, - // API schemas - Datasource - CreateDatasourceRequest, - CreateDatasourceResponse, - UpdateDatasourceRequest, - UpdateDatasourceResponse, + // API schemas - Package config + CreatePackageConfigRequest, + CreatePackageConfigResponse, + UpdatePackageConfigRequest, + UpdatePackageConfigResponse, // API schemas - Data Streams GetDataStreamsResponse, // API schemas - Agents diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/intra_app_route_state.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/intra_app_route_state.ts index c5833adcded5f..4fd770501ae3f 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/intra_app_route_state.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/intra_app_route_state.ts @@ -5,16 +5,16 @@ */ import { ApplicationStart } from 'kibana/public'; -import { Datasource } from '../../../../common/types/models'; +import { PackageConfig } from './'; /** - * Supported routing state for the create datasource page routes + * Supported routing state for the create package config page routes */ -export interface CreateDatasourceRouteState { - /** On a successful save of the datasource, use navigate to the given app */ +export interface CreatePackageConfigRouteState { + /** On a successful save of the package config, use navigate to the given app */ onSaveNavigateTo?: | Parameters - | ((newDatasource: Datasource) => Parameters); + | ((newPackageConfig: PackageConfig) => Parameters); /** On cancel, navigate to the given app */ onCancelNavigateTo?: Parameters; /** Url to be used on cancel links */ @@ -41,6 +41,6 @@ export interface AgentDetailsReassignConfigAction { * All possible Route states. */ export type AnyIntraAppRouteState = - | CreateDatasourceRouteState + | CreatePackageConfigRouteState | AgentConfigDetailsDeployAgentAction | AgentDetailsReassignConfigAction; diff --git a/x-pack/plugins/ingest_manager/public/index.ts b/x-pack/plugins/ingest_manager/public/index.ts index ac56349b30c13..866d17145b075 100644 --- a/x-pack/plugins/ingest_manager/public/index.ts +++ b/x-pack/plugins/ingest_manager/public/index.ts @@ -13,10 +13,10 @@ export const plugin = (initializerContext: PluginInitializerContext) => { }; export { - CustomConfigureDatasourceContent, - CustomConfigureDatasourceProps, - registerDatasource, -} from './applications/ingest_manager/sections/agent_config/create_datasource_page/components/custom_configure_datasource'; + CustomConfigurePackageConfigContent, + CustomConfigurePackageConfigProps, + registerPackageConfigComponent, +} from './applications/ingest_manager/sections/agent_config/create_package_config_page/components/custom_package_config'; -export { NewDatasource } from './applications/ingest_manager/types'; +export { NewPackageConfig } from './applications/ingest_manager/types'; export * from './applications/ingest_manager/types/intra_app_route_state'; diff --git a/x-pack/plugins/ingest_manager/public/plugin.ts b/x-pack/plugins/ingest_manager/public/plugin.ts index 4a10a26151e78..69dd5e42a0bc5 100644 --- a/x-pack/plugins/ingest_manager/public/plugin.ts +++ b/x-pack/plugins/ingest_manager/public/plugin.ts @@ -18,7 +18,7 @@ import { PLUGIN_ID, CheckPermissionsResponse, PostIngestSetupResponse } from '.. import { IngestManagerConfigType } from '../common/types'; import { setupRouteService, appRoutesService } from '../common'; -import { registerDatasource } from './applications/ingest_manager/sections/agent_config/create_datasource_page/components/custom_configure_datasource'; +import { registerPackageConfigComponent } from './applications/ingest_manager/sections/agent_config/create_package_config_page/components/custom_package_config'; export { IngestManagerConfigType } from '../common/types'; @@ -31,7 +31,7 @@ export interface IngestManagerSetup {} * Describes public IngestManager plugin contract returned at the `start` stage. */ export interface IngestManagerStart { - registerDatasource: typeof registerDatasource; + registerPackageConfigComponent: typeof registerPackageConfigComponent; success: Promise; } @@ -102,7 +102,7 @@ export class IngestManagerPlugin return { success: successPromise, - registerDatasource, + registerPackageConfigComponent, }; } diff --git a/x-pack/plugins/ingest_manager/server/constants/index.ts b/x-pack/plugins/ingest_manager/server/constants/index.ts index ebcce6320ec4b..650211ce9c1b2 100644 --- a/x-pack/plugins/ingest_manager/server/constants/index.ts +++ b/x-pack/plugins/ingest_manager/server/constants/index.ts @@ -16,7 +16,7 @@ export { PLUGIN_ID, EPM_API_ROUTES, DATA_STREAM_API_ROUTES, - DATASOURCE_API_ROUTES, + PACKAGE_CONFIG_API_ROUTES, AGENT_API_ROUTES, AGENT_CONFIG_API_ROUTES, FLEET_SETUP_API_ROUTES, @@ -31,7 +31,7 @@ export { AGENT_EVENT_SAVED_OBJECT_TYPE, AGENT_ACTION_SAVED_OBJECT_TYPE, AGENT_CONFIG_SAVED_OBJECT_TYPE, - DATASOURCE_SAVED_OBJECT_TYPE, + PACKAGE_CONFIG_SAVED_OBJECT_TYPE, OUTPUT_SAVED_OBJECT_TYPE, PACKAGES_SAVED_OBJECT_TYPE, INDEX_PATTERN_SAVED_OBJECT_TYPE, diff --git a/x-pack/plugins/ingest_manager/server/index.ts b/x-pack/plugins/ingest_manager/server/index.ts index 1e9011c9dfe4f..5d6a1ad321b6d 100644 --- a/x-pack/plugins/ingest_manager/server/index.ts +++ b/x-pack/plugins/ingest_manager/server/index.ts @@ -43,7 +43,7 @@ export const config = { export type IngestManagerConfigType = TypeOf; -export { DatasourceServiceInterface } from './services/datasource'; +export { PackageConfigServiceInterface } from './services/package_config'; export const plugin = (initializerContext: PluginInitializerContext) => { return new IngestManagerPlugin(initializerContext); diff --git a/x-pack/plugins/ingest_manager/server/integration_tests/router.test.ts b/x-pack/plugins/ingest_manager/server/integration_tests/router.test.ts index bfd8428222643..9d671c629ef91 100644 --- a/x-pack/plugins/ingest_manager/server/integration_tests/router.test.ts +++ b/x-pack/plugins/ingest_manager/server/integration_tests/router.test.ts @@ -46,8 +46,8 @@ describe('ingestManager', () => { await kbnTestServer.request.get(root, '/api/ingest_manager/agent_configs').expect(404); }); - it('does not have datasources api', async () => { - await kbnTestServer.request.get(root, '/api/ingest_manager/datasources').expect(404); + it('does not have package configs api', async () => { + await kbnTestServer.request.get(root, '/api/ingest_manager/package_configs').expect(404); }); it('does not have EPM api', async () => { @@ -79,8 +79,8 @@ describe('ingestManager', () => { await kbnTestServer.request.get(root, '/api/ingest_manager/agent_configs').expect(200); }); - it('has datasources api', async () => { - await kbnTestServer.request.get(root, '/api/ingest_manager/datasources').expect(200); + it('has package configs api', async () => { + await kbnTestServer.request.get(root, '/api/ingest_manager/package_configs').expect(200); }); it('does not have EPM api', async () => { @@ -92,7 +92,7 @@ describe('ingestManager', () => { }); }); - // For now, only the manager routes (/agent_configs & /datasources) are added + // For now, only the manager routes (/agent_configs & /package_configs) are added // EPM and ingest will be conditionally added when we enable these lines // https://github.com/jfsiii/kibana/blob/f73b54ebb7e0f6fc00efd8a6800a01eb2d9fb772/x-pack/plugins/ingest_manager/server/plugin.ts#L84 // adding tests to confirm the Fleet & EPM routes are never added @@ -118,8 +118,8 @@ describe('ingestManager', () => { await kbnTestServer.request.get(root, '/api/ingest_manager/agent_configs').expect(200); }); - it('has datasources api', async () => { - await kbnTestServer.request.get(root, '/api/ingest_manager/datasources').expect(200); + it('has package configs api', async () => { + await kbnTestServer.request.get(root, '/api/ingest_manager/package_configs').expect(200); }); it('does have EPM api', async () => { @@ -152,8 +152,8 @@ describe('ingestManager', () => { await kbnTestServer.request.get(root, '/api/ingest_manager/agent_configs').expect(200); }); - it('has datasources api', async () => { - await kbnTestServer.request.get(root, '/api/ingest_manager/datasources').expect(200); + it('has package configs api', async () => { + await kbnTestServer.request.get(root, '/api/ingest_manager/package_configs').expect(200); }); it('does not have EPM api', async () => { @@ -187,8 +187,8 @@ describe('ingestManager', () => { await kbnTestServer.request.get(root, '/api/ingest_manager/agent_configs').expect(200); }); - it('has datasources api', async () => { - await kbnTestServer.request.get(root, '/api/ingest_manager/datasources').expect(200); + it('has package configs api', async () => { + await kbnTestServer.request.get(root, '/api/ingest_manager/package_configs').expect(200); }); it('does have EPM api', async () => { diff --git a/x-pack/plugins/ingest_manager/server/mocks.ts b/x-pack/plugins/ingest_manager/server/mocks.ts index 3bdef14dc85a0..f305d9dd0c1a7 100644 --- a/x-pack/plugins/ingest_manager/server/mocks.ts +++ b/x-pack/plugins/ingest_manager/server/mocks.ts @@ -8,7 +8,7 @@ import { loggingSystemMock, savedObjectsServiceMock } from 'src/core/server/mock import { IngestManagerAppContext } from './plugin'; import { encryptedSavedObjectsMock } from '../../encrypted_saved_objects/server/mocks'; import { securityMock } from '../../security/server/mocks'; -import { DatasourceServiceInterface } from './services/datasource'; +import { PackageConfigServiceInterface } from './services/package_config'; export const createAppContextStartContractMock = (): IngestManagerAppContext => { return { @@ -21,10 +21,10 @@ export const createAppContextStartContractMock = (): IngestManagerAppContext => }; }; -export const createDatasourceServiceMock = () => { +export const createPackageConfigServiceMock = () => { return { assignPackageStream: jest.fn(), - buildDatasourceFromPackage: jest.fn(), + buildPackageConfigFromPackage: jest.fn(), bulkCreate: jest.fn(), create: jest.fn(), delete: jest.fn(), @@ -32,5 +32,5 @@ export const createDatasourceServiceMock = () => { getByIDs: jest.fn(), list: jest.fn(), update: jest.fn(), - } as jest.Mocked; + } as jest.Mocked; }; diff --git a/x-pack/plugins/ingest_manager/server/plugin.ts b/x-pack/plugins/ingest_manager/server/plugin.ts index 1ae9528f34410..91201dbf9848b 100644 --- a/x-pack/plugins/ingest_manager/server/plugin.ts +++ b/x-pack/plugins/ingest_manager/server/plugin.ts @@ -25,7 +25,7 @@ import { PLUGIN_ID, OUTPUT_SAVED_OBJECT_TYPE, AGENT_CONFIG_SAVED_OBJECT_TYPE, - DATASOURCE_SAVED_OBJECT_TYPE, + PACKAGE_CONFIG_SAVED_OBJECT_TYPE, PACKAGES_SAVED_OBJECT_TYPE, AGENT_SAVED_OBJECT_TYPE, AGENT_EVENT_SAVED_OBJECT_TYPE, @@ -34,7 +34,7 @@ import { import { registerSavedObjects, registerEncryptedSavedObjects } from './saved_objects'; import { registerEPMRoutes, - registerDatasourceRoutes, + registerPackageConfigRoutes, registerDataStreamRoutes, registerAgentConfigRoutes, registerSetupRoutes, @@ -45,14 +45,14 @@ import { registerSettingsRoutes, registerAppRoutes, } from './routes'; -import { IngestManagerConfigType, NewDatasource } from '../common'; +import { IngestManagerConfigType, NewPackageConfig } from '../common'; import { appContextService, licenseService, ESIndexPatternSavedObjectService, ESIndexPatternService, AgentService, - datasourceService, + packageConfigService, } from './services'; import { getAgentStatusById, @@ -91,7 +91,7 @@ export type IngestManagerSetupContract = void; const allSavedObjectTypes = [ OUTPUT_SAVED_OBJECT_TYPE, AGENT_CONFIG_SAVED_OBJECT_TYPE, - DATASOURCE_SAVED_OBJECT_TYPE, + PACKAGE_CONFIG_SAVED_OBJECT_TYPE, PACKAGES_SAVED_OBJECT_TYPE, AGENT_SAVED_OBJECT_TYPE, AGENT_EVENT_SAVED_OBJECT_TYPE, @@ -102,8 +102,8 @@ const allSavedObjectTypes = [ * Callbacks supported by the Ingest plugin */ export type ExternalCallback = [ - 'datasourceCreate', - (newDatasource: NewDatasource) => Promise + 'packageConfigCreate', + (newPackageConfig: NewPackageConfig) => Promise ]; export type ExternalCallbacksStorage = Map>; @@ -115,9 +115,9 @@ export interface IngestManagerStartContract { esIndexPatternService: ESIndexPatternService; agentService: AgentService; /** - * Services for Ingest's Datasources + * Services for Ingest's package configs */ - datasourceService: typeof datasourceService; + packageConfigService: typeof packageConfigService; /** * Register callbacks for inclusion in ingest API processing * @param args @@ -205,7 +205,7 @@ export class IngestManagerPlugin if (this.security) { registerSetupRoutes(router, config); registerAgentConfigRoutes(router); - registerDatasourceRoutes(router); + registerPackageConfigRoutes(router); registerOutputRoutes(router); registerSettingsRoutes(router); registerDataStreamRoutes(router); @@ -265,7 +265,7 @@ export class IngestManagerPlugin getAgentStatusById, authenticateAgentWithAccessToken, }, - datasourceService, + packageConfigService, registerExternalCallback: (...args: ExternalCallback) => { return appContextService.addExternalCallback(...args); }, diff --git a/x-pack/plugins/ingest_manager/server/routes/agent_config/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/agent_config/handlers.ts index d01b361bd6ca4..7b12a076ff041 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent_config/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent_config/handlers.ts @@ -7,7 +7,7 @@ import { TypeOf } from '@kbn/config-schema'; import { RequestHandler, ResponseHeaders } from 'src/core/server'; import bluebird from 'bluebird'; import { configToYaml } from '../../../common/services'; -import { appContextService, agentConfigService, datasourceService } from '../../services'; +import { appContextService, agentConfigService, packageConfigService } from '../../services'; import { listAgents } from '../../services/agents'; import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; import { @@ -20,7 +20,7 @@ import { GetFullAgentConfigRequestSchema, AgentConfig, DefaultPackages, - NewDatasource, + NewPackageConfig, } from '../../types'; import { GetAgentConfigsResponse, @@ -107,30 +107,34 @@ export const createAgentConfigHandler: RequestHandler< const withSysMonitoring = request.query.sys_monitoring ?? false; try { // eslint-disable-next-line prefer-const - let [agentConfig, newSysDatasource] = await Promise.all( - [ - agentConfigService.create(soClient, request.body, { - user, - }), - // If needed, retrieve System package information and build a new Datasource for the system package - // NOTE: we ignore failures in attempting to create datasource, since config might have been created - // successfully - withSysMonitoring - ? datasourceService - .buildDatasourceFromPackage(soClient, DefaultPackages.system) - .catch(() => undefined) - : undefined, - ] - ); + let [agentConfig, newSysPackageConfig] = await Promise.all< + AgentConfig, + NewPackageConfig | undefined + >([ + agentConfigService.create(soClient, request.body, { + user, + }), + // If needed, retrieve System package information and build a new package config for the system package + // NOTE: we ignore failures in attempting to create package config, since config might have been created + // successfully + withSysMonitoring + ? packageConfigService + .buildPackageConfigFromPackage(soClient, DefaultPackages.system) + .catch(() => undefined) + : undefined, + ]); - // Create the system monitoring datasource and add it to config. - if (withSysMonitoring && newSysDatasource !== undefined && agentConfig !== undefined) { - newSysDatasource.config_id = agentConfig.id; - const sysDatasource = await datasourceService.create(soClient, newSysDatasource, { user }); + // Create the system monitoring package config and add it to agent config. + if (withSysMonitoring && newSysPackageConfig !== undefined && agentConfig !== undefined) { + newSysPackageConfig.config_id = agentConfig.id; + newSysPackageConfig.namespace = agentConfig.namespace; + const sysPackageConfig = await packageConfigService.create(soClient, newSysPackageConfig, { + user, + }); - if (sysDatasource) { - agentConfig = await agentConfigService.assignDatasources(soClient, agentConfig.id, [ - sysDatasource.id, + if (sysPackageConfig) { + agentConfig = await agentConfigService.assignPackageConfigs(soClient, agentConfig.id, [ + sysPackageConfig.id, ]); } } diff --git a/x-pack/plugins/ingest_manager/server/routes/datasource/index.ts b/x-pack/plugins/ingest_manager/server/routes/datasource/index.ts deleted file mode 100644 index 7217f28053cf3..0000000000000 --- a/x-pack/plugins/ingest_manager/server/routes/datasource/index.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { IRouter } from 'src/core/server'; -import { PLUGIN_ID, DATASOURCE_API_ROUTES } from '../../constants'; -import { - GetDatasourcesRequestSchema, - GetOneDatasourceRequestSchema, - CreateDatasourceRequestSchema, - UpdateDatasourceRequestSchema, - DeleteDatasourcesRequestSchema, -} from '../../types'; -import { - getDatasourcesHandler, - getOneDatasourceHandler, - createDatasourceHandler, - updateDatasourceHandler, - deleteDatasourceHandler, -} from './handlers'; - -export const registerRoutes = (router: IRouter) => { - // List - router.get( - { - path: DATASOURCE_API_ROUTES.LIST_PATTERN, - validate: GetDatasourcesRequestSchema, - options: { tags: [`access:${PLUGIN_ID}-read`] }, - }, - getDatasourcesHandler - ); - - // Get one - router.get( - { - path: DATASOURCE_API_ROUTES.INFO_PATTERN, - validate: GetOneDatasourceRequestSchema, - options: { tags: [`access:${PLUGIN_ID}-read`] }, - }, - getOneDatasourceHandler - ); - - // Create - router.post( - { - path: DATASOURCE_API_ROUTES.CREATE_PATTERN, - validate: CreateDatasourceRequestSchema, - options: { tags: [`access:${PLUGIN_ID}-all`] }, - }, - createDatasourceHandler - ); - - // Update - router.put( - { - path: DATASOURCE_API_ROUTES.UPDATE_PATTERN, - validate: UpdateDatasourceRequestSchema, - options: { tags: [`access:${PLUGIN_ID}-all`] }, - }, - updateDatasourceHandler - ); - - // Delete - router.post( - { - path: DATASOURCE_API_ROUTES.DELETE_PATTERN, - validate: DeleteDatasourcesRequestSchema, - options: { tags: [`access:${PLUGIN_ID}`] }, - }, - deleteDatasourceHandler - ); -}; diff --git a/x-pack/plugins/ingest_manager/server/routes/index.ts b/x-pack/plugins/ingest_manager/server/routes/index.ts index 0978c2aa57bf6..f6b4439d8bef1 100644 --- a/x-pack/plugins/ingest_manager/server/routes/index.ts +++ b/x-pack/plugins/ingest_manager/server/routes/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ export { registerRoutes as registerAgentConfigRoutes } from './agent_config'; -export { registerRoutes as registerDatasourceRoutes } from './datasource'; +export { registerRoutes as registerPackageConfigRoutes } from './package_config'; export { registerRoutes as registerDataStreamRoutes } from './data_streams'; export { registerRoutes as registerEPMRoutes } from './epm'; export { registerRoutes as registerSetupRoutes } from './setup'; diff --git a/x-pack/plugins/ingest_manager/server/routes/datasource/datasource_handlers.test.ts b/x-pack/plugins/ingest_manager/server/routes/package_config/handlers.test.ts similarity index 85% rename from x-pack/plugins/ingest_manager/server/routes/datasource/datasource_handlers.test.ts rename to x-pack/plugins/ingest_manager/server/routes/package_config/handlers.test.ts index 07cbeb8b2cec5..6d712ce063290 100644 --- a/x-pack/plugins/ingest_manager/server/routes/datasource/datasource_handlers.test.ts +++ b/x-pack/plugins/ingest_manager/server/routes/package_config/handlers.test.ts @@ -7,23 +7,23 @@ import { httpServerMock, httpServiceMock } from 'src/core/server/mocks'; import { IRouter, KibanaRequest, Logger, RequestHandler, RouteConfig } from 'kibana/server'; import { registerRoutes } from './index'; -import { DATASOURCE_API_ROUTES } from '../../../common/constants'; +import { PACKAGE_CONFIG_API_ROUTES } from '../../../common/constants'; import { xpackMocks } from '../../../../../mocks'; import { appContextService } from '../../services'; import { createAppContextStartContractMock } from '../../mocks'; -import { DatasourceServiceInterface, ExternalCallback } from '../..'; -import { CreateDatasourceRequestSchema } from '../../types/rest_spec'; -import { datasourceService } from '../../services'; +import { PackageConfigServiceInterface, ExternalCallback } from '../..'; +import { CreatePackageConfigRequestSchema } from '../../types/rest_spec'; +import { packageConfigService } from '../../services'; -const datasourceServiceMock = datasourceService as jest.Mocked; +const packageConfigServiceMock = packageConfigService as jest.Mocked; -jest.mock('../../services/datasource', (): { - datasourceService: jest.Mocked; +jest.mock('../../services/package_config', (): { + packageConfigService: jest.Mocked; } => { return { - datasourceService: { + packageConfigService: { assignPackageStream: jest.fn((packageInfo, dataInputs) => Promise.resolve(dataInputs)), - buildDatasourceFromPackage: jest.fn(), + buildPackageConfigFromPackage: jest.fn(), bulkCreate: jest.fn(), create: jest.fn((soClient, newData) => Promise.resolve({ @@ -52,7 +52,7 @@ jest.mock('../../services/epm/packages', () => { }; }); -describe('When calling datasource', () => { +describe('When calling package config', () => { let routerMock: jest.Mocked; let routeHandler: RequestHandler; let routeConfig: RouteConfig; @@ -77,12 +77,12 @@ describe('When calling datasource', () => { describe('create api handler', () => { const getCreateKibanaRequest = ( - newData?: typeof CreateDatasourceRequestSchema.body - ): KibanaRequest => { + newData?: typeof CreatePackageConfigRequestSchema.body + ): KibanaRequest => { return httpServerMock.createKibanaRequest< undefined, undefined, - typeof CreateDatasourceRequestSchema.body + typeof CreatePackageConfigRequestSchema.body >({ path: routeConfig.path, method: 'post', @@ -102,7 +102,7 @@ describe('When calling datasource', () => { // Set the routeConfig and routeHandler to the Create API beforeAll(() => { [routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) => - path.startsWith(DATASOURCE_API_ROUTES.CREATE_PATTERN) + path.startsWith(PACKAGE_CONFIG_API_ROUTES.CREATE_PATTERN) )!; }); @@ -151,8 +151,8 @@ describe('When calling datasource', () => { }); beforeEach(() => { - appContextService.addExternalCallback('datasourceCreate', callbackOne); - appContextService.addExternalCallback('datasourceCreate', callbackTwo); + appContextService.addExternalCallback('packageConfigCreate', callbackOne); + appContextService.addExternalCallback('packageConfigCreate', callbackTwo); }); afterEach(() => (callbackCallingOrder.length = 0)); @@ -164,7 +164,7 @@ describe('When calling datasource', () => { expect(callbackCallingOrder).toEqual(['one', 'two']); }); - it('should feed datasource returned by last callback', async () => { + it('should feed package config returned by last callback', async () => { const request = getCreateKibanaRequest(); await routeHandler(context, request, response); expect(response.ok).toHaveBeenCalled(); @@ -213,7 +213,7 @@ describe('When calling datasource', () => { const request = getCreateKibanaRequest(); await routeHandler(context, request, response); expect(response.ok).toHaveBeenCalled(); - expect(datasourceServiceMock.create.mock.calls[0][1]).toEqual({ + expect(packageConfigServiceMock.create.mock.calls[0][1]).toEqual({ config_id: 'a5ca00c0-b30c-11ea-9732-1bb05811278c', description: '', enabled: true, @@ -268,8 +268,8 @@ describe('When calling datasource', () => { }); beforeEach(() => { - appContextService.addExternalCallback('datasourceCreate', callbackThree); - appContextService.addExternalCallback('datasourceCreate', callbackFour); + appContextService.addExternalCallback('packageConfigCreate', callbackThree); + appContextService.addExternalCallback('packageConfigCreate', callbackFour); }); it('should skip over callback exceptions and still execute other callbacks', async () => { @@ -285,16 +285,16 @@ describe('When calling datasource', () => { await routeHandler(context, request, response); expect(response.ok).toHaveBeenCalled(); expect(errorLogger.mock.calls).toEqual([ - ['An external registered [datasourceCreate] callback failed when executed'], + ['An external registered [packageConfigCreate] callback failed when executed'], [new Error('callbackThree threw error on purpose')], ]); }); - it('should create datasource with last successful returned datasource', async () => { + it('should create package config with last successful returned package config', async () => { const request = getCreateKibanaRequest(); await routeHandler(context, request, response); expect(response.ok).toHaveBeenCalled(); - expect(datasourceServiceMock.create.mock.calls[0][1]).toEqual({ + expect(packageConfigServiceMock.create.mock.calls[0][1]).toEqual({ config_id: 'a5ca00c0-b30c-11ea-9732-1bb05811278c', description: '', enabled: true, diff --git a/x-pack/plugins/ingest_manager/server/routes/datasource/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/package_config/handlers.ts similarity index 56% rename from x-pack/plugins/ingest_manager/server/routes/datasource/handlers.ts rename to x-pack/plugins/ingest_manager/server/routes/package_config/handlers.ts index 4f83d24a846ea..e212c861ce770 100644 --- a/x-pack/plugins/ingest_manager/server/routes/datasource/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/package_config/handlers.ts @@ -6,25 +6,28 @@ import { TypeOf } from '@kbn/config-schema'; import Boom from 'boom'; import { RequestHandler } from 'src/core/server'; -import { appContextService, datasourceService } from '../../services'; +import { appContextService, packageConfigService } from '../../services'; import { ensureInstalledPackage, getPackageInfo } from '../../services/epm/packages'; import { - GetDatasourcesRequestSchema, - GetOneDatasourceRequestSchema, - CreateDatasourceRequestSchema, - UpdateDatasourceRequestSchema, - DeleteDatasourcesRequestSchema, - NewDatasource, + GetPackageConfigsRequestSchema, + GetOnePackageConfigRequestSchema, + CreatePackageConfigRequestSchema, + UpdatePackageConfigRequestSchema, + DeletePackageConfigsRequestSchema, + NewPackageConfig, } from '../../types'; -import { CreateDatasourceResponse, DeleteDatasourcesResponse } from '../../../common'; +import { CreatePackageConfigResponse, DeletePackageConfigsResponse } from '../../../common'; -export const getDatasourcesHandler: RequestHandler< +export const getPackageConfigsHandler: RequestHandler< undefined, - TypeOf + TypeOf > = async (context, request, response) => { const soClient = context.core.savedObjects.client; try { - const { items, total, page, perPage } = await datasourceService.list(soClient, request.query); + const { items, total, page, perPage } = await packageConfigService.list( + soClient, + request.query + ); return response.ok({ body: { items, @@ -42,23 +45,23 @@ export const getDatasourcesHandler: RequestHandler< } }; -export const getOneDatasourceHandler: RequestHandler> = async (context, request, response) => { const soClient = context.core.savedObjects.client; try { - const datasource = await datasourceService.get(soClient, request.params.datasourceId); - if (datasource) { + const packageConfig = await packageConfigService.get(soClient, request.params.packageConfigId); + if (packageConfig) { return response.ok({ body: { - item: datasource, + item: packageConfig, success: true, }, }); } else { return response.customError({ statusCode: 404, - body: { message: 'Datasource not found' }, + body: { message: 'Package config not found' }, }); } } catch (e) { @@ -69,10 +72,10 @@ export const getOneDatasourceHandler: RequestHandler + TypeOf > = async (context, request, response) => { const soClient = context.core.savedObjects.client; const callCluster = context.core.elasticsearch.legacy.client.callAsCurrentUser; @@ -80,33 +83,30 @@ export const createDatasourceHandler: RequestHandler< const logger = appContextService.getLogger(); let newData = { ...request.body }; try { - // If we have external callbacks, then process those now before creating the actual datasource - const externalCallbacks = appContextService.getExternalCallbacks('datasourceCreate'); + // If we have external callbacks, then process those now before creating the actual package config + const externalCallbacks = appContextService.getExternalCallbacks('packageConfigCreate'); if (externalCallbacks && externalCallbacks.size > 0) { - let updatedNewData: NewDatasource = newData; + let updatedNewData: NewPackageConfig = newData; for (const callback of externalCallbacks) { try { // ensure that the returned value by the callback passes schema validation - updatedNewData = CreateDatasourceRequestSchema.body.validate( + updatedNewData = CreatePackageConfigRequestSchema.body.validate( await callback(updatedNewData) ); } catch (error) { // Log the error, but keep going and process the other callbacks - logger.error('An external registered [datasourceCreate] callback failed when executed'); + logger.error( + 'An external registered [packageConfigCreate] callback failed when executed' + ); logger.error(error); } } - // The type `NewDatasource` and the `DatasourceBaseSchema` are incompatible. - // `NewDatasrouce` defines `namespace` as optional string, which means that `undefined` is a - // valid value, however, the schema defines it as string with a minimum length of 1. - // Here, we need to cast the value back to the schema type and ignore the TS error. - // @ts-ignore - newData = updatedNewData as typeof CreateDatasourceRequestSchema.body; + newData = updatedNewData; } - // Make sure the datasource package is installed + // Make sure the associated package is installed if (newData.package?.name) { await ensureInstalledPackage({ savedObjectsClient: soClient, @@ -118,15 +118,15 @@ export const createDatasourceHandler: RequestHandler< pkgName: newData.package.name, pkgVersion: newData.package.version, }); - newData.inputs = (await datasourceService.assignPackageStream( + newData.inputs = (await packageConfigService.assignPackageStream( pkgInfo, newData.inputs - )) as TypeOf['inputs']; + )) as TypeOf['inputs']; } - // Create datasource - const datasource = await datasourceService.create(soClient, newData, { user }); - const body: CreateDatasourceResponse = { item: datasource, success: true }; + // Create package config + const packageConfig = await packageConfigService.create(soClient, newData, { user }); + const body: CreatePackageConfigResponse = { item: packageConfig, success: true }; return response.ok({ body, }); @@ -139,42 +139,42 @@ export const createDatasourceHandler: RequestHandler< } }; -export const updateDatasourceHandler: RequestHandler< - TypeOf, +export const updatePackageConfigHandler: RequestHandler< + TypeOf, unknown, - TypeOf + TypeOf > = async (context, request, response) => { const soClient = context.core.savedObjects.client; const user = (await appContextService.getSecurity()?.authc.getCurrentUser(request)) || undefined; try { - const datasource = await datasourceService.get(soClient, request.params.datasourceId); + const packageConfig = await packageConfigService.get(soClient, request.params.packageConfigId); - if (!datasource) { - throw Boom.notFound('Datasource not found'); + if (!packageConfig) { + throw Boom.notFound('Package config not found'); } const newData = { ...request.body }; - const pkg = newData.package || datasource.package; - const inputs = newData.inputs || datasource.inputs; + const pkg = newData.package || packageConfig.package; + const inputs = newData.inputs || packageConfig.inputs; if (pkg && (newData.inputs || newData.package)) { const pkgInfo = await getPackageInfo({ savedObjectsClient: soClient, pkgName: pkg.name, pkgVersion: pkg.version, }); - newData.inputs = (await datasourceService.assignPackageStream(pkgInfo, inputs)) as TypeOf< - typeof CreateDatasourceRequestSchema.body + newData.inputs = (await packageConfigService.assignPackageStream(pkgInfo, inputs)) as TypeOf< + typeof CreatePackageConfigRequestSchema.body >['inputs']; } - const updatedDatasource = await datasourceService.update( + const updatedPackageConfig = await packageConfigService.update( soClient, - request.params.datasourceId, + request.params.packageConfigId, newData, { user } ); return response.ok({ - body: { item: updatedDatasource, success: true }, + body: { item: updatedPackageConfig, success: true }, }); } catch (e) { return response.customError({ @@ -184,17 +184,17 @@ export const updateDatasourceHandler: RequestHandler< } }; -export const deleteDatasourceHandler: RequestHandler< +export const deletePackageConfigHandler: RequestHandler< unknown, unknown, - TypeOf + TypeOf > = async (context, request, response) => { const soClient = context.core.savedObjects.client; const user = (await appContextService.getSecurity()?.authc.getCurrentUser(request)) || undefined; try { - const body: DeleteDatasourcesResponse = await datasourceService.delete( + const body: DeletePackageConfigsResponse = await packageConfigService.delete( soClient, - request.body.datasourceIds, + request.body.packageConfigIds, { user } ); return response.ok({ diff --git a/x-pack/plugins/ingest_manager/server/routes/package_config/index.ts b/x-pack/plugins/ingest_manager/server/routes/package_config/index.ts new file mode 100644 index 0000000000000..1da045e052997 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/routes/package_config/index.ts @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { IRouter } from 'src/core/server'; +import { PLUGIN_ID, PACKAGE_CONFIG_API_ROUTES } from '../../constants'; +import { + GetPackageConfigsRequestSchema, + GetOnePackageConfigRequestSchema, + CreatePackageConfigRequestSchema, + UpdatePackageConfigRequestSchema, + DeletePackageConfigsRequestSchema, +} from '../../types'; +import { + getPackageConfigsHandler, + getOnePackageConfigHandler, + createPackageConfigHandler, + updatePackageConfigHandler, + deletePackageConfigHandler, +} from './handlers'; + +export const registerRoutes = (router: IRouter) => { + // List + router.get( + { + path: PACKAGE_CONFIG_API_ROUTES.LIST_PATTERN, + validate: GetPackageConfigsRequestSchema, + options: { tags: [`access:${PLUGIN_ID}-read`] }, + }, + getPackageConfigsHandler + ); + + // Get one + router.get( + { + path: PACKAGE_CONFIG_API_ROUTES.INFO_PATTERN, + validate: GetOnePackageConfigRequestSchema, + options: { tags: [`access:${PLUGIN_ID}-read`] }, + }, + getOnePackageConfigHandler + ); + + // Create + router.post( + { + path: PACKAGE_CONFIG_API_ROUTES.CREATE_PATTERN, + validate: CreatePackageConfigRequestSchema, + options: { tags: [`access:${PLUGIN_ID}-all`] }, + }, + createPackageConfigHandler + ); + + // Update + router.put( + { + path: PACKAGE_CONFIG_API_ROUTES.UPDATE_PATTERN, + validate: UpdatePackageConfigRequestSchema, + options: { tags: [`access:${PLUGIN_ID}-all`] }, + }, + updatePackageConfigHandler + ); + + // Delete + router.post( + { + path: PACKAGE_CONFIG_API_ROUTES.DELETE_PATTERN, + validate: DeletePackageConfigsRequestSchema, + options: { tags: [`access:${PLUGIN_ID}`] }, + }, + deletePackageConfigHandler + ); +}; diff --git a/x-pack/plugins/ingest_manager/server/saved_objects/index.ts b/x-pack/plugins/ingest_manager/server/saved_objects/index.ts index 1d412937e244f..98de9ac217af9 100644 --- a/x-pack/plugins/ingest_manager/server/saved_objects/index.ts +++ b/x-pack/plugins/ingest_manager/server/saved_objects/index.ts @@ -9,7 +9,7 @@ import { EncryptedSavedObjectsPluginSetup } from '../../../encrypted_saved_objec import { OUTPUT_SAVED_OBJECT_TYPE, AGENT_CONFIG_SAVED_OBJECT_TYPE, - DATASOURCE_SAVED_OBJECT_TYPE, + PACKAGE_CONFIG_SAVED_OBJECT_TYPE, PACKAGES_SAVED_OBJECT_TYPE, AGENT_SAVED_OBJECT_TYPE, AGENT_EVENT_SAVED_OBJECT_TYPE, @@ -17,14 +17,13 @@ import { ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, GLOBAL_SETTINGS_SAVED_OBJECT_TYPE, } from '../constants'; -import { migrateDatasourcesToV790 } from './migrations/datasources_v790'; -import { migrateAgentConfigToV790 } from './migrations/agent_config_v790'; + /* * Saved object types and mappings * - * Please update typings in `/common/types` if mappings are updated. + * Please update typings in `/common/types` as well as + * schemas in `/server/types` if mappings are updated. */ - const savedObjectTypes: { [key: string]: SavedObjectsType } = { [GLOBAL_SETTINGS_SAVED_OBJECT_TYPE]: { name: GLOBAL_SETTINGS_SAVED_OBJECT_TYPE, @@ -122,20 +121,17 @@ const savedObjectTypes: { [key: string]: SavedObjectsType } = { properties: { id: { type: 'keyword' }, name: { type: 'text' }, - is_default: { type: 'boolean' }, - namespace: { type: 'keyword' }, description: { type: 'text' }, + namespace: { type: 'keyword' }, + is_default: { type: 'boolean' }, status: { type: 'keyword' }, - datasources: { type: 'keyword' }, + package_configs: { type: 'keyword' }, updated_at: { type: 'date' }, updated_by: { type: 'keyword' }, revision: { type: 'integer' }, monitoring_enabled: { type: 'keyword', index: false }, }, }, - migrations: { - '7.9.0': migrateAgentConfigToV790, - }, }, [ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE]: { name: ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, @@ -178,8 +174,8 @@ const savedObjectTypes: { [key: string]: SavedObjectsType } = { }, }, }, - [DATASOURCE_SAVED_OBJECT_TYPE]: { - name: DATASOURCE_SAVED_OBJECT_TYPE, + [PACKAGE_CONFIG_SAVED_OBJECT_TYPE]: { + name: PACKAGE_CONFIG_SAVED_OBJECT_TYPE, hidden: false, namespaceType: 'agnostic', management: { @@ -190,8 +186,9 @@ const savedObjectTypes: { [key: string]: SavedObjectsType } = { name: { type: 'keyword' }, description: { type: 'text' }, namespace: { type: 'keyword' }, - config_id: { type: 'keyword' }, enabled: { type: 'boolean' }, + config_id: { type: 'keyword' }, + output_id: { type: 'keyword' }, package: { properties: { name: { type: 'keyword' }, @@ -199,16 +196,14 @@ const savedObjectTypes: { [key: string]: SavedObjectsType } = { version: { type: 'keyword' }, }, }, - output_id: { type: 'keyword' }, inputs: { type: 'nested', enabled: false, properties: { type: { type: 'keyword' }, enabled: { type: 'boolean' }, - processors: { type: 'keyword' }, - config: { type: 'flattened' }, vars: { type: 'flattened' }, + config: { type: 'flattened' }, streams: { type: 'nested', properties: { @@ -220,10 +215,9 @@ const savedObjectTypes: { [key: string]: SavedObjectsType } = { type: { type: 'keyword' }, }, }, - processors: { type: 'keyword' }, - config: { type: 'flattened' }, - agent_stream: { type: 'flattened' }, vars: { type: 'flattened' }, + config: { type: 'flattened' }, + compiled_stream: { type: 'flattened' }, }, }, }, @@ -235,9 +229,6 @@ const savedObjectTypes: { [key: string]: SavedObjectsType } = { created_by: { type: 'keyword' }, }, }, - migrations: { - '7.9.0': migrateDatasourcesToV790, - }, }, [PACKAGES_SAVED_OBJECT_TYPE]: { name: PACKAGES_SAVED_OBJECT_TYPE, diff --git a/x-pack/plugins/ingest_manager/server/saved_objects/migrations/agent_config_v790.ts b/x-pack/plugins/ingest_manager/server/saved_objects/migrations/agent_config_v790.ts deleted file mode 100644 index 0c850f2c25fbf..0000000000000 --- a/x-pack/plugins/ingest_manager/server/saved_objects/migrations/agent_config_v790.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { SavedObjectMigrationFn } from 'kibana/server'; -import { cloneDeep } from 'lodash'; -import { AgentConfig } from '../../types'; - -type Pre790AgentConfig = Exclude & { - updated_on: string; -}; - -export const migrateAgentConfigToV790: SavedObjectMigrationFn = ( - doc -) => { - const updatedAgentConfig = cloneDeep(doc); - - updatedAgentConfig.attributes.updated_at = doc.attributes.updated_on; - delete updatedAgentConfig.attributes.updated_on; - - return updatedAgentConfig; -}; diff --git a/x-pack/plugins/ingest_manager/server/saved_objects/migrations/datasources_v790.ts b/x-pack/plugins/ingest_manager/server/saved_objects/migrations/datasources_v790.ts deleted file mode 100644 index d1e4b29daefc6..0000000000000 --- a/x-pack/plugins/ingest_manager/server/saved_objects/migrations/datasources_v790.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { SavedObjectMigrationFn } from 'kibana/server'; -import { cloneDeep } from 'lodash'; -import { Datasource, DatasourceInput, DatasourceInputStream } from '../../types'; - -type Pre790Datasource = Exclude< - Datasource, - 'created_at' | 'created_by' | 'updated_at' | 'updated_by' -> & { - inputs: Array< - Exclude & { - streams: Array< - Exclude & { - dataset: string; - } - >; - } - >; -}; - -export const migrateDatasourcesToV790: SavedObjectMigrationFn = ( - doc -) => { - const updatedDatasource = cloneDeep(doc); - const defDate = new Date().toISOString(); - - updatedDatasource.attributes.created_by = 'system'; - updatedDatasource.attributes.created_at = updatedDatasource?.updated_at ?? defDate; - updatedDatasource.attributes.updated_by = 'system'; - updatedDatasource.attributes.updated_at = updatedDatasource?.updated_at ?? defDate; - updatedDatasource.attributes.inputs.forEach((input) => { - input.streams.forEach((stream) => { - stream.dataset = { name: (stream.dataset as unknown) as string, type: '' }; - }); - }); - - return updatedDatasource; -}; diff --git a/x-pack/plugins/ingest_manager/server/services/agent_config.ts b/x-pack/plugins/ingest_manager/server/services/agent_config.ts index 9c27e9b7a3a7a..ada35d1825069 100644 --- a/x-pack/plugins/ingest_manager/server/services/agent_config.ts +++ b/x-pack/plugins/ingest_manager/server/services/agent_config.ts @@ -12,7 +12,7 @@ import { AGENT_SAVED_OBJECT_TYPE, } from '../constants'; import { - Datasource, + PackageConfig, NewAgentConfig, AgentConfig, AgentConfigSOAttributes, @@ -20,9 +20,9 @@ import { AgentConfigStatus, ListWithKuery, } from '../types'; -import { DeleteAgentConfigResponse, storedDatasourcesToAgentInputs } from '../../common'; +import { DeleteAgentConfigResponse, storedPackageConfigsToAgentInputs } from '../../common'; import { listAgents } from './agents'; -import { datasourceService } from './datasource'; +import { packageConfigService } from './package_config'; import { outputService } from './output'; import { agentConfigUpdateEventHandler } from './agent_config_update'; @@ -115,7 +115,7 @@ class AgentConfigService { public async get( soClient: SavedObjectsClientContract, id: string, - withDatasources: boolean = true + withPackageConfigs: boolean = true ): Promise { const agentConfigSO = await soClient.get(SAVED_OBJECT_TYPE, id); if (!agentConfigSO) { @@ -128,11 +128,11 @@ class AgentConfigService { const agentConfig = { id: agentConfigSO.id, ...agentConfigSO.attributes }; - if (withDatasources) { - agentConfig.datasources = - (await datasourceService.getByIDs( + if (withPackageConfigs) { + agentConfig.package_configs = + (await packageConfigService.getByIDs( soClient, - (agentConfigSO.attributes.datasources as string[]) || [] + (agentConfigSO.attributes.package_configs as string[]) || [] )) || []; } @@ -200,15 +200,20 @@ class AgentConfigService { options ); - // Copy all datasources - if (baseAgentConfig.datasources.length) { - const newDatasources = (baseAgentConfig.datasources as Datasource[]).map( - (datasource: Datasource) => { - const { id: datasourceId, ...newDatasource } = datasource; - return newDatasource; + // Copy all package configs + if (baseAgentConfig.package_configs.length) { + const newPackageConfigs = (baseAgentConfig.package_configs as PackageConfig[]).map( + (packageConfig: PackageConfig) => { + const { id: packageConfigId, ...newPackageConfig } = packageConfig; + return newPackageConfig; } ); - await datasourceService.bulkCreate(soClient, newDatasources, newAgentConfig.id, options); + await packageConfigService.bulkCreate( + soClient, + newPackageConfigs, + newAgentConfig.id, + options + ); } // Get updated config @@ -228,10 +233,10 @@ class AgentConfigService { return this._update(soClient, id, {}, options?.user); } - public async assignDatasources( + public async assignPackageConfigs( soClient: SavedObjectsClientContract, id: string, - datasourceIds: string[], + packageConfigIds: string[], options?: { user?: AuthenticatedUser } ): Promise { const oldAgentConfig = await this.get(soClient, id, false); @@ -244,18 +249,18 @@ class AgentConfigService { soClient, id, { - datasources: uniq( - [...((oldAgentConfig.datasources || []) as string[])].concat(datasourceIds) + package_configs: uniq( + [...((oldAgentConfig.package_configs || []) as string[])].concat(packageConfigIds) ), }, options?.user ); } - public async unassignDatasources( + public async unassignPackageConfigs( soClient: SavedObjectsClientContract, id: string, - datasourceIds: string[], + packageConfigIds: string[], options?: { user?: AuthenticatedUser } ): Promise { const oldAgentConfig = await this.get(soClient, id, false); @@ -269,9 +274,9 @@ class AgentConfigService { id, { ...oldAgentConfig, - datasources: uniq( - [...((oldAgentConfig.datasources || []) as string[])].filter( - (dsId) => !datasourceIds.includes(dsId) + package_configs: uniq( + [...((oldAgentConfig.package_configs || []) as string[])].filter( + (pkgConfigId) => !packageConfigIds.includes(pkgConfigId) ) ), }, @@ -318,8 +323,8 @@ class AgentConfigService { throw new Error('Cannot delete agent config that is assigned to agent(s)'); } - if (config.datasources && config.datasources.length) { - await datasourceService.delete(soClient, config.datasources as string[], { + if (config.package_configs && config.package_configs.length) { + await packageConfigService.delete(soClient, config.package_configs as string[], { skipUnassignFromAgentConfigs: true, }); } @@ -373,7 +378,7 @@ class AgentConfigService { {} as FullAgentConfig['outputs'] ), }, - inputs: storedDatasourcesToAgentInputs(config.datasources as Datasource[]), + inputs: storedPackageConfigsToAgentInputs(config.package_configs as PackageConfig[]), revision: config.revision, ...(config.monitoring_enabled && config.monitoring_enabled.length > 0 ? { diff --git a/x-pack/plugins/ingest_manager/server/services/epm/agent/agent.ts b/x-pack/plugins/ingest_manager/server/services/epm/agent/agent.ts index 0bcb2464f8d74..d697ad0576396 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/agent/agent.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/agent/agent.ts @@ -6,9 +6,9 @@ import Handlebars from 'handlebars'; import { safeLoad, safeDump } from 'js-yaml'; -import { DatasourceConfigRecord } from '../../../../common'; +import { PackageConfigConfigRecord } from '../../../../common'; -export function createStream(variables: DatasourceConfigRecord, streamTemplate: string) { +export function createStream(variables: PackageConfigConfigRecord, streamTemplate: string) { const { vars, yamlValues } = buildTemplateVariables(variables, streamTemplate); const template = Handlebars.compile(streamTemplate, { noEscape: true }); @@ -52,7 +52,7 @@ function replaceVariablesInYaml(yamlVariables: { [k: string]: any }, yaml: any) return yaml; } -function buildTemplateVariables(variables: DatasourceConfigRecord, streamTemplate: string) { +function buildTemplateVariables(variables: PackageConfigConfigRecord, streamTemplate: string) { const yamlValues: { [k: string]: any } = {}; const vars = Object.entries(variables).reduce((acc, [key, recordEntry]) => { // support variables with . like key.patterns diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts index 9b506a2d055a7..94af672d8e29f 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts @@ -6,12 +6,12 @@ import { SavedObjectsClientContract } from 'src/core/server'; import Boom from 'boom'; -import { PACKAGES_SAVED_OBJECT_TYPE, DATASOURCE_SAVED_OBJECT_TYPE } from '../../../constants'; +import { PACKAGES_SAVED_OBJECT_TYPE, PACKAGE_CONFIG_SAVED_OBJECT_TYPE } from '../../../constants'; import { AssetReference, AssetType, ElasticsearchAssetType } from '../../../types'; import { CallESAsCurrentUser } from '../../../types'; import { getInstallation, savedObjectTypes } from './index'; import { installIndexPatterns } from '../kibana/index_pattern/install'; -import { datasourceService } from '../..'; +import { packageConfigService } from '../..'; export async function removeInstallation(options: { savedObjectsClient: SavedObjectsClientContract; @@ -27,15 +27,15 @@ export async function removeInstallation(options: { throw Boom.badRequest(`${pkgName} is installed by default and cannot be removed`); const installedObjects = installation.installed || []; - const { total } = await datasourceService.list(savedObjectsClient, { - kuery: `${DATASOURCE_SAVED_OBJECT_TYPE}.package.name:${pkgName}`, + const { total } = await packageConfigService.list(savedObjectsClient, { + kuery: `${PACKAGE_CONFIG_SAVED_OBJECT_TYPE}.package.name:${pkgName}`, page: 0, perPage: 0, }); if (total > 0) throw Boom.badRequest( - `unable to remove package with existing datasource(s) in use by agent(s)` + `unable to remove package with existing package config(s) in use by agent(s)` ); // Delete the manager saved object with references to the asset objects diff --git a/x-pack/plugins/ingest_manager/server/services/index.ts b/x-pack/plugins/ingest_manager/server/services/index.ts index 49896959f3c3a..74adab09d12eb 100644 --- a/x-pack/plugins/ingest_manager/server/services/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/index.ts @@ -59,8 +59,8 @@ export interface AgentService { } // Saved object services -export { datasourceService } from './datasource'; export { agentConfigService } from './agent_config'; +export { packageConfigService } from './package_config'; export { outputService } from './output'; export { settingsService }; diff --git a/x-pack/plugins/ingest_manager/server/services/datasource.test.ts b/x-pack/plugins/ingest_manager/server/services/package_config.test.ts similarity index 92% rename from x-pack/plugins/ingest_manager/server/services/datasource.test.ts rename to x-pack/plugins/ingest_manager/server/services/package_config.test.ts index 8d98e41c8ae69..f8dd1c65e3e72 100644 --- a/x-pack/plugins/ingest_manager/server/services/datasource.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/package_config.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { datasourceService } from './datasource'; +import { packageConfigService } from './package_config'; import { PackageInfo } from '../types'; async function mockedGetAssetsData(_a: any, _b: any, dataset: string) { @@ -37,10 +37,10 @@ jest.mock('./epm/registry', () => { }; }); -describe('Datasource service', () => { +describe('Package config service', () => { describe('assignPackageStream', () => { it('should work with config variables from the stream', async () => { - const inputs = await datasourceService.assignPackageStream( + const inputs = await packageConfigService.assignPackageStream( ({ datasets: [ { @@ -89,7 +89,7 @@ describe('Datasource service', () => { value: ['/var/log/set.log'], }, }, - agent_stream: { + compiled_stream: { metricset: ['dataset1'], paths: ['/var/log/set.log'], type: 'log', @@ -101,7 +101,7 @@ describe('Datasource service', () => { }); it('should work with config variables at the input level', async () => { - const inputs = await datasourceService.assignPackageStream( + const inputs = await packageConfigService.assignPackageStream( ({ datasets: [ { @@ -150,7 +150,7 @@ describe('Datasource service', () => { id: 'dataset01', dataset: { name: 'package.dataset1', type: 'logs' }, enabled: true, - agent_stream: { + compiled_stream: { metricset: ['dataset1'], paths: ['/var/log/set.log'], type: 'log', diff --git a/x-pack/plugins/ingest_manager/server/services/datasource.ts b/x-pack/plugins/ingest_manager/server/services/package_config.ts similarity index 67% rename from x-pack/plugins/ingest_manager/server/services/datasource.ts rename to x-pack/plugins/ingest_manager/server/services/package_config.ts index fecec0c463459..c886f4868ad30 100644 --- a/x-pack/plugins/ingest_manager/server/services/datasource.ts +++ b/x-pack/plugins/ingest_manager/server/services/package_config.ts @@ -6,18 +6,18 @@ import { SavedObjectsClientContract } from 'src/core/server'; import { AuthenticatedUser } from '../../../security/server'; import { - DeleteDatasourcesResponse, - packageToConfigDatasource, - DatasourceInput, - DatasourceInputStream, + DeletePackageConfigsResponse, + packageToPackageConfig, + PackageConfigInput, + PackageConfigInputStream, PackageInfo, } from '../../common'; -import { DATASOURCE_SAVED_OBJECT_TYPE } from '../constants'; +import { PACKAGE_CONFIG_SAVED_OBJECT_TYPE } from '../constants'; import { - NewDatasource, - Datasource, + NewPackageConfig, + PackageConfig, ListWithKuery, - DatasourceSOAttributes, + PackageConfigSOAttributes, RegistryPackage, } from '../types'; import { agentConfigService } from './agent_config'; @@ -27,23 +27,23 @@ import { getPackageInfo, getInstallation } from './epm/packages'; import { getAssetsData } from './epm/packages/assets'; import { createStream } from './epm/agent/agent'; -const SAVED_OBJECT_TYPE = DATASOURCE_SAVED_OBJECT_TYPE; +const SAVED_OBJECT_TYPE = PACKAGE_CONFIG_SAVED_OBJECT_TYPE; function getDataset(st: string) { return st.split('.')[1]; } -class DatasourceService { +class PackageConfigService { public async create( soClient: SavedObjectsClientContract, - datasource: NewDatasource, + packageConfig: NewPackageConfig, options?: { id?: string; user?: AuthenticatedUser } - ): Promise { + ): Promise { const isoDate = new Date().toISOString(); - const newSo = await soClient.create( + const newSo = await soClient.create( SAVED_OBJECT_TYPE, { - ...datasource, + ...packageConfig, revision: 1, created_at: isoDate, created_by: options?.user?.username ?? 'system', @@ -54,7 +54,7 @@ class DatasourceService { ); // Assign it to the given agent config - await agentConfigService.assignDatasources(soClient, datasource.config_id, [newSo.id], { + await agentConfigService.assignPackageConfigs(soClient, packageConfig.config_id, [newSo.id], { user: options?.user, }); @@ -66,16 +66,16 @@ class DatasourceService { public async bulkCreate( soClient: SavedObjectsClientContract, - datasources: NewDatasource[], + packageConfigs: NewPackageConfig[], configId: string, options?: { user?: AuthenticatedUser } - ): Promise { + ): Promise { const isoDate = new Date().toISOString(); - const { saved_objects: newSos } = await soClient.bulkCreate>( - datasources.map((datasource) => ({ + const { saved_objects: newSos } = await soClient.bulkCreate>( + packageConfigs.map((packageConfig) => ({ type: SAVED_OBJECT_TYPE, attributes: { - ...datasource, + ...packageConfig, config_id: configId, revision: 1, created_at: isoDate, @@ -87,7 +87,7 @@ class DatasourceService { ); // Assign it to the given agent config - await agentConfigService.assignDatasources( + await agentConfigService.assignPackageConfigs( soClient, configId, newSos.map((newSo) => newSo.id), @@ -102,37 +102,40 @@ class DatasourceService { })); } - public async get(soClient: SavedObjectsClientContract, id: string): Promise { - const datasourceSO = await soClient.get(SAVED_OBJECT_TYPE, id); - if (!datasourceSO) { + public async get( + soClient: SavedObjectsClientContract, + id: string + ): Promise { + const packageConfigSO = await soClient.get(SAVED_OBJECT_TYPE, id); + if (!packageConfigSO) { return null; } - if (datasourceSO.error) { - throw new Error(datasourceSO.error.message); + if (packageConfigSO.error) { + throw new Error(packageConfigSO.error.message); } return { - id: datasourceSO.id, - ...datasourceSO.attributes, + id: packageConfigSO.id, + ...packageConfigSO.attributes, }; } public async getByIDs( soClient: SavedObjectsClientContract, ids: string[] - ): Promise { - const datasourceSO = await soClient.bulkGet( + ): Promise { + const packageConfigSO = await soClient.bulkGet( ids.map((id) => ({ id, type: SAVED_OBJECT_TYPE, })) ); - if (!datasourceSO) { + if (!packageConfigSO) { return null; } - return datasourceSO.saved_objects.map((so) => ({ + return packageConfigSO.saved_objects.map((so) => ({ id: so.id, ...so.attributes, })); @@ -141,10 +144,10 @@ class DatasourceService { public async list( soClient: SavedObjectsClientContract, options: ListWithKuery - ): Promise<{ items: Datasource[]; total: number; page: number; perPage: number }> { + ): Promise<{ items: PackageConfig[]; total: number; page: number; perPage: number }> { const { page = 1, perPage = 20, kuery } = options; - const datasources = await soClient.find({ + const packageConfigs = await soClient.find({ type: SAVED_OBJECT_TYPE, page, perPage, @@ -158,11 +161,11 @@ class DatasourceService { }); return { - items: datasources.saved_objects.map((datasourceSO) => ({ - id: datasourceSO.id, - ...datasourceSO.attributes, + items: packageConfigs.saved_objects.map((packageConfigSO) => ({ + id: packageConfigSO.id, + ...packageConfigSO.attributes, })), - total: datasources.total, + total: packageConfigs.total, page, perPage, }; @@ -171,46 +174,48 @@ class DatasourceService { public async update( soClient: SavedObjectsClientContract, id: string, - datasource: NewDatasource, + packageConfig: NewPackageConfig, options?: { user?: AuthenticatedUser } - ): Promise { - const oldDatasource = await this.get(soClient, id); + ): Promise { + const oldPackageConfig = await this.get(soClient, id); - if (!oldDatasource) { - throw new Error('Datasource not found'); + if (!oldPackageConfig) { + throw new Error('Package config not found'); } - await soClient.update(SAVED_OBJECT_TYPE, id, { - ...datasource, - revision: oldDatasource.revision + 1, + await soClient.update(SAVED_OBJECT_TYPE, id, { + ...packageConfig, + revision: oldPackageConfig.revision + 1, updated_at: new Date().toISOString(), updated_by: options?.user?.username ?? 'system', }); // Bump revision of associated agent config - await agentConfigService.bumpRevision(soClient, datasource.config_id, { user: options?.user }); + await agentConfigService.bumpRevision(soClient, packageConfig.config_id, { + user: options?.user, + }); - return (await this.get(soClient, id)) as Datasource; + return (await this.get(soClient, id)) as PackageConfig; } public async delete( soClient: SavedObjectsClientContract, ids: string[], options?: { user?: AuthenticatedUser; skipUnassignFromAgentConfigs?: boolean } - ): Promise { - const result: DeleteDatasourcesResponse = []; + ): Promise { + const result: DeletePackageConfigsResponse = []; for (const id of ids) { try { - const oldDatasource = await this.get(soClient, id); - if (!oldDatasource) { - throw new Error('Datasource not found'); + const oldPackageConfig = await this.get(soClient, id); + if (!oldPackageConfig) { + throw new Error('Package config not found'); } if (!options?.skipUnassignFromAgentConfigs) { - await agentConfigService.unassignDatasources( + await agentConfigService.unassignPackageConfigs( soClient, - oldDatasource.config_id, - [oldDatasource.id], + oldPackageConfig.config_id, + [oldPackageConfig.id], { user: options?.user, } @@ -232,10 +237,10 @@ class DatasourceService { return result; } - public async buildDatasourceFromPackage( + public async buildPackageConfigFromPackage( soClient: SavedObjectsClientContract, pkgName: string - ): Promise { + ): Promise { const pkgInstall = await getInstallation({ savedObjectsClient: soClient, pkgName }); if (pkgInstall) { const [pkgInfo, defaultOutputId] = await Promise.all([ @@ -250,15 +255,15 @@ class DatasourceService { if (!defaultOutputId) { throw new Error('Default output is not set'); } - return packageToConfigDatasource(pkgInfo, '', defaultOutputId); + return packageToPackageConfig(pkgInfo, '', defaultOutputId); } } } public async assignPackageStream( pkgInfo: PackageInfo, - inputs: DatasourceInput[] - ): Promise { + inputs: PackageConfigInput[] + ): Promise { const registryPkgInfo = await Registry.fetchInfo(pkgInfo.name, pkgInfo.version); const inputsPromises = inputs.map((input) => _assignPackageStreamToInput(registryPkgInfo, pkgInfo, input) @@ -271,7 +276,7 @@ class DatasourceService { async function _assignPackageStreamToInput( registryPkgInfo: RegistryPackage, pkgInfo: PackageInfo, - input: DatasourceInput + input: PackageConfigInput ) { const streamsPromises = input.streams.map((stream) => _assignPackageStreamToStream(registryPkgInfo, pkgInfo, input, stream) @@ -284,11 +289,11 @@ async function _assignPackageStreamToInput( async function _assignPackageStreamToStream( registryPkgInfo: RegistryPackage, pkgInfo: PackageInfo, - input: DatasourceInput, - stream: DatasourceInputStream + input: PackageConfigInput, + stream: PackageConfigInputStream ) { if (!stream.enabled) { - return { ...stream, agent_stream: undefined }; + return { ...stream, compiled_stream: undefined }; } const datasetPath = getDataset(stream.dataset.name); const packageDatasets = pkgInfo.datasets; @@ -332,10 +337,10 @@ async function _assignPackageStreamToStream( pkgStream.buffer.toString() ); - stream.agent_stream = yaml; + stream.compiled_stream = yaml; return { ...stream }; } -export type DatasourceServiceInterface = DatasourceService; -export const datasourceService = new DatasourceService(); +export type PackageConfigServiceInterface = PackageConfigService; +export const packageConfigService = new PackageConfigService(); diff --git a/x-pack/plugins/ingest_manager/server/services/setup.ts b/x-pack/plugins/ingest_manager/server/services/setup.ts index 9cf1e5b368385..61e1d0ad94db8 100644 --- a/x-pack/plugins/ingest_manager/server/services/setup.ts +++ b/x-pack/plugins/ingest_manager/server/services/setup.ts @@ -13,8 +13,8 @@ import { outputService } from './output'; import { ensureInstalledDefaultPackages } from './epm/packages/install'; import { ensureDefaultIndices } from './epm/kibana/index_pattern/install'; import { - packageToConfigDatasource, - Datasource, + packageToPackageConfig, + PackageConfig, AgentConfig, Installation, Output, @@ -22,7 +22,7 @@ import { decodeCloudId, } from '../../common'; import { getPackageInfo } from './epm/packages'; -import { datasourceService } from './datasource'; +import { packageConfigService } from './package_config'; import { generateEnrollmentAPIKey } from './api_keys'; import { settingsService } from '.'; import { appContextService } from './app_context'; @@ -86,13 +86,13 @@ export async function setupIngestManager( ]); // ensure default packages are added to the default conifg - const configWithDatasource = await agentConfigService.get(soClient, config.id, true); - if (!configWithDatasource) { + const configWithPackageConfigs = await agentConfigService.get(soClient, config.id, true); + if (!configWithPackageConfigs) { throw new Error('Config not found'); } if ( - configWithDatasource.datasources.length && - typeof configWithDatasource.datasources[0] === 'string' + configWithPackageConfigs.package_configs.length && + typeof configWithPackageConfigs.package_configs[0] === 'string' ) { throw new Error('Config not found'); } @@ -104,11 +104,19 @@ export async function setupIngestManager( continue; } - const isInstalled = configWithDatasource.datasources.some((d: Datasource | string) => { - return typeof d !== 'string' && d.package?.name === installedPackage.name; - }); + const isInstalled = configWithPackageConfigs.package_configs.some( + (d: PackageConfig | string) => { + return typeof d !== 'string' && d.package?.name === installedPackage.name; + } + ); + if (!isInstalled) { - await addPackageToConfig(soClient, installedPackage, configWithDatasource, defaultOutput); + await addPackageToConfig( + soClient, + installedPackage, + configWithPackageConfigs, + defaultOutput + ); } } @@ -194,17 +202,16 @@ async function addPackageToConfig( pkgVersion: packageToInstall.version, }); - const newDatasource = packageToConfigDatasource( + const newPackageConfig = packageToPackageConfig( packageInfo, config.id, defaultOutput.id, - undefined, config.namespace ); - newDatasource.inputs = await datasourceService.assignPackageStream( + newPackageConfig.inputs = await packageConfigService.assignPackageStream( packageInfo, - newDatasource.inputs + newPackageConfig.inputs ); - await datasourceService.create(soClient, newDatasource); + await packageConfigService.create(soClient, newPackageConfig); } diff --git a/x-pack/plugins/ingest_manager/server/types/index.tsx b/x-pack/plugins/ingest_manager/server/types/index.tsx index eedb5d86abda3..179474d31bc18 100644 --- a/x-pack/plugins/ingest_manager/server/types/index.tsx +++ b/x-pack/plugins/ingest_manager/server/types/index.tsx @@ -17,11 +17,11 @@ export { AgentEventSOAttributes, AgentAction, AgentActionSOAttributes, - Datasource, - DatasourceInput, - DatasourceInputStream, - NewDatasource, - DatasourceSOAttributes, + PackageConfig, + PackageConfigInput, + PackageConfigInputStream, + NewPackageConfig, + PackageConfigSOAttributes, FullAgentConfigInput, FullAgentConfig, AgentConfig, diff --git a/x-pack/plugins/ingest_manager/server/types/models/agent_config.ts b/x-pack/plugins/ingest_manager/server/types/models/agent_config.ts index ee91813a48e2f..a9e14301cd7c3 100644 --- a/x-pack/plugins/ingest_manager/server/types/models/agent_config.ts +++ b/x-pack/plugins/ingest_manager/server/types/models/agent_config.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { schema } from '@kbn/config-schema'; -import { DatasourceSchema } from './datasource'; +import { PackageConfigSchema } from './package_config'; import { AgentConfigStatus } from '../../../common'; const AgentConfigBaseSchema = { @@ -27,7 +27,10 @@ export const AgentConfigSchema = schema.object({ schema.literal(AgentConfigStatus.Active), schema.literal(AgentConfigStatus.Inactive), ]), - datasources: schema.oneOf([schema.arrayOf(schema.string()), schema.arrayOf(DatasourceSchema)]), + package_configs: schema.oneOf([ + schema.arrayOf(schema.string()), + schema.arrayOf(PackageConfigSchema), + ]), updated_at: schema.string(), updated_by: schema.string(), }); diff --git a/x-pack/plugins/ingest_manager/server/types/models/index.ts b/x-pack/plugins/ingest_manager/server/types/models/index.ts index 7da36c8a18ad2..268e87eb529be 100644 --- a/x-pack/plugins/ingest_manager/server/types/models/index.ts +++ b/x-pack/plugins/ingest_manager/server/types/models/index.ts @@ -5,6 +5,6 @@ */ export * from './agent_config'; export * from './agent'; -export * from './datasource'; +export * from './package_config'; export * from './output'; export * from './enrollment_api_key'; diff --git a/x-pack/plugins/ingest_manager/server/types/models/datasource.ts b/x-pack/plugins/ingest_manager/server/types/models/package_config.ts similarity index 84% rename from x-pack/plugins/ingest_manager/server/types/models/datasource.ts rename to x-pack/plugins/ingest_manager/server/types/models/package_config.ts index 114986c4a486e..4b9718dfbe165 100644 --- a/x-pack/plugins/ingest_manager/server/types/models/datasource.ts +++ b/x-pack/plugins/ingest_manager/server/types/models/package_config.ts @@ -13,7 +13,7 @@ const ConfigRecordSchema = schema.recordOf( }) ); -const DatasourceBaseSchema = { +const PackageConfigBaseSchema = { name: schema.string(), description: schema.maybe(schema.string()), namespace: schema.string({ minLength: 1 }), @@ -31,7 +31,6 @@ const DatasourceBaseSchema = { schema.object({ type: schema.string(), enabled: schema.boolean(), - processors: schema.maybe(schema.arrayOf(schema.string())), vars: schema.maybe(ConfigRecordSchema), config: schema.maybe( schema.recordOf( @@ -47,7 +46,6 @@ const DatasourceBaseSchema = { id: schema.string(), enabled: schema.boolean(), dataset: schema.object({ name: schema.string(), type: schema.string() }), - processors: schema.maybe(schema.arrayOf(schema.string())), vars: schema.maybe(ConfigRecordSchema), config: schema.maybe( schema.recordOf( @@ -64,11 +62,11 @@ const DatasourceBaseSchema = { ), }; -export const NewDatasourceSchema = schema.object({ - ...DatasourceBaseSchema, +export const NewPackageConfigSchema = schema.object({ + ...PackageConfigBaseSchema, }); -export const DatasourceSchema = schema.object({ - ...DatasourceBaseSchema, +export const PackageConfigSchema = schema.object({ + ...PackageConfigBaseSchema, id: schema.string(), }); diff --git a/x-pack/plugins/ingest_manager/server/types/rest_spec/datasource.ts b/x-pack/plugins/ingest_manager/server/types/rest_spec/datasource.ts deleted file mode 100644 index fce2c94b282bd..0000000000000 --- a/x-pack/plugins/ingest_manager/server/types/rest_spec/datasource.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { schema } from '@kbn/config-schema'; -import { NewDatasourceSchema } from '../models'; -import { ListWithKuerySchema } from './index'; - -export const GetDatasourcesRequestSchema = { - query: ListWithKuerySchema, -}; - -export const GetOneDatasourceRequestSchema = { - params: schema.object({ - datasourceId: schema.string(), - }), -}; - -export const CreateDatasourceRequestSchema = { - body: NewDatasourceSchema, -}; - -export const UpdateDatasourceRequestSchema = { - ...GetOneDatasourceRequestSchema, - body: NewDatasourceSchema, -}; - -export const DeleteDatasourcesRequestSchema = { - body: schema.object({ - datasourceIds: schema.arrayOf(schema.string()), - }), -}; diff --git a/x-pack/plugins/ingest_manager/server/types/rest_spec/index.ts b/x-pack/plugins/ingest_manager/server/types/rest_spec/index.ts index 7dc3d4f8f1961..f3ee868f43f00 100644 --- a/x-pack/plugins/ingest_manager/server/types/rest_spec/index.ts +++ b/x-pack/plugins/ingest_manager/server/types/rest_spec/index.ts @@ -6,7 +6,7 @@ export * from './common'; export * from './agent_config'; export * from './agent'; -export * from './datasource'; +export * from './package_config'; export * from './epm'; export * from './enrollment_api_key'; export * from './install_script'; diff --git a/x-pack/plugins/ingest_manager/server/types/rest_spec/package_config.ts b/x-pack/plugins/ingest_manager/server/types/rest_spec/package_config.ts new file mode 100644 index 0000000000000..7b7ae1957c15e --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/types/rest_spec/package_config.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { schema } from '@kbn/config-schema'; +import { NewPackageConfigSchema } from '../models'; +import { ListWithKuerySchema } from './index'; + +export const GetPackageConfigsRequestSchema = { + query: ListWithKuerySchema, +}; + +export const GetOnePackageConfigRequestSchema = { + params: schema.object({ + packageConfigId: schema.string(), + }), +}; + +export const CreatePackageConfigRequestSchema = { + body: NewPackageConfigSchema, +}; + +export const UpdatePackageConfigRequestSchema = { + ...GetOnePackageConfigRequestSchema, + body: NewPackageConfigSchema, +}; + +export const DeletePackageConfigsRequestSchema = { + body: schema.object({ + packageConfigIds: schema.arrayOf(schema.string()), + }), +}; diff --git a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts index 8ebb130f5fc83..c075e1041973b 100644 --- a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts @@ -1006,9 +1006,9 @@ export class EndpointDocGenerator { } /** - * Generates an Ingest `datasource` that includes the Endpoint Policy data + * Generates an Ingest `package config` that includes the Endpoint Policy data */ - public generatePolicyDatasource(): PolicyData { + public generatePolicyPackageConfig(): PolicyData { const created = new Date(Date.now() - 8.64e7).toISOString(); // 24h ago return { id: this.seededUUIDv4(), diff --git a/x-pack/plugins/security_solution/common/endpoint/types.ts b/x-pack/plugins/security_solution/common/endpoint/types.ts index 72839a8370495..ca5cc449a7ad7 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Datasource, NewDatasource } from '../../../ingest_manager/common'; +import { PackageConfig, NewPackageConfig } from '../../../ingest_manager/common'; /** * Object that allows you to maintain stateful information in the location object across navigation events @@ -670,14 +670,14 @@ export enum ProtectionModes { } /** - * Endpoint Policy data, which extends Ingest's `Datasource` type + * Endpoint Policy data, which extends Ingest's `PackageConfig` type */ -export type PolicyData = Datasource & NewPolicyData; +export type PolicyData = PackageConfig & NewPolicyData; /** * New policy data. Used when updating the policy record via ingest APIs */ -export type NewPolicyData = NewDatasource & { +export type NewPolicyData = NewPackageConfig & { inputs: [ { type: 'endpoint'; diff --git a/x-pack/plugins/security_solution/public/common/mock/endpoint/dependencies_start_mock.ts b/x-pack/plugins/security_solution/public/common/mock/endpoint/dependencies_start_mock.ts index f2e8d045eccf9..9276d503176c6 100644 --- a/x-pack/plugins/security_solution/public/common/mock/endpoint/dependencies_start_mock.ts +++ b/x-pack/plugins/security_solution/public/common/mock/endpoint/dependencies_start_mock.ts @@ -4,7 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IngestManagerStart, registerDatasource } from '../../../../../ingest_manager/public'; +import { + IngestManagerStart, + registerPackageConfigComponent, +} from '../../../../../ingest_manager/public'; import { dataPluginMock, Start as DataPublicStartMock, @@ -56,6 +59,6 @@ export const depsStartMock: () => DepsStartMock = () => { return { data: dataMock, - ingestManager: { success: Promise.resolve(true), registerDatasource }, + ingestManager: { success: Promise.resolve(true), registerPackageConfigComponent }, }; }; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts index ce164318fdadc..12fa3dc47beac 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts @@ -16,7 +16,7 @@ import { } from './selectors'; import { HostState } from '../types'; import { - sendGetEndpointSpecificDatasources, + sendGetEndpointSpecificPackageConfigs, sendGetEndpointSecurityPackage, } from '../../policy/store/policy_list/services/ingest'; @@ -69,7 +69,7 @@ export const hostMiddlewareFactory: ImmutableMiddlewareFactory = (cor if (hostResponse && hostResponse.hosts.length === 0) { const http = coreStart.http; try { - const policyDataResponse: GetPolicyListResponse = await sendGetEndpointSpecificDatasources( + const policyDataResponse: GetPolicyListResponse = await sendGetEndpointSpecificPackageConfigs( http, { query: { diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx index 3601b8db5ee59..d49335ca8de2c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx @@ -37,7 +37,7 @@ import { PolicyEmptyState, EndpointsEmptyState } from '../../../components/manag import { FormattedDate } from '../../../../common/components/formatted_date'; import { useNavigateToAppEventHandler } from '../../../../common/hooks/endpoint/use_navigate_to_app_event_handler'; import { - CreateDatasourceRouteState, + CreatePackageConfigRouteState, AgentConfigDetailsDeployAgentAction, } from '../../../../../../ingest_manager/public'; import { SecurityPageName } from '../../../../app/types'; @@ -118,11 +118,11 @@ export const HostList = () => { [history, queryParams] ); - const handleCreatePolicyClick = useNavigateToAppEventHandler( + const handleCreatePolicyClick = useNavigateToAppEventHandler( 'ingestManager', { path: `#/integrations${ - endpointPackageVersion ? `/endpoint-${endpointPackageVersion}/add-datasource` : '' + endpointPackageVersion ? `/endpoint-${endpointPackageVersion}/add-integration` : '' }`, state: { onCancelNavigateTo: [ diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware.ts index 899f85ecdea30..cfa1a478619b7 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware.ts @@ -12,9 +12,9 @@ import { policyDetailsForUpdate, } from './selectors'; import { - sendGetDatasource, + sendGetPackageConfig, sendGetFleetAgentStatusForConfig, - sendPutDatasource, + sendPutPackageConfig, } from '../policy_list/services/ingest'; import { NewPolicyData, PolicyData } from '../../../../../../common/endpoint/types'; import { ImmutableMiddlewareFactory } from '../../../../../common/store'; @@ -33,7 +33,7 @@ export const policyDetailsMiddlewareFactory: ImmutableMiddlewareFactory { }, }); await waitForAction('serverReturnedPolicyListData'); - expect(fakeCoreStart.http.get).toHaveBeenCalledWith(INGEST_API_DATASOURCES, { + expect(fakeCoreStart.http.get).toHaveBeenCalledWith(INGEST_API_PACKAGE_CONFIGS, { query: { - kuery: `${DATASOURCE_SAVED_OBJECT_TYPE}.package.name: endpoint`, + kuery: `${PACKAGE_CONFIG_SAVED_OBJECT_TYPE}.package.name: endpoint`, page: 1, perPage: 10, }, @@ -188,9 +188,9 @@ describe('policy list store concerns', () => { it('uses pagination params from url', async () => { dispatchUserChangedUrl('?page_size=50&page_index=0'); await waitForAction('serverReturnedPolicyListData'); - expect(fakeCoreStart.http.get).toHaveBeenCalledWith(INGEST_API_DATASOURCES, { + expect(fakeCoreStart.http.get).toHaveBeenCalledWith(INGEST_API_PACKAGE_CONFIGS, { query: { - kuery: `${DATASOURCE_SAVED_OBJECT_TYPE}.package.name: endpoint`, + kuery: `${PACKAGE_CONFIG_SAVED_OBJECT_TYPE}.package.name: endpoint`, page: 1, perPage: 50, }, @@ -211,9 +211,9 @@ describe('policy list store concerns', () => { it('accepts only positive numbers for page_index and page_size', async () => { dispatchUserChangedUrl('?page_size=-50&page_index=-99'); await waitForAction('serverReturnedPolicyListData'); - expect(fakeCoreStart.http.get).toHaveBeenCalledWith(INGEST_API_DATASOURCES, { + expect(fakeCoreStart.http.get).toHaveBeenCalledWith(INGEST_API_PACKAGE_CONFIGS, { query: { - kuery: `${DATASOURCE_SAVED_OBJECT_TYPE}.package.name: endpoint`, + kuery: `${PACKAGE_CONFIG_SAVED_OBJECT_TYPE}.package.name: endpoint`, page: 1, perPage: 10, }, @@ -222,9 +222,9 @@ describe('policy list store concerns', () => { it('it ignores non-numeric values for page_index and page_size', async () => { dispatchUserChangedUrl('?page_size=fifty&page_index=ten'); await waitForAction('serverReturnedPolicyListData'); - expect(fakeCoreStart.http.get).toHaveBeenCalledWith(INGEST_API_DATASOURCES, { + expect(fakeCoreStart.http.get).toHaveBeenCalledWith(INGEST_API_PACKAGE_CONFIGS, { query: { - kuery: `${DATASOURCE_SAVED_OBJECT_TYPE}.package.name: endpoint`, + kuery: `${PACKAGE_CONFIG_SAVED_OBJECT_TYPE}.package.name: endpoint`, page: 1, perPage: 10, }, @@ -233,9 +233,9 @@ describe('policy list store concerns', () => { it('accepts only known values for `page_size`', async () => { dispatchUserChangedUrl('?page_size=300&page_index=10'); await waitForAction('serverReturnedPolicyListData'); - expect(fakeCoreStart.http.get).toHaveBeenCalledWith(INGEST_API_DATASOURCES, { + expect(fakeCoreStart.http.get).toHaveBeenCalledWith(INGEST_API_PACKAGE_CONFIGS, { query: { - kuery: `${DATASOURCE_SAVED_OBJECT_TYPE}.package.name: endpoint`, + kuery: `${PACKAGE_CONFIG_SAVED_OBJECT_TYPE}.package.name: endpoint`, page: 11, perPage: 10, }, @@ -262,9 +262,9 @@ describe('policy list store concerns', () => { expect(endpointPackageVersion(store.getState())).toEqual('0.5.0'); fakeCoreStart.http.get.mockClear(); dispatchUserChangedUrl('?page_size=10&page_index=11'); - expect(fakeCoreStart.http.get).toHaveBeenCalledWith(INGEST_API_DATASOURCES, { + expect(fakeCoreStart.http.get).toHaveBeenCalledWith(INGEST_API_PACKAGE_CONFIGS, { query: { - kuery: `${DATASOURCE_SAVED_OBJECT_TYPE}.package.name: endpoint`, + kuery: `${PACKAGE_CONFIG_SAVED_OBJECT_TYPE}.package.name: endpoint`, page: 12, perPage: 10, }, diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/middleware.ts index 7d8620a5831d0..b4e1da4e43da3 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/middleware.ts @@ -6,8 +6,8 @@ import { GetPolicyListResponse, PolicyListState } from '../../types'; import { - sendGetEndpointSpecificDatasources, - sendDeleteDatasource, + sendGetEndpointSpecificPackageConfigs, + sendDeletePackageConfig, sendGetFleetAgentStatusForConfig, sendGetEndpointSecurityPackage, } from './services/ingest'; @@ -15,8 +15,8 @@ import { endpointPackageInfo, isOnPolicyListPage, urlSearchParams } from './sele import { ImmutableMiddlewareFactory } from '../../../../../common/store'; import { initialPolicyListState } from './reducer'; import { - DeleteDatasourcesResponse, - DeleteDatasourcesRequest, + DeletePackageConfigsResponse, + DeletePackageConfigsRequest, GetAgentStatusResponse, } from '../../../../../../../ingest_manager/common'; @@ -56,7 +56,7 @@ export const policyListMiddlewareFactory: ImmutableMiddlewareFactory { @@ -21,22 +21,22 @@ describe('ingest service', () => { http = httpServiceMock.createStartContract(); }); - describe('sendGetEndpointSpecificDatasources()', () => { + describe('sendGetEndpointSpecificPackageConfigs()', () => { it('auto adds kuery to api request', async () => { - await sendGetEndpointSpecificDatasources(http); - expect(http.get).toHaveBeenCalledWith('/api/ingest_manager/datasources', { + await sendGetEndpointSpecificPackageConfigs(http); + expect(http.get).toHaveBeenCalledWith('/api/ingest_manager/package_configs', { query: { - kuery: `${DATASOURCE_SAVED_OBJECT_TYPE}.package.name: endpoint`, + kuery: `${PACKAGE_CONFIG_SAVED_OBJECT_TYPE}.package.name: endpoint`, }, }); }); it('supports additional KQL to be defined on input for query params', async () => { - await sendGetEndpointSpecificDatasources(http, { + await sendGetEndpointSpecificPackageConfigs(http, { query: { kuery: 'someValueHere', page: 1, perPage: 10 }, }); - expect(http.get).toHaveBeenCalledWith('/api/ingest_manager/datasources', { + expect(http.get).toHaveBeenCalledWith('/api/ingest_manager/package_configs', { query: { - kuery: `someValueHere and ${DATASOURCE_SAVED_OBJECT_TYPE}.package.name: endpoint`, + kuery: `someValueHere and ${PACKAGE_CONFIG_SAVED_OBJECT_TYPE}.package.name: endpoint`, perPage: 10, page: 1, }, @@ -44,14 +44,14 @@ describe('ingest service', () => { }); }); - describe('sendGetDatasource()', () => { + describe('sendGetPackageConfig()', () => { it('builds correct API path', async () => { - await sendGetDatasource(http, '123'); - expect(http.get).toHaveBeenCalledWith('/api/ingest_manager/datasources/123', undefined); + await sendGetPackageConfig(http, '123'); + expect(http.get).toHaveBeenCalledWith('/api/ingest_manager/package_configs/123', undefined); }); it('supports http options', async () => { - await sendGetDatasource(http, '123', { query: { page: 1 } }); - expect(http.get).toHaveBeenCalledWith('/api/ingest_manager/datasources/123', { + await sendGetPackageConfig(http, '123', { query: { page: 1 } }); + expect(http.get).toHaveBeenCalledWith('/api/ingest_manager/package_configs/123', { query: { page: 1, }, diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.ts index cbdd67261739f..48b6bedf50fd8 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.ts @@ -6,92 +6,92 @@ import { HttpFetchOptions, HttpStart } from 'kibana/public'; import { - GetDatasourcesRequest, + GetPackageConfigsRequest, GetAgentStatusResponse, - DeleteDatasourcesResponse, - DeleteDatasourcesRequest, - DATASOURCE_SAVED_OBJECT_TYPE, + DeletePackageConfigsResponse, + DeletePackageConfigsRequest, + PACKAGE_CONFIG_SAVED_OBJECT_TYPE, GetPackagesResponse, } from '../../../../../../../../ingest_manager/common'; import { GetPolicyListResponse, GetPolicyResponse, UpdatePolicyResponse } from '../../../types'; import { NewPolicyData } from '../../../../../../../common/endpoint/types'; const INGEST_API_ROOT = `/api/ingest_manager`; -export const INGEST_API_DATASOURCES = `${INGEST_API_ROOT}/datasources`; +export const INGEST_API_PACKAGE_CONFIGS = `${INGEST_API_ROOT}/package_configs`; const INGEST_API_FLEET = `${INGEST_API_ROOT}/fleet`; const INGEST_API_FLEET_AGENT_STATUS = `${INGEST_API_FLEET}/agent-status`; export const INGEST_API_EPM_PACKAGES = `${INGEST_API_ROOT}/epm/packages`; -const INGEST_API_DELETE_DATASOURCE = `${INGEST_API_DATASOURCES}/delete`; +const INGEST_API_DELETE_PACKAGE_CONFIG = `${INGEST_API_PACKAGE_CONFIGS}/delete`; /** - * Retrieves a list of endpoint specific datasources (those created with a `package.name` of + * Retrieves a list of endpoint specific package configs (those created with a `package.name` of * `endpoint`) from Ingest * @param http * @param options */ -export const sendGetEndpointSpecificDatasources = ( +export const sendGetEndpointSpecificPackageConfigs = ( http: HttpStart, - options: HttpFetchOptions & Partial = {} + options: HttpFetchOptions & Partial = {} ): Promise => { - return http.get(INGEST_API_DATASOURCES, { + return http.get(INGEST_API_PACKAGE_CONFIGS, { ...options, query: { ...options.query, kuery: `${ options?.query?.kuery ? `${options.query.kuery} and ` : '' - }${DATASOURCE_SAVED_OBJECT_TYPE}.package.name: endpoint`, + }${PACKAGE_CONFIG_SAVED_OBJECT_TYPE}.package.name: endpoint`, }, }); }; /** - * Retrieves a single datasource based on ID from ingest + * Retrieves a single package config based on ID from ingest * @param http - * @param datasourceId + * @param packageConfigId * @param options */ -export const sendGetDatasource = ( +export const sendGetPackageConfig = ( http: HttpStart, - datasourceId: string, + packageConfigId: string, options?: HttpFetchOptions ) => { - return http.get(`${INGEST_API_DATASOURCES}/${datasourceId}`, options); + return http.get(`${INGEST_API_PACKAGE_CONFIGS}/${packageConfigId}`, options); }; /** - * Retrieves a single datasource based on ID from ingest + * Retrieves a single package config based on ID from ingest * @param http - * @param datasourceId + * @param body * @param options */ -export const sendDeleteDatasource = ( +export const sendDeletePackageConfig = ( http: HttpStart, - body: DeleteDatasourcesRequest, + body: DeletePackageConfigsRequest, options?: HttpFetchOptions ) => { - return http.post(INGEST_API_DELETE_DATASOURCE, { + return http.post(INGEST_API_DELETE_PACKAGE_CONFIG, { ...options, body: JSON.stringify(body.body), }); }; /** - * Updates a datasources + * Updates a package config * * @param http - * @param datasourceId - * @param datasource + * @param packageConfigId + * @param packageConfig * @param options */ -export const sendPutDatasource = ( +export const sendPutPackageConfig = ( http: HttpStart, - datasourceId: string, - datasource: NewPolicyData, + packageConfigId: string, + packageConfig: NewPolicyData, options: Exclude = {} ): Promise => { - return http.put(`${INGEST_API_DATASOURCES}/${datasourceId}`, { + return http.put(`${INGEST_API_PACKAGE_CONFIGS}/${packageConfigId}`, { ...options, - body: JSON.stringify(datasource), + body: JSON.stringify(packageConfig), }); }; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/test_mock_utils.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/test_mock_utils.ts index 46f84d296bd4e..963b7922a7bff 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/test_mock_utils.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/test_mock_utils.ts @@ -5,7 +5,7 @@ */ import { HttpStart } from 'kibana/public'; -import { INGEST_API_DATASOURCES, INGEST_API_EPM_PACKAGES } from './services/ingest'; +import { INGEST_API_PACKAGE_CONFIGS, INGEST_API_EPM_PACKAGES } from './services/ingest'; import { EndpointDocGenerator } from '../../../../../../common/endpoint/generate_data'; import { GetPolicyListResponse } from '../../types'; import { @@ -99,12 +99,12 @@ export const apiPathMockResponseProviders = { */ export const setPolicyListApiMockImplementation = ( mockedHttpService: jest.Mocked, - responseItems: GetPolicyListResponse['items'] = [generator.generatePolicyDatasource()] + responseItems: GetPolicyListResponse['items'] = [generator.generatePolicyPackageConfig()] ): void => { mockedHttpService.get.mockImplementation((...args) => { const [path] = args; if (typeof path === 'string') { - if (path === INGEST_API_DATASOURCES) { + if (path === INGEST_API_PACKAGE_CONFIGS) { return Promise.resolve({ items: responseItems, total: 10, diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/types.ts b/x-pack/plugins/security_solution/public/management/pages/policy/types.ts index a3a0983331ac3..7c27acdb51568 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/types.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/types.ts @@ -14,10 +14,10 @@ import { import { ServerApiError } from '../../../common/types'; import { GetAgentStatusResponse, - GetDatasourcesResponse, - GetOneDatasourceResponse, + GetPackageConfigsResponse, + GetOnePackageConfigResponse, GetPackagesResponse, - UpdateDatasourceResponse, + UpdatePackageConfigResponse, } from '../../../../../ingest_manager/common'; /** @@ -169,14 +169,14 @@ export type KeysByValueCriteria = { /** Returns an array of the policy OSes that have a malware protection field */ export type MalwareProtectionOSes = KeysByValueCriteria; -export interface GetPolicyListResponse extends GetDatasourcesResponse { +export interface GetPolicyListResponse extends GetPackageConfigsResponse { items: PolicyData[]; } -export interface GetPolicyResponse extends GetOneDatasourceResponse { +export interface GetPolicyResponse extends GetOnePackageConfigResponse { item: PolicyData; } -export interface UpdatePolicyResponse extends UpdateDatasourceResponse { +export interface UpdatePolicyResponse extends UpdatePackageConfigResponse { item: PolicyData; } diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/configure_datasource.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/configure_package_config.tsx similarity index 79% rename from x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/configure_datasource.tsx rename to x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/configure_package_config.tsx index 9b2b4b19ce55c..ebcfd3f1bb209 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/configure_datasource.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/configure_package_config.tsx @@ -10,21 +10,21 @@ import { EuiCallOut, EuiText, EuiTitle, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { LinkToApp } from '../../../../../common/components/endpoint/link_to_app'; import { - CustomConfigureDatasourceContent, - CustomConfigureDatasourceProps, + CustomConfigurePackageConfigContent, + CustomConfigurePackageConfigProps, } from '../../../../../../../ingest_manager/public'; import { getPolicyDetailPath } from '../../../../common/routing'; import { MANAGEMENT_APP_ID } from '../../../../common/constants'; /** - * Exports Endpoint-specific datasource configuration instructions - * for use in the Ingest app create / edit datasource config + * Exports Endpoint-specific package config instructions + * for use in the Ingest app create / edit package config */ -export const ConfigureEndpointDatasource = memo( - ({ from, datasourceId }: CustomConfigureDatasourceProps) => { +export const ConfigureEndpointPackageConfig = memo( + ({ from, packageConfigId }: CustomConfigurePackageConfigProps) => { let policyUrl = ''; - if (from === 'edit' && datasourceId) { - policyUrl = getPolicyDetailPath(datasourceId); + if (from === 'edit' && packageConfigId) { + policyUrl = getPolicyDetailPath(packageConfigId); } return ( @@ -39,7 +39,7 @@ export const ConfigureEndpointDatasource = memo @@ -66,14 +66,14 @@ export const ConfigureEndpointDatasource = memo ) : ( )} @@ -85,4 +85,4 @@ export const ConfigureEndpointDatasource = memo { let middlewareSpy: AppContextTestRender['middlewareSpy']; let http: typeof coreStart.http; let render: (ui: Parameters[0]) => ReturnType; - let policyDatasource: ReturnType; + let policyPackageConfig: ReturnType; let policyView: ReturnType; beforeEach(() => { @@ -77,17 +77,17 @@ describe('Policy Details', () => { let asyncActions: Promise = Promise.resolve(); beforeEach(() => { - policyDatasource = generator.generatePolicyDatasource(); - policyDatasource.id = '1'; + policyPackageConfig = generator.generatePolicyPackageConfig(); + policyPackageConfig.id = '1'; http.get.mockImplementation((...args) => { const [path] = args; if (typeof path === 'string') { // GET datasouce - if (path === '/api/ingest_manager/datasources/1') { + if (path === '/api/ingest_manager/package_configs/1') { asyncActions = asyncActions.then(async (): Promise => sleep()); return Promise.resolve({ - item: policyDatasource, + item: policyPackageConfig, success: true, }); } @@ -132,7 +132,7 @@ describe('Policy Details', () => { const pageTitle = pageHeaderLeft.find('[data-test-subj="pageViewHeaderLeftTitle"]'); expect(pageTitle).toHaveLength(1); - expect(pageTitle.text()).toEqual(policyDatasource.name); + expect(pageTitle.text()).toEqual(policyPackageConfig.name); }); it('should navigate to list if back to link is clicked', async () => { policyView.update(); @@ -202,9 +202,9 @@ describe('Policy Details', () => { asyncActions = asyncActions.then(async () => sleep()); const [path] = args; if (typeof path === 'string') { - if (path === '/api/ingest_manager/datasources/1') { + if (path === '/api/ingest_manager/package_configs/1') { return Promise.resolve({ - item: policyDatasource, + item: policyPackageConfig, success: true, }); } @@ -247,7 +247,7 @@ describe('Policy Details', () => { // API should be called await asyncActions; - expect(http.put.mock.calls[0][0]).toEqual(`/api/ingest_manager/datasources/1`); + expect(http.put.mock.calls[0][0]).toEqual(`/api/ingest_manager/package_configs/1`); policyView.update(); // Toast notification should be shown @@ -259,7 +259,7 @@ describe('Policy Details', () => { }); }); it('should show an error notification toast if update fails', async () => { - policyDatasource.id = 'invalid'; + policyPackageConfig.id = 'invalid'; modalConfirmButton.simulate('click'); await asyncActions; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx index 5690f1381eaf3..08c6ec89ff051 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx @@ -45,7 +45,7 @@ import { SecurityPageName } from '../../../../app/types'; import { useFormatUrl } from '../../../../common/components/link_to'; import { getPolicyDetailPath, getPoliciesPath } from '../../../common/routing'; import { useNavigateToAppEventHandler } from '../../../../common/hooks/endpoint/use_navigate_to_app_event_handler'; -import { CreateDatasourceRouteState } from '../../../../../../ingest_manager/public'; +import { CreatePackageConfigRouteState } from '../../../../../../ingest_manager/public'; import { MANAGEMENT_APP_ID } from '../../../common/constants'; interface TableChangeCallbackArguments { @@ -142,16 +142,16 @@ export const PolicyList = React.memo(() => { endpointPackageVersion, } = usePolicyListSelector(selector); - const handleCreatePolicyClick = useNavigateToAppEventHandler( + const handleCreatePolicyClick = useNavigateToAppEventHandler( 'ingestManager', { // We redirect to Ingest's Integaration page if we can't get the package version, and - // to the Integration Endpoint Package Add Datasource if we have package information. + // to the Integration Endpoint Package Add Integration if we have package information. // Also, // We pass along soem state information so that the Ingest page can change the behaviour // of the cancel and submit buttons and redirect the user back to endpoint policy path: `#/integrations${ - endpointPackageVersion ? `/endpoint-${endpointPackageVersion}/add-datasource` : '' + endpointPackageVersion ? `/endpoint-${endpointPackageVersion}/add-integration` : '' }`, state: { onCancelNavigateTo: [MANAGEMENT_APP_ID, { path: getPoliciesPath() }], diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index 283ef8bad896b..65121327b40b9 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -43,7 +43,7 @@ import { APP_CASES_PATH, APP_PATH, } from '../common/constants'; -import { ConfigureEndpointDatasource } from './management/pages/policy/view/ingest_manager_integration/configure_datasource'; +import { ConfigureEndpointPackageConfig } from './management/pages/policy/view/ingest_manager_integration/configure_package_config'; import { State, createStore, createInitialState } from './common/store'; import { SecurityPageName } from './app/types'; @@ -317,7 +317,10 @@ export class Plugin implements IPlugin => { - // We only care about Endpoint datasources - if (newDatasource.package?.name !== 'endpoint') { - return newDatasource; +export const handlePackageConfigCreate = async ( + newPackageConfig: NewPackageConfig +): Promise => { + // We only care about Endpoint package configs + if (newPackageConfig.package?.name !== 'endpoint') { + return newPackageConfig; } // We cast the type here so that any changes to the Endpoint specific data // follow the types/schema expected - let updatedDatasource = newDatasource as NewPolicyData; + let updatedPackageConfig = newPackageConfig as NewPolicyData; // Until we get the Default Policy Configuration in the Endpoint package, // we will add it here manually at creation time. // @ts-ignore - if (newDatasource.inputs.length === 0) { - updatedDatasource = { - ...newDatasource, + if (newPackageConfig.inputs.length === 0) { + updatedPackageConfig = { + ...newPackageConfig, inputs: [ { type: 'endpoint', @@ -45,5 +45,5 @@ export const handleDatasourceCreate = async ( }; } - return updatedDatasource; + return updatedPackageConfig; }; diff --git a/x-pack/plugins/security_solution/server/endpoint/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/mocks.ts index 84f3d1a5631bf..ffd919db87fc9 100644 --- a/x-pack/plugins/security_solution/server/endpoint/mocks.ts +++ b/x-pack/plugins/security_solution/server/endpoint/mocks.ts @@ -12,7 +12,7 @@ import { ExternalCallback, } from '../../../ingest_manager/server'; import { EndpointAppContextServiceStartContract } from './endpoint_app_context_services'; -import { createDatasourceServiceMock } from '../../../ingest_manager/server/mocks'; +import { createPackageConfigServiceMock } from '../../../ingest_manager/server/mocks'; /** * Crates a mocked input contract for the `EndpointAppContextService#start()` method @@ -57,7 +57,7 @@ export const createMockIngestManagerStartContract = ( }, agentService: createMockAgentService(), registerExternalCallback: jest.fn((...args: ExternalCallback) => {}), - datasourceService: createDatasourceServiceMock(), + packageConfigService: createPackageConfigServiceMock(), }; }; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index b2533f1bc8c19..074e93e10fd12 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -8031,7 +8031,7 @@ "xpack.ingestManager.agentConfigList.addButton": "エージェント構成を作成", "xpack.ingestManager.agentConfigList.agentsColumnTitle": "エージェント", "xpack.ingestManager.agentConfigList.clearFiltersLinkText": "フィルターを消去", - "xpack.ingestManager.agentConfigList.datasourcesCountColumnTitle": "データソース", + "xpack.ingestManager.agentConfigList.packageConfigsCountColumnTitle": "データソース", "xpack.ingestManager.agentConfigList.descriptionColumnTitle": "説明", "xpack.ingestManager.agentConfigList.loadingAgentConfigsMessage": "エージェント構成を読み込み中...", "xpack.ingestManager.agentConfigList.nameColumnTitle": "名前", @@ -8156,28 +8156,27 @@ "xpack.ingestManager.appNavigation.sendFeedbackButton": "フィードバックを送信", "xpack.ingestManager.appNavigation.settingsButton": "設定", "xpack.ingestManager.appTitle": "Ingest Manager", - "xpack.ingestManager.configDetails.addDatasourceButtonText": "データソースを作成", + "xpack.ingestManager.configDetails.addPackageConfigButtonText": "データソースを作成", "xpack.ingestManager.configDetails.configDetailsTitle": "構成「{id}」", "xpack.ingestManager.configDetails.configNotFoundErrorTitle": "構成「{id}」が見つかりません", - "xpack.ingestManager.configDetails.datasourcesTable.actionsColumnTitle": "アクション", - "xpack.ingestManager.configDetails.datasourcesTable.deleteActionTitle": "データソースを削除", - "xpack.ingestManager.configDetails.datasourcesTable.descriptionColumnTitle": "説明", - "xpack.ingestManager.configDetails.datasourcesTable.editActionTitle": "データソースを編集", - "xpack.ingestManager.configDetails.datasourcesTable.nameColumnTitle": "データソース", - "xpack.ingestManager.configDetails.datasourcesTable.namespaceColumnTitle": "名前空間", - "xpack.ingestManager.configDetails.datasourcesTable.packageNameColumnTitle": "統合", - "xpack.ingestManager.configDetails.datasourcesTable.streamsCountColumnTitle": "ストリーム", - "xpack.ingestManager.configDetails.subTabs.datasourcesTabText": "データソース", + "xpack.ingestManager.configDetails.packageConfigsTable.actionsColumnTitle": "アクション", + "xpack.ingestManager.configDetails.packageConfigsTable.deleteActionTitle": "データソースを削除", + "xpack.ingestManager.configDetails.packageConfigsTable.descriptionColumnTitle": "説明", + "xpack.ingestManager.configDetails.packageConfigsTable.editActionTitle": "データソースを編集", + "xpack.ingestManager.configDetails.packageConfigsTable.nameColumnTitle": "データソース", + "xpack.ingestManager.configDetails.packageConfigsTable.namespaceColumnTitle": "名前空間", + "xpack.ingestManager.configDetails.packageConfigsTable.packageNameColumnTitle": "統合", + "xpack.ingestManager.configDetails.packageConfigsTable.streamsCountColumnTitle": "ストリーム", + "xpack.ingestManager.configDetails.subTabs.packageConfigsTabText": "データソース", "xpack.ingestManager.configDetails.subTabs.settingsTabText": "設定", - "xpack.ingestManager.configDetails.summary.datasources": "データソース", "xpack.ingestManager.configDetails.summary.lastUpdated": "最終更新日:", "xpack.ingestManager.configDetails.summary.revision": "リビジョン", "xpack.ingestManager.configDetails.summary.usedBy": "使用者", "xpack.ingestManager.configDetails.unexceptedErrorTitle": "構成を読み込む間にエラーが発生しました", "xpack.ingestManager.configDetails.viewAgentListTitle": "すべてのエージェント構成を表示", - "xpack.ingestManager.configDetailsDatasources.createFirstButtonText": "データソースを作成", - "xpack.ingestManager.configDetailsDatasources.createFirstMessage": "この構成にはデータソースはまだありません。", - "xpack.ingestManager.configDetailsDatasources.createFirstTitle": "初めてのデーソースを作成する", + "xpack.ingestManager.configDetailsPackageConfigs.createFirstButtonText": "データソースを作成", + "xpack.ingestManager.configDetailsPackageConfigs.createFirstMessage": "この構成にはデータソースはまだありません。", + "xpack.ingestManager.configDetailsPackageConfigs.createFirstTitle": "初めてのデーソースを作成する", "xpack.ingestManager.configForm.deleteConfigActionText": "構成を削除", "xpack.ingestManager.configForm.deleteConfigGroupDescription": "既存のデータは削除されません。", "xpack.ingestManager.configForm.deleteConfigGroupTitle": "構成を削除", @@ -8190,50 +8189,50 @@ "xpack.ingestManager.createAgentConfig.flyoutTitleDescription": "エージェント構成は、エージェントのグループ全体にわたる設定を管理する目的で使用されます。エージェント構成にデータソースを追加すると、エージェントで収集するデータを指定できます。エージェント構成の編集時には、フリートを使用して、指定したエージェントのグループに更新をデプロイできます。", "xpack.ingestManager.createAgentConfig.submitButtonLabel": "エージェント構成を作成", "xpack.ingestManager.createAgentConfig.successNotificationTitle": "エージェント構成「{name}」を作成しました", - "xpack.ingestManager.createDatasource.addedNotificationMessage": "フリートは'{agentConfigName}'構成で使用されているすべてのエージェントに更新をデプロイします。", - "xpack.ingestManager.createDatasource.addedNotificationTitle": "正常に'{datasourceName}'を追加しました", - "xpack.ingestManager.createDatasource.agentConfigurationNameLabel": "構成", - "xpack.ingestManager.createDatasource.cancelButton": "キャンセル", - "xpack.ingestManager.createDatasource.cancelLinkText": "キャンセル", - "xpack.ingestManager.createDatasource.packageNameLabel": "統合", - "xpack.ingestManager.createDatasource.pageDescriptionfromConfig": "次の手順に従い、統合をこのエージェント構成に追加します。", - "xpack.ingestManager.createDatasource.pageDescriptionfromPackage": "次の手順に従い、この統合をエージェント構成に追加します。", - "xpack.ingestManager.createDatasource.pageTitle": "データソースを追加", - "xpack.ingestManager.createDatasource.saveButton": "データソースを保存", - "xpack.ingestManager.createDatasource.stepConfgiureDatasourceTitle": "収集するデータを選択", - "xpack.ingestManager.createDatasource.stepConfigure.advancedOptionsToggleLinkText": "高度なオプション", - "xpack.ingestManager.createDatasource.stepConfigure.datasourceDescriptionInputLabel": "説明", - "xpack.ingestManager.createDatasource.stepConfigure.datasourceNameInputLabel": "データソース名", - "xpack.ingestManager.createDatasource.stepConfigure.datasourceNamespaceInputLabel": "名前空間", - "xpack.ingestManager.createDatasource.stepConfigure.hideStreamsAriaLabel": "{type} ストリームを隠す", - "xpack.ingestManager.createDatasource.stepConfigure.inputConfigErrorsTooltip": "構成エラーを修正してください", - "xpack.ingestManager.createDatasource.stepConfigure.inputLevelErrorsTooltip": "構成エラーを修正してください", - "xpack.ingestManager.createDatasource.stepConfigure.inputSettingsDescription": "次の設定はすべてのストリームに適用されます。", - "xpack.ingestManager.createDatasource.stepConfigure.inputSettingsTitle": "設定", - "xpack.ingestManager.createDatasource.stepConfigure.inputVarFieldOptionalLabel": "オプション", - "xpack.ingestManager.createDatasource.stepConfigure.noConfigOptionsMessage": "構成するものがありません", - "xpack.ingestManager.createDatasource.stepConfigure.showStreamsAriaLabel": "{type} ストリームを表示", - "xpack.ingestManager.createDatasource.stepConfigure.streamLevelErrorsTooltip": "構成エラーを修正してください", - "xpack.ingestManager.createDatasource.stepConfigure.streamsEnabledCountText": "{count} / {total, plural, one {# ストリーム} other {# ストリーム}}が有効です", - "xpack.ingestManager.createDatasource.stepConfigure.toggleAdvancedOptionsButtonText": "高度なオプション", - "xpack.ingestManager.createDatasource.stepConfigure.validationErrorText": "続行する前に、上記のエラーを修正してください", - "xpack.ingestManager.createDatasource.stepConfigure.validationErrorTitle": "データソース構成にエラーがあります", - "xpack.ingestManager.createDatasource.stepDefineDatasourceTitle": "データソースを定義", - "xpack.ingestManager.createDatasource.stepSelectAgentConfigTitle": "エージェント構成を選択する", - "xpack.ingestManager.createDatasource.StepSelectConfig.agentConfigAgentsCountText": "{count, plural, one {# エージェント} other {# エージェント}}", - "xpack.ingestManager.createDatasource.StepSelectConfig.errorLoadingAgentConfigsTitle": "エージェント構成の読み込みエラー", - "xpack.ingestManager.createDatasource.StepSelectConfig.errorLoadingPackageTitle": "パッケージ情報の読み込みエラー", - "xpack.ingestManager.createDatasource.StepSelectConfig.errorLoadingSelectedAgentConfigTitle": "選択したエージェント構成の読み込みエラー", - "xpack.ingestManager.createDatasource.StepSelectConfig.filterAgentConfigsInputPlaceholder": "エージェント構成の検索", - "xpack.ingestManager.createDatasource.stepSelectPackage.errorLoadingConfigTitle": "エージェント構成情報の読み込みエラー", - "xpack.ingestManager.createDatasource.stepSelectPackage.errorLoadingPackagesTitle": "統合の読み込みエラー", - "xpack.ingestManager.createDatasource.stepSelectPackage.errorLoadingSelectedPackageTitle": "選択した統合の読み込みエラー", - "xpack.ingestManager.createDatasource.stepSelectPackage.filterPackagesInputPlaceholder": "統合を検索", - "xpack.ingestManager.createDatasource.stepSelectPackageTitle": "統合を選択", - "xpack.ingestManager.datasourceValidation.invalidArrayErrorMessage": "無効なフォーマット", - "xpack.ingestManager.datasourceValidation.invalidYamlFormatErrorMessage": "YAML形式が無効です", - "xpack.ingestManager.datasourceValidation.nameRequiredErrorMessage": "名前が必要です", - "xpack.ingestManager.datasourceValidation.requiredErrorMessage": "{fieldName}が必要です", + "xpack.ingestManager.createPackageConfig.addedNotificationMessage": "フリートは'{agentConfigName}'構成で使用されているすべてのエージェントに更新をデプロイします。", + "xpack.ingestManager.createPackageConfig.addedNotificationTitle": "正常に'{packageConfigName}'を追加しました", + "xpack.ingestManager.createPackageConfig.agentConfigurationNameLabel": "構成", + "xpack.ingestManager.createPackageConfig.cancelButton": "キャンセル", + "xpack.ingestManager.createPackageConfig.cancelLinkText": "キャンセル", + "xpack.ingestManager.createPackageConfig.packageNameLabel": "統合", + "xpack.ingestManager.createPackageConfig.pageDescriptionfromConfig": "次の手順に従い、統合をこのエージェント構成に追加します。", + "xpack.ingestManager.createPackageConfig.pageDescriptionfromPackage": "次の手順に従い、この統合をエージェント構成に追加します。", + "xpack.ingestManager.createPackageConfig.pageTitle": "データソースを追加", + "xpack.ingestManager.createPackageConfig.saveButton": "データソースを保存", + "xpack.ingestManager.createPackageConfig.stepConfigurePackageConfigTitle": "収集するデータを選択", + "xpack.ingestManager.createPackageConfig.stepConfigure.advancedOptionsToggleLinkText": "高度なオプション", + "xpack.ingestManager.createPackageConfig.stepConfigure.packageConfigDescriptionInputLabel": "説明", + "xpack.ingestManager.createPackageConfig.stepConfigure.packageConfigNameInputLabel": "データソース名", + "xpack.ingestManager.createPackageConfig.stepConfigure.packageConfigNamespaceInputLabel": "名前空間", + "xpack.ingestManager.createPackageConfig.stepConfigure.hideStreamsAriaLabel": "{type} ストリームを隠す", + "xpack.ingestManager.createPackageConfig.stepConfigure.inputConfigErrorsTooltip": "構成エラーを修正してください", + "xpack.ingestManager.createPackageConfig.stepConfigure.inputLevelErrorsTooltip": "構成エラーを修正してください", + "xpack.ingestManager.createPackageConfig.stepConfigure.inputSettingsDescription": "次の設定はすべてのストリームに適用されます。", + "xpack.ingestManager.createPackageConfig.stepConfigure.inputSettingsTitle": "設定", + "xpack.ingestManager.createPackageConfig.stepConfigure.inputVarFieldOptionalLabel": "オプション", + "xpack.ingestManager.createPackageConfig.stepConfigure.noConfigOptionsMessage": "構成するものがありません", + "xpack.ingestManager.createPackageConfig.stepConfigure.showStreamsAriaLabel": "{type} ストリームを表示", + "xpack.ingestManager.createPackageConfig.stepConfigure.streamLevelErrorsTooltip": "構成エラーを修正してください", + "xpack.ingestManager.createPackageConfig.stepConfigure.streamsEnabledCountText": "{count} / {total, plural, one {# ストリーム} other {# ストリーム}}が有効です", + "xpack.ingestManager.createPackageConfig.stepConfigure.toggleAdvancedOptionsButtonText": "高度なオプション", + "xpack.ingestManager.createPackageConfig.stepConfigure.validationErrorText": "続行する前に、上記のエラーを修正してください", + "xpack.ingestManager.createPackageConfig.stepConfigure.validationErrorTitle": "データソース構成にエラーがあります", + "xpack.ingestManager.createPackageConfig.stepDefinePackageConfigTitle": "データソースを定義", + "xpack.ingestManager.createPackageConfig.stepSelectAgentConfigTitle": "エージェント構成を選択する", + "xpack.ingestManager.createPackageConfig.StepSelectConfig.agentConfigAgentsCountText": "{count, plural, one {# エージェント} other {# エージェント}}", + "xpack.ingestManager.createPackageConfig.StepSelectConfig.errorLoadingAgentConfigsTitle": "エージェント構成の読み込みエラー", + "xpack.ingestManager.createPackageConfig.StepSelectConfig.errorLoadingPackageTitle": "パッケージ情報の読み込みエラー", + "xpack.ingestManager.createPackageConfig.StepSelectConfig.errorLoadingSelectedAgentConfigTitle": "選択したエージェント構成の読み込みエラー", + "xpack.ingestManager.createPackageConfig.StepSelectConfig.filterAgentConfigsInputPlaceholder": "エージェント構成の検索", + "xpack.ingestManager.createPackageConfig.stepSelectPackage.errorLoadingConfigTitle": "エージェント構成情報の読み込みエラー", + "xpack.ingestManager.createPackageConfig.stepSelectPackage.errorLoadingPackagesTitle": "統合の読み込みエラー", + "xpack.ingestManager.createPackageConfig.stepSelectPackage.errorLoadingSelectedPackageTitle": "選択した統合の読み込みエラー", + "xpack.ingestManager.createPackageConfig.stepSelectPackage.filterPackagesInputPlaceholder": "統合を検索", + "xpack.ingestManager.createPackageConfig.stepSelectPackageTitle": "統合を選択", + "xpack.ingestManager.packageConfigValidation.invalidArrayErrorMessage": "無効なフォーマット", + "xpack.ingestManager.packageConfigValidation.invalidYamlFormatErrorMessage": "YAML形式が無効です", + "xpack.ingestManager.packageConfigValidation.nameRequiredErrorMessage": "名前が必要です", + "xpack.ingestManager.packageConfigValidation.requiredErrorMessage": "{fieldName}が必要です", "xpack.ingestManager.dataStreamList.actionsColumnTitle": "アクション", "xpack.ingestManager.dataStreamList.datasetColumnTitle": "データセット", "xpack.ingestManager.dataStreamList.integrationColumnTitle": "統合", @@ -8263,19 +8262,19 @@ "xpack.ingestManager.deleteAgentConfig.failureSingleNotificationTitle": "エージェント構成「{id}」の削除エラー", "xpack.ingestManager.deleteAgentConfig.fatalErrorNotificationTitle": "エージェント構成の削除エラー", "xpack.ingestManager.deleteAgentConfig.successSingleNotificationTitle": "エージェント構成「{id}」を削除しました", - "xpack.ingestManager.deleteDatasource.confirmModal.affectedAgentsMessage": "{agentConfigName} が一部のエージェントで既に使用されていることをフリートが検出しました。", - "xpack.ingestManager.deleteDatasource.confirmModal.affectedAgentsTitle": "このアクションは {agentsCount} {agentsCount, plural, one {# エージェント} other {# エージェント}}に影響します", - "xpack.ingestManager.deleteDatasource.confirmModal.cancelButtonLabel": "キャンセル", - "xpack.ingestManager.deleteDatasource.confirmModal.confirmButtonLabel": "{agentConfigsCount, plural, one {データソース} other {データソース}} を削除", - "xpack.ingestManager.deleteDatasource.confirmModal.deleteMultipleTitle": "{count, plural, one {データソース} other {# データソース}} を削除しますか?", - "xpack.ingestManager.deleteDatasource.confirmModal.generalMessage": "このアクションは元に戻せません。続行していいですか?", - "xpack.ingestManager.deleteDatasource.confirmModal.loadingAgentsCountMessage": "影響があるエージェントを確認中...", - "xpack.ingestManager.deleteDatasource.confirmModal.loadingButtonLabel": "読み込み中...", - "xpack.ingestManager.deleteDatasource.failureMultipleNotificationTitle": "{count} 件のデータソースの削除エラー", - "xpack.ingestManager.deleteDatasource.failureSingleNotificationTitle": "データソース「{id}」の削除エラー", - "xpack.ingestManager.deleteDatasource.fatalErrorNotificationTitle": "データソースの削除エラー", - "xpack.ingestManager.deleteDatasource.successMultipleNotificationTitle": "{count} 件のデータソースを削除しました", - "xpack.ingestManager.deleteDatasource.successSingleNotificationTitle": "データソース「{id}」を削除しました", + "xpack.ingestManager.deletePackageConfig.confirmModal.affectedAgentsMessage": "{agentConfigName} が一部のエージェントで既に使用されていることをフリートが検出しました。", + "xpack.ingestManager.deletePackageConfig.confirmModal.affectedAgentsTitle": "このアクションは {agentsCount} {agentsCount, plural, one {# エージェント} other {# エージェント}}に影響します", + "xpack.ingestManager.deletePackageConfig.confirmModal.cancelButtonLabel": "キャンセル", + "xpack.ingestManager.deletePackageConfig.confirmModal.confirmButtonLabel": "{agentConfigsCount, plural, one {データソース} other {データソース}} を削除", + "xpack.ingestManager.deletePackageConfig.confirmModal.deleteMultipleTitle": "{count, plural, one {データソース} other {# データソース}} を削除しますか?", + "xpack.ingestManager.deletePackageConfig.confirmModal.generalMessage": "このアクションは元に戻せません。続行していいですか?", + "xpack.ingestManager.deletePackageConfig.confirmModal.loadingAgentsCountMessage": "影響があるエージェントを確認中...", + "xpack.ingestManager.deletePackageConfig.confirmModal.loadingButtonLabel": "読み込み中...", + "xpack.ingestManager.deletePackageConfig.failureMultipleNotificationTitle": "{count} 件のデータソースの削除エラー", + "xpack.ingestManager.deletePackageConfig.failureSingleNotificationTitle": "データソース「{id}」の削除エラー", + "xpack.ingestManager.deletePackageConfig.fatalErrorNotificationTitle": "データソースの削除エラー", + "xpack.ingestManager.deletePackageConfig.successMultipleNotificationTitle": "{count} 件のデータソースを削除しました", + "xpack.ingestManager.deletePackageConfig.successSingleNotificationTitle": "データソース「{id}」を削除しました", "xpack.ingestManager.disabledSecurityDescription": "Elastic Fleet を使用するには、Kibana と Elasticsearch でセキュリティを有効にする必要があります。", "xpack.ingestManager.disabledSecurityTitle": "セキュリティが有効ではありません", "xpack.ingestManager.editAgentConfig.cancelButtonText": "キャンセル", @@ -8284,16 +8283,16 @@ "xpack.ingestManager.editAgentConfig.savingButtonText": "保存中…", "xpack.ingestManager.editAgentConfig.successNotificationTitle": "正常に'{name}'設定を更新しました", "xpack.ingestManager.editAgentConfig.unsavedChangesText": "保存されていない変更があります", - "xpack.ingestManager.editDatasource.cancelButton": "キャンセル", - "xpack.ingestManager.editDatasource.errorLoadingDataMessage": "このデータソース情報の読み込みエラーが発生しました", - "xpack.ingestManager.editDatasource.errorLoadingDataTitle": "データの読み込み中にエラーが発生", - "xpack.ingestManager.editDatasource.pageDescription": "次の手順に従い、このデータソースを編集します。", - "xpack.ingestManager.editDatasource.pageTitle": "データソースを編集", - "xpack.ingestManager.editDatasource.saveButton": "データソースを保存", - "xpack.ingestManager.editDatasource.stepConfgiureDatasourceTitle": "収集するデータを選択", - "xpack.ingestManager.editDatasource.stepDefineDatasourceTitle": "データソースを定義", - "xpack.ingestManager.editDatasource.updatedNotificationMessage": "フリートは'{agentConfigName}'構成で使用されているすべてのエージェントに更新をデプロイします。", - "xpack.ingestManager.editDatasource.updatedNotificationTitle": "正常に'{datasourceName}'を更新しました", + "xpack.ingestManager.editPackageConfig.cancelButton": "キャンセル", + "xpack.ingestManager.editPackageConfig.errorLoadingDataMessage": "このデータソース情報の読み込みエラーが発生しました", + "xpack.ingestManager.editPackageConfig.errorLoadingDataTitle": "データの読み込み中にエラーが発生", + "xpack.ingestManager.editPackageConfig.pageDescription": "次の手順に従い、このデータソースを編集します。", + "xpack.ingestManager.editPackageConfig.pageTitle": "データソースを編集", + "xpack.ingestManager.editPackageConfig.saveButton": "データソースを保存", + "xpack.ingestManager.editPackageConfig.stepConfigurePackageConfigTitle": "収集するデータを選択", + "xpack.ingestManager.editPackageConfig.stepDefinePackageConfigTitle": "データソースを定義", + "xpack.ingestManager.editPackageConfig.updatedNotificationMessage": "フリートは'{agentConfigName}'構成で使用されているすべてのエージェントに更新をデプロイします。", + "xpack.ingestManager.editPackageConfig.updatedNotificationTitle": "正常に'{packageConfigName}'を更新しました", "xpack.ingestManager.enrollemntAPIKeyList.emptyMessage": "登録トークンが見つかりません。", "xpack.ingestManager.enrollemntAPIKeyList.loadingTokensMessage": "登録トークンを読み込んでいます...", "xpack.ingestManager.enrollmentInstructions.copyButton": "コマンドをコピー", @@ -8316,7 +8315,7 @@ "xpack.ingestManager.enrollmentTokensList.secretTitle": "シークレット", "xpack.ingestManager.epm.browseAllButtonText": "すべての統合を参照", "xpack.ingestManager.epm.illustrationAltText": "Elastic統合の例", - "xpack.ingestManager.epm.packageDetailsNav.datasourcesLinkText": "データソース", + "xpack.ingestManager.epm.packageDetailsNav.packageConfigsLinkText": "データソース", "xpack.ingestManager.epm.packageDetailsNav.overviewLinkText": "概要", "xpack.ingestManager.epm.packageDetailsNav.settingsLinkText": "設定", "xpack.ingestManager.epm.pageSubtitle": "一般的なアプリやサービスの統合を参照する", @@ -8391,7 +8390,7 @@ "xpack.ingestManager.overviewAgentOfflineTitle": "オフライン", "xpack.ingestManager.overviewAgentTotalTitle": "合計エージェント数", "xpack.ingestManager.overviewConfigTotalTitle": "合計構成数", - "xpack.ingestManager.overviewDatasourceTitle": "データソース", + "xpack.ingestManager.overviewPackageConfigTitle": "データソース", "xpack.ingestManager.overviewDatastreamNamespacesTitle": "名前空間", "xpack.ingestManager.overviewDatastreamSizeTitle": "合計サイズ", "xpack.ingestManager.overviewDatastreamTotalTitle": "データストリーム", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 68e8b26a7e5d3..9d0bd95526670 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -8035,7 +8035,7 @@ "xpack.ingestManager.agentConfigList.addButton": "创建代理配置", "xpack.ingestManager.agentConfigList.agentsColumnTitle": "代理", "xpack.ingestManager.agentConfigList.clearFiltersLinkText": "清除筛选", - "xpack.ingestManager.agentConfigList.datasourcesCountColumnTitle": "数据源", + "xpack.ingestManager.agentConfigList.packageConfigsCountColumnTitle": "数据源", "xpack.ingestManager.agentConfigList.descriptionColumnTitle": "描述", "xpack.ingestManager.agentConfigList.loadingAgentConfigsMessage": "正在加载代理配置……", "xpack.ingestManager.agentConfigList.nameColumnTitle": "名称", @@ -8160,28 +8160,27 @@ "xpack.ingestManager.appNavigation.sendFeedbackButton": "发送反馈", "xpack.ingestManager.appNavigation.settingsButton": "设置", "xpack.ingestManager.appTitle": "Ingest Manager", - "xpack.ingestManager.configDetails.addDatasourceButtonText": "创建数据源", + "xpack.ingestManager.configDetails.addPackageConfigButtonText": "创建数据源", "xpack.ingestManager.configDetails.configDetailsTitle": "配置“{id}”", "xpack.ingestManager.configDetails.configNotFoundErrorTitle": "未找到配置“{id}”", - "xpack.ingestManager.configDetails.datasourcesTable.actionsColumnTitle": "操作", - "xpack.ingestManager.configDetails.datasourcesTable.deleteActionTitle": "删除数据源", - "xpack.ingestManager.configDetails.datasourcesTable.descriptionColumnTitle": "描述", - "xpack.ingestManager.configDetails.datasourcesTable.editActionTitle": "编辑数据源", - "xpack.ingestManager.configDetails.datasourcesTable.nameColumnTitle": "数据源", - "xpack.ingestManager.configDetails.datasourcesTable.namespaceColumnTitle": "命名空间", - "xpack.ingestManager.configDetails.datasourcesTable.packageNameColumnTitle": "集成", - "xpack.ingestManager.configDetails.datasourcesTable.streamsCountColumnTitle": "流计数", - "xpack.ingestManager.configDetails.subTabs.datasourcesTabText": "数据源", + "xpack.ingestManager.configDetails.packageConfigsTable.actionsColumnTitle": "操作", + "xpack.ingestManager.configDetails.packageConfigsTable.deleteActionTitle": "删除数据源", + "xpack.ingestManager.configDetails.packageConfigsTable.descriptionColumnTitle": "描述", + "xpack.ingestManager.configDetails.packageConfigsTable.editActionTitle": "编辑数据源", + "xpack.ingestManager.configDetails.packageConfigsTable.nameColumnTitle": "数据源", + "xpack.ingestManager.configDetails.packageConfigsTable.namespaceColumnTitle": "命名空间", + "xpack.ingestManager.configDetails.packageConfigsTable.packageNameColumnTitle": "集成", + "xpack.ingestManager.configDetails.packageConfigsTable.streamsCountColumnTitle": "流计数", + "xpack.ingestManager.configDetails.subTabs.packageConfigsTabText": "数据源", "xpack.ingestManager.configDetails.subTabs.settingsTabText": "设置", - "xpack.ingestManager.configDetails.summary.datasources": "数据源", "xpack.ingestManager.configDetails.summary.lastUpdated": "最后更新时间", "xpack.ingestManager.configDetails.summary.revision": "修订", "xpack.ingestManager.configDetails.summary.usedBy": "使用者", "xpack.ingestManager.configDetails.unexceptedErrorTitle": "加载配置时发生错误", "xpack.ingestManager.configDetails.viewAgentListTitle": "查看所有代理配置", - "xpack.ingestManager.configDetailsDatasources.createFirstButtonText": "创建数据源", - "xpack.ingestManager.configDetailsDatasources.createFirstMessage": "此配置尚未有任何数据源。", - "xpack.ingestManager.configDetailsDatasources.createFirstTitle": "创建您的首个数据源", + "xpack.ingestManager.configDetailsPackageConfigs.createFirstButtonText": "创建数据源", + "xpack.ingestManager.configDetailsPackageConfigs.createFirstMessage": "此配置尚未有任何数据源。", + "xpack.ingestManager.configDetailsPackageConfigs.createFirstTitle": "创建您的首个数据源", "xpack.ingestManager.configForm.deleteConfigActionText": "删除配置", "xpack.ingestManager.configForm.deleteConfigGroupDescription": "将不会删除现有数据。", "xpack.ingestManager.configForm.deleteConfigGroupTitle": "删除配置", @@ -8194,50 +8193,50 @@ "xpack.ingestManager.createAgentConfig.flyoutTitleDescription": "代理配置用于管理整个代理组的设置。您可以将数据源添加到代理配置以指定代理收集的数据。编辑代理配置时,可以使用 Fleet 将更新部署到指定的代理组。", "xpack.ingestManager.createAgentConfig.submitButtonLabel": "创建代理配置", "xpack.ingestManager.createAgentConfig.successNotificationTitle": "代理配置“{name}”已创建", - "xpack.ingestManager.createDatasource.addedNotificationMessage": "Fleet 会将更新部署到所有使用配置“{agentConfigName}”的代理", - "xpack.ingestManager.createDatasource.addedNotificationTitle": "已成功添加“{datasourceName}”", - "xpack.ingestManager.createDatasource.agentConfigurationNameLabel": "配置", - "xpack.ingestManager.createDatasource.cancelButton": "取消", - "xpack.ingestManager.createDatasource.cancelLinkText": "取消", - "xpack.ingestManager.createDatasource.packageNameLabel": "集成", - "xpack.ingestManager.createDatasource.pageDescriptionfromConfig": "按照下面的说明将集成添加此代理配置。", - "xpack.ingestManager.createDatasource.pageDescriptionfromPackage": "按照下面的说明将此集成添加代理配置。", - "xpack.ingestManager.createDatasource.pageTitle": "添加数据源", - "xpack.ingestManager.createDatasource.saveButton": "保存数据源", - "xpack.ingestManager.createDatasource.stepConfgiureDatasourceTitle": "选择要收集的数据", - "xpack.ingestManager.createDatasource.stepConfigure.advancedOptionsToggleLinkText": "高级选项", - "xpack.ingestManager.createDatasource.stepConfigure.datasourceDescriptionInputLabel": "描述", - "xpack.ingestManager.createDatasource.stepConfigure.datasourceNameInputLabel": "数据源名称", - "xpack.ingestManager.createDatasource.stepConfigure.datasourceNamespaceInputLabel": "命名空间", - "xpack.ingestManager.createDatasource.stepConfigure.hideStreamsAriaLabel": "隐藏 {type} 流", - "xpack.ingestManager.createDatasource.stepConfigure.inputConfigErrorsTooltip": "解决配置错误", - "xpack.ingestManager.createDatasource.stepConfigure.inputLevelErrorsTooltip": "解决配置错误", - "xpack.ingestManager.createDatasource.stepConfigure.inputSettingsDescription": "以下设置适用于所有流。", - "xpack.ingestManager.createDatasource.stepConfigure.inputSettingsTitle": "设置", - "xpack.ingestManager.createDatasource.stepConfigure.inputVarFieldOptionalLabel": "可选", - "xpack.ingestManager.createDatasource.stepConfigure.noConfigOptionsMessage": "没有可配置的内容", - "xpack.ingestManager.createDatasource.stepConfigure.showStreamsAriaLabel": "显示 {type} 流", - "xpack.ingestManager.createDatasource.stepConfigure.streamLevelErrorsTooltip": "解决配置错误", - "xpack.ingestManager.createDatasource.stepConfigure.streamsEnabledCountText": "{count} / {total, plural, one {# 个流} other {# 个流}}已启用", - "xpack.ingestManager.createDatasource.stepConfigure.toggleAdvancedOptionsButtonText": "高级选项", - "xpack.ingestManager.createDatasource.stepConfigure.validationErrorText": "在继续之前请解决上述错误", - "xpack.ingestManager.createDatasource.stepConfigure.validationErrorTitle": "您的数据源配置有错误", - "xpack.ingestManager.createDatasource.stepDefineDatasourceTitle": "定义您的数据源", - "xpack.ingestManager.createDatasource.stepSelectAgentConfigTitle": "选择代理配置", - "xpack.ingestManager.createDatasource.StepSelectConfig.agentConfigAgentsCountText": "{count, plural, one {# 个代理} other {# 个代理}}", - "xpack.ingestManager.createDatasource.StepSelectConfig.errorLoadingAgentConfigsTitle": "加载代理配置时出错", - "xpack.ingestManager.createDatasource.StepSelectConfig.errorLoadingPackageTitle": "加载软件包信息时出错", - "xpack.ingestManager.createDatasource.StepSelectConfig.errorLoadingSelectedAgentConfigTitle": "加载选定代理配置时出错", - "xpack.ingestManager.createDatasource.StepSelectConfig.filterAgentConfigsInputPlaceholder": "搜索代理配置", - "xpack.ingestManager.createDatasource.stepSelectPackage.errorLoadingConfigTitle": "加载代理配置信息时出错", - "xpack.ingestManager.createDatasource.stepSelectPackage.errorLoadingPackagesTitle": "加载集成时出错", - "xpack.ingestManager.createDatasource.stepSelectPackage.errorLoadingSelectedPackageTitle": "加载选定集成时出错", - "xpack.ingestManager.createDatasource.stepSelectPackage.filterPackagesInputPlaceholder": "搜索集成", - "xpack.ingestManager.createDatasource.stepSelectPackageTitle": "选择集成", - "xpack.ingestManager.datasourceValidation.invalidArrayErrorMessage": "格式无效", - "xpack.ingestManager.datasourceValidation.invalidYamlFormatErrorMessage": "YAML 格式无效", - "xpack.ingestManager.datasourceValidation.nameRequiredErrorMessage": "“名称”必填", - "xpack.ingestManager.datasourceValidation.requiredErrorMessage": "“{fieldName}”必填", + "xpack.ingestManager.createPackageConfig.addedNotificationMessage": "Fleet 会将更新部署到所有使用配置“{agentConfigName}”的代理", + "xpack.ingestManager.createPackageConfig.addedNotificationTitle": "已成功添加“{packageConfigName}”", + "xpack.ingestManager.createPackageConfig.agentConfigurationNameLabel": "配置", + "xpack.ingestManager.createPackageConfig.cancelButton": "取消", + "xpack.ingestManager.createPackageConfig.cancelLinkText": "取消", + "xpack.ingestManager.createPackageConfig.packageNameLabel": "集成", + "xpack.ingestManager.createPackageConfig.pageDescriptionfromConfig": "按照下面的说明将集成添加此代理配置。", + "xpack.ingestManager.createPackageConfig.pageDescriptionfromPackage": "按照下面的说明将此集成添加代理配置。", + "xpack.ingestManager.createPackageConfig.pageTitle": "添加数据源", + "xpack.ingestManager.createPackageConfig.saveButton": "保存数据源", + "xpack.ingestManager.createPackageConfig.stepConfigurePackageConfigTitle": "选择要收集的数据", + "xpack.ingestManager.createPackageConfig.stepConfigure.advancedOptionsToggleLinkText": "高级选项", + "xpack.ingestManager.createPackageConfig.stepConfigure.packageConfigDescriptionInputLabel": "描述", + "xpack.ingestManager.createPackageConfig.stepConfigure.packageConfigNameInputLabel": "数据源名称", + "xpack.ingestManager.createPackageConfig.stepConfigure.packageConfigNamespaceInputLabel": "命名空间", + "xpack.ingestManager.createPackageConfig.stepConfigure.hideStreamsAriaLabel": "隐藏 {type} 流", + "xpack.ingestManager.createPackageConfig.stepConfigure.inputConfigErrorsTooltip": "解决配置错误", + "xpack.ingestManager.createPackageConfig.stepConfigure.inputLevelErrorsTooltip": "解决配置错误", + "xpack.ingestManager.createPackageConfig.stepConfigure.inputSettingsDescription": "以下设置适用于所有流。", + "xpack.ingestManager.createPackageConfig.stepConfigure.inputSettingsTitle": "设置", + "xpack.ingestManager.createPackageConfig.stepConfigure.inputVarFieldOptionalLabel": "可选", + "xpack.ingestManager.createPackageConfig.stepConfigure.noConfigOptionsMessage": "没有可配置的内容", + "xpack.ingestManager.createPackageConfig.stepConfigure.showStreamsAriaLabel": "显示 {type} 流", + "xpack.ingestManager.createPackageConfig.stepConfigure.streamLevelErrorsTooltip": "解决配置错误", + "xpack.ingestManager.createPackageConfig.stepConfigure.streamsEnabledCountText": "{count} / {total, plural, one {# 个流} other {# 个流}}已启用", + "xpack.ingestManager.createPackageConfig.stepConfigure.toggleAdvancedOptionsButtonText": "高级选项", + "xpack.ingestManager.createPackageConfig.stepConfigure.validationErrorText": "在继续之前请解决上述错误", + "xpack.ingestManager.createPackageConfig.stepConfigure.validationErrorTitle": "您的数据源配置有错误", + "xpack.ingestManager.createPackageConfig.stepDefinePackageConfigTitle": "定义您的数据源", + "xpack.ingestManager.createPackageConfig.stepSelectAgentConfigTitle": "选择代理配置", + "xpack.ingestManager.createPackageConfig.StepSelectConfig.agentConfigAgentsCountText": "{count, plural, one {# 个代理} other {# 个代理}}", + "xpack.ingestManager.createPackageConfig.StepSelectConfig.errorLoadingAgentConfigsTitle": "加载代理配置时出错", + "xpack.ingestManager.createPackageConfig.StepSelectConfig.errorLoadingPackageTitle": "加载软件包信息时出错", + "xpack.ingestManager.createPackageConfig.StepSelectConfig.errorLoadingSelectedAgentConfigTitle": "加载选定代理配置时出错", + "xpack.ingestManager.createPackageConfig.StepSelectConfig.filterAgentConfigsInputPlaceholder": "搜索代理配置", + "xpack.ingestManager.createPackageConfig.stepSelectPackage.errorLoadingConfigTitle": "加载代理配置信息时出错", + "xpack.ingestManager.createPackageConfig.stepSelectPackage.errorLoadingPackagesTitle": "加载集成时出错", + "xpack.ingestManager.createPackageConfig.stepSelectPackage.errorLoadingSelectedPackageTitle": "加载选定集成时出错", + "xpack.ingestManager.createPackageConfig.stepSelectPackage.filterPackagesInputPlaceholder": "搜索集成", + "xpack.ingestManager.createPackageConfig.stepSelectPackageTitle": "选择集成", + "xpack.ingestManager.packageConfigValidation.invalidArrayErrorMessage": "格式无效", + "xpack.ingestManager.packageConfigValidation.invalidYamlFormatErrorMessage": "YAML 格式无效", + "xpack.ingestManager.packageConfigValidation.nameRequiredErrorMessage": "“名称”必填", + "xpack.ingestManager.packageConfigValidation.requiredErrorMessage": "“{fieldName}”必填", "xpack.ingestManager.dataStreamList.actionsColumnTitle": "操作", "xpack.ingestManager.dataStreamList.datasetColumnTitle": "数据集", "xpack.ingestManager.dataStreamList.integrationColumnTitle": "集成", @@ -8267,19 +8266,19 @@ "xpack.ingestManager.deleteAgentConfig.failureSingleNotificationTitle": "删除代理配置“{id}”时出错", "xpack.ingestManager.deleteAgentConfig.fatalErrorNotificationTitle": "删除代理配置时出错", "xpack.ingestManager.deleteAgentConfig.successSingleNotificationTitle": "已删除代理配置“{id}”", - "xpack.ingestManager.deleteDatasource.confirmModal.affectedAgentsMessage": "Fleet 已检测到 {agentConfigName} 已由您的部分代理使用。", - "xpack.ingestManager.deleteDatasource.confirmModal.affectedAgentsTitle": "此操作将影响 {agentsCount} 个 {agentsCount, plural, one {代理} other {代理}}。", - "xpack.ingestManager.deleteDatasource.confirmModal.cancelButtonLabel": "取消", - "xpack.ingestManager.deleteDatasource.confirmModal.confirmButtonLabel": "删除{agentConfigsCount, plural, one {数据源} other {数据源}}", - "xpack.ingestManager.deleteDatasource.confirmModal.deleteMultipleTitle": "删除{count, plural, one {数据源} other {# 个数据源}}?", - "xpack.ingestManager.deleteDatasource.confirmModal.generalMessage": "此操作无法撤消。是否确定要继续?", - "xpack.ingestManager.deleteDatasource.confirmModal.loadingAgentsCountMessage": "正在检查受影响的代理……", - "xpack.ingestManager.deleteDatasource.confirmModal.loadingButtonLabel": "正在加载……", - "xpack.ingestManager.deleteDatasource.failureMultipleNotificationTitle": "删除 {count} 个数据源时出错", - "xpack.ingestManager.deleteDatasource.failureSingleNotificationTitle": "删除数据源“{id}”时出错", - "xpack.ingestManager.deleteDatasource.fatalErrorNotificationTitle": "删除数据源时出错", - "xpack.ingestManager.deleteDatasource.successMultipleNotificationTitle": "已删除 {count} 个数据源", - "xpack.ingestManager.deleteDatasource.successSingleNotificationTitle": "已删除数据源“{id}”", + "xpack.ingestManager.deletePackageConfig.confirmModal.affectedAgentsMessage": "Fleet 已检测到 {agentConfigName} 已由您的部分代理使用。", + "xpack.ingestManager.deletePackageConfig.confirmModal.affectedAgentsTitle": "此操作将影响 {agentsCount} 个 {agentsCount, plural, one {代理} other {代理}}。", + "xpack.ingestManager.deletePackageConfig.confirmModal.cancelButtonLabel": "取消", + "xpack.ingestManager.deletePackageConfig.confirmModal.confirmButtonLabel": "删除{agentConfigsCount, plural, one {数据源} other {数据源}}", + "xpack.ingestManager.deletePackageConfig.confirmModal.deleteMultipleTitle": "删除{count, plural, one {数据源} other {# 个数据源}}?", + "xpack.ingestManager.deletePackageConfig.confirmModal.generalMessage": "此操作无法撤消。是否确定要继续?", + "xpack.ingestManager.deletePackageConfig.confirmModal.loadingAgentsCountMessage": "正在检查受影响的代理……", + "xpack.ingestManager.deletePackageConfig.confirmModal.loadingButtonLabel": "正在加载……", + "xpack.ingestManager.deletePackageConfig.failureMultipleNotificationTitle": "删除 {count} 个数据源时出错", + "xpack.ingestManager.deletePackageConfig.failureSingleNotificationTitle": "删除数据源“{id}”时出错", + "xpack.ingestManager.deletePackageConfig.fatalErrorNotificationTitle": "删除数据源时出错", + "xpack.ingestManager.deletePackageConfig.successMultipleNotificationTitle": "已删除 {count} 个数据源", + "xpack.ingestManager.deletePackageConfig.successSingleNotificationTitle": "已删除数据源“{id}”", "xpack.ingestManager.disabledSecurityDescription": "必须在 Kibana 和 Elasticsearch 启用安全性,才能使用 Elastic Fleet。", "xpack.ingestManager.disabledSecurityTitle": "安全性未启用", "xpack.ingestManager.editAgentConfig.cancelButtonText": "取消", @@ -8288,16 +8287,16 @@ "xpack.ingestManager.editAgentConfig.savingButtonText": "正在保存……", "xpack.ingestManager.editAgentConfig.successNotificationTitle": "已成功更新“{name}”设置", "xpack.ingestManager.editAgentConfig.unsavedChangesText": "您有未保存更改", - "xpack.ingestManager.editDatasource.cancelButton": "取消", - "xpack.ingestManager.editDatasource.errorLoadingDataMessage": "加载此数据源信息时出错", - "xpack.ingestManager.editDatasource.errorLoadingDataTitle": "加载数据时出错", - "xpack.ingestManager.editDatasource.pageDescription": "按照下面的说明编辑此数据源。", - "xpack.ingestManager.editDatasource.pageTitle": "编辑数据源", - "xpack.ingestManager.editDatasource.saveButton": "保存数据源", - "xpack.ingestManager.editDatasource.stepConfgiureDatasourceTitle": "选择要收集的数据", - "xpack.ingestManager.editDatasource.stepDefineDatasourceTitle": "定义您的数据源", - "xpack.ingestManager.editDatasource.updatedNotificationMessage": "Fleet 会将更新部署到所有使用配置“{agentConfigName}”的代理", - "xpack.ingestManager.editDatasource.updatedNotificationTitle": "已成功更新“{datasourceName}”", + "xpack.ingestManager.editPackageConfig.cancelButton": "取消", + "xpack.ingestManager.editPackageConfig.errorLoadingDataMessage": "加载此数据源信息时出错", + "xpack.ingestManager.editPackageConfig.errorLoadingDataTitle": "加载数据时出错", + "xpack.ingestManager.editPackageConfig.pageDescription": "按照下面的说明编辑此数据源。", + "xpack.ingestManager.editPackageConfig.pageTitle": "编辑数据源", + "xpack.ingestManager.editPackageConfig.saveButton": "保存数据源", + "xpack.ingestManager.editPackageConfig.stepConfigurePackageConfigTitle": "选择要收集的数据", + "xpack.ingestManager.editPackageConfig.stepDefinePackageConfigTitle": "定义您的数据源", + "xpack.ingestManager.editPackageConfig.updatedNotificationMessage": "Fleet 会将更新部署到所有使用配置“{agentConfigName}”的代理", + "xpack.ingestManager.editPackageConfig.updatedNotificationTitle": "已成功更新“{packageConfigName}”", "xpack.ingestManager.enrollemntAPIKeyList.emptyMessage": "未找到任何注册令牌。", "xpack.ingestManager.enrollemntAPIKeyList.loadingTokensMessage": "正在加载注册令牌......", "xpack.ingestManager.enrollmentInstructions.copyButton": "复制命令", @@ -8320,7 +8319,7 @@ "xpack.ingestManager.enrollmentTokensList.secretTitle": "密钥", "xpack.ingestManager.epm.browseAllButtonText": "浏览所有集成", "xpack.ingestManager.epm.illustrationAltText": "Elastic 集成的图示", - "xpack.ingestManager.epm.packageDetailsNav.datasourcesLinkText": "数据源", + "xpack.ingestManager.epm.packageDetailsNav.packageConfigsLinkText": "数据源", "xpack.ingestManager.epm.packageDetailsNav.overviewLinkText": "概览", "xpack.ingestManager.epm.packageDetailsNav.settingsLinkText": "设置", "xpack.ingestManager.epm.pageSubtitle": "浏览集成以了解热门应用和服务。", @@ -8395,7 +8394,7 @@ "xpack.ingestManager.overviewAgentOfflineTitle": "脱机", "xpack.ingestManager.overviewAgentTotalTitle": "代理总数", "xpack.ingestManager.overviewConfigTotalTitle": "配置总数", - "xpack.ingestManager.overviewDatasourceTitle": "数据源", + "xpack.ingestManager.overviewPackageConfigTitle": "数据源", "xpack.ingestManager.overviewDatastreamNamespacesTitle": "命名空间", "xpack.ingestManager.overviewDatastreamSizeTitle": "总大小", "xpack.ingestManager.overviewDatastreamTotalTitle": "数据流", diff --git a/x-pack/test/api_integration/apis/ingest_manager/agent_config.ts b/x-pack/test/api_integration/apis/ingest_manager/agent_config.ts index caa29561a1256..8bf3efbdaf501 100644 --- a/x-pack/test/api_integration/apis/ingest_manager/agent_config.ts +++ b/x-pack/test/api_integration/apis/ingest_manager/agent_config.ts @@ -69,7 +69,7 @@ export default function ({ getService }: FtrProviderContext) { monitoring_enabled: ['logs', 'metrics'], revision: 1, updated_by: 'elastic', - datasources: [], + package_configs: [], }); }); diff --git a/x-pack/test/functional/es_archives/fleet/agents/data.json b/x-pack/test/functional/es_archives/fleet/agents/data.json index 047f26a3f443c..c317aad8ba05b 100644 --- a/x-pack/test/functional/es_archives/fleet/agents/data.json +++ b/x-pack/test/functional/es_archives/fleet/agents/data.json @@ -212,14 +212,14 @@ "namespace": "default", "description": "Config 1", "status": "active", - "datasources": [], + "package_configs": [], "is_default": true, "monitoring_enabled": [ "logs", "metrics" ], "revision": 2, - "updated_on": "2020-05-07T19:34:42.533Z", + "updated_at": "2020-05-07T19:34:42.533Z", "updated_by": "system", "id": "config1" } 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 15e5a5524107b..0b84514de23f2 100644 --- a/x-pack/test/functional/es_archives/fleet/agents/mappings.json +++ b/x-pack/test/functional/es_archives/fleet/agents/mappings.json @@ -28,7 +28,7 @@ "application_usage_transactional": "965839e75f809fefe04f92dc4d99722a", "action_task_params": "a9d49f184ee89641044be0ca2950fa3a", "fleet-agent-events": "3231653fafe4ef3196fe3b32ab774bf2", - "ingest-datasources": "2346514df03316001d56ed4c8d46fa94", + "ingest-package-configs": "2346514df03316001d56ed4c8d46fa94", "apm-indices": "9bb9b2bf1fa636ed8619cbab5ce6a1dd", "inventory-view": "5299b67717e96502c77babf1c16fd4d3", "upgrade-assistant-reindex-operation": "296a89039fc4260292be36b1b005d8f2", @@ -1799,7 +1799,7 @@ }, "ingest-agent-configs": { "properties": { - "datasources": { + "package_configs": { "type": "keyword" }, "description": { @@ -1829,12 +1829,12 @@ "updated_by": { "type": "keyword" }, - "updated_on": { + "updated_at": { "type": "keyword" } } }, - "ingest-datasources": { + "ingest-package-configs": { "properties": { "config_id": { "type": "keyword" diff --git a/x-pack/test/functional/es_archives/ingest/policies/data.json b/x-pack/test/functional/es_archives/ingest/policies/data.json deleted file mode 100644 index 78cf18d501a3e..0000000000000 --- a/x-pack/test/functional/es_archives/ingest/policies/data.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "type": "doc", - "value": { - "index": ".kibana", - "id": "policies:1", - "source": { - "policies": { - "name": "Policy 1", - "description": "Amazing policy", - "status": "active", - "updated_on": "2019-09-20T17:35:09+0000", - "updated_by": "nchaulet" - }, - "type": "policies", - "references": [], - "updated_at": "2019-09-20T17:30:22.950Z" - } - } -} - -{ - "type": "doc", - "value": { - "index": ".kibana", - "id": "policies:2", - "source": { - "policies": { - "name": "Policy", - "description": "Amazing policy", - "status": "active", - "updated_on": "2019-09-20T17:35:09+0000", - "updated_by": "nchaulet" - }, - "type": "policies", - "references": [], - "updated_at": "2019-09-20T17:30:22.950Z" - } - } -} - -{ - "type": "doc", - "value": { - "index": ".kibana", - "id": "policies:3", - "source": { - "policies": { - "name": "Policy 3", - "description": "Amazing policy", - "status": "active", - "updated_on": "2019-09-20T17:35:09+0000", - "updated_by": "nchaulet" - }, - "type": "policies", - "references": [], - "updated_at": "2019-09-20T17:30:22.950Z" - } - } -} diff --git a/x-pack/test/functional/es_archives/ingest/policies/mappings.json b/x-pack/test/functional/es_archives/ingest/policies/mappings.json deleted file mode 100644 index 878d6aa58c225..0000000000000 --- a/x-pack/test/functional/es_archives/ingest/policies/mappings.json +++ /dev/null @@ -1,1545 +0,0 @@ -{ - "type": "index", - "value": { - "aliases": { - ".kibana": {} - }, - "index": ".kibana_1", - "mappings": { - "dynamic": "strict", - "_meta": { - "migrationMappingPropertyHashes": { - "ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9", - "server": "ec97f1c5da1a19609a60874e5af1100c", - "visualization": "52d7a13ad68a150c4525b292d23e12cc", - "references": "7997cf5a56cc02bdc9c93361bde732b0", - "graph-workspace": "cd7ba1330e6682e9cc00b78850874be1", - "siem-ui-timeline-note": "8874706eedc49059d4cf0f5094559084", - "policies": "1a096b98c98c2efebfdba77cefcfe54a", - "type": "2f4316de49999235636386fe51dc06c1", - "lens": "21c3ea0763beb1ecb0162529706b88c5", - "space": "c5ca8acafa0beaa4d08d014a97b6bc6b", - "infrastructure-ui-source": "ddc0ecb18383f6b26101a2fadb2dab0c", - "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", - "search": "181661168bbadd1eff5902361e2a0d5c", - "updated_at": "00da57df13e94e9d98437d13ace4bfe0", - "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231", - "map": "23d7aa4a720d4938ccde3983f87bd58d", - "dashboard": "d00f614b29a80360e1190193fd333bab", - "apm-services-telemetry": "07ee1939fa4302c62ddc052ec03fed90", - "metrics-explorer-view": "53c5365793677328df0ccb6138bf3cdd", - "epm": "abf5b64aa599932bd181efc86dce14a7", - "siem-ui-timeline": "6485ab095be8d15246667b98a1a34295", - "agent_events": "8060c5567d33f6697164e1fd5c81b8ed", - "file-upload-telemetry": "0ed4d3e1983d1217a30982630897092e", - "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", - "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", - "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", - "url": "c7f66a0df8b1b52f17c28c4adb111105", - "apm-indices": "c69b68f3fe2bf27b4788d4191c1d6011", - "agents": "1c8e942384219bd899f381fd40e407d7", - "migrationVersion": "4a1746014a75ade3a714e1db5763276f", - "inventory-view": "84b320fd67209906333ffce261128462", - "enrollment_api_keys": "90e66b79e8e948e9c15434fdb3ae576e", - "upgrade-assistant-reindex-operation": "a53a20fe086b72c9a86da3cc12dad8a6", - "index-pattern": "66eccb05066c5a89924f48a9e9736499", - "canvas-element": "7390014e1091044523666d97247392fc", - "datasources": "2fed9e9883b9622cd59a73ee5550ef4f", - "maps-telemetry": "a4229f8b16a6820c6d724b7e0c1f729d", - "namespace": "2f4316de49999235636386fe51dc06c1", - "telemetry": "358ffaa88ba34a97d55af0933a117de4", - "siem-ui-timeline-pinned-event": "20638091112f0e14f0e443d512301c29", - "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", - "config": "87aca8fdb053154f11383fce3dbf3edf", - "upgrade-assistant-telemetry": "56702cec857e0a9dacfb696655b4ff7b", - "lens-ui-telemetry": "509bfa5978586998e05f9e303c07a327" - } - }, - "properties": { - "agent_events": { - "properties": { - "agent_id": { - "type": "keyword" - }, - "data": { - "type": "text" - }, - "message": { - "type": "text" - }, - "payload": { - "type": "text" - }, - "subtype": { - "type": "keyword" - }, - "timestamp": { - "type": "date" - }, - "type": { - "type": "keyword" - } - } - }, - "agents": { - "properties": { - "access_api_key_id": { - "type": "keyword" - }, - "actions": { - "type": "nested", - "properties": { - "created_at": { - "type": "date" - }, - "data": { - "type": "text" - }, - "id": { - "type": "keyword" - }, - "sent_at": { - "type": "date" - }, - "type": { - "type": "keyword" - } - } - }, - "active": { - "type": "boolean" - }, - "enrolled_at": { - "type": "date" - }, - "last_checkin": { - "type": "date" - }, - "last_updated": { - "type": "date" - }, - "local_metadata": { - "type": "text" - }, - "config_id": { - "type": "keyword" - }, - "shared_id": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "updated_at": { - "type": "date" - }, - "user_provided_metadata": { - "type": "text" - }, - "version": { - "type": "keyword" - } - } - }, - "apm-indices": { - "properties": { - "apm_oss": { - "properties": { - "apmAgentConfigurationIndex": { - "type": "keyword" - }, - "errorIndices": { - "type": "keyword" - }, - "metricsIndices": { - "type": "keyword" - }, - "onboardingIndices": { - "type": "keyword" - }, - "sourcemapIndices": { - "type": "keyword" - }, - "spanIndices": { - "type": "keyword" - }, - "transactionIndices": { - "type": "keyword" - } - } - } - } - }, - "apm-services-telemetry": { - "properties": { - "has_any_services": { - "type": "boolean" - }, - "services_per_agent": { - "properties": { - "dotnet": { - "type": "long", - "null_value": 0 - }, - "go": { - "type": "long", - "null_value": 0 - }, - "java": { - "type": "long", - "null_value": 0 - }, - "js-base": { - "type": "long", - "null_value": 0 - }, - "nodejs": { - "type": "long", - "null_value": 0 - }, - "python": { - "type": "long", - "null_value": 0 - }, - "ruby": { - "type": "long", - "null_value": 0 - }, - "rum-js": { - "type": "long", - "null_value": 0 - } - } - } - } - }, - "canvas-element": { - "dynamic": "false", - "properties": { - "@created": { - "type": "date" - }, - "@timestamp": { - "type": "date" - }, - "content": { - "type": "text" - }, - "help": { - "type": "text" - }, - "image": { - "type": "text" - }, - "name": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword" - } - } - } - } - }, - "canvas-workpad": { - "dynamic": "false", - "properties": { - "@created": { - "type": "date" - }, - "@timestamp": { - "type": "date" - }, - "name": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword" - } - } - } - } - }, - "config": { - "dynamic": "true", - "properties": { - "buildNum": { - "type": "keyword" - } - } - }, - "dashboard": { - "properties": { - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "optionsJSON": { - "type": "text" - }, - "panelsJSON": { - "type": "text" - }, - "refreshInterval": { - "properties": { - "display": { - "type": "keyword" - }, - "pause": { - "type": "boolean" - }, - "section": { - "type": "integer" - }, - "value": { - "type": "integer" - } - } - }, - "timeFrom": { - "type": "keyword" - }, - "timeRestore": { - "type": "boolean" - }, - "timeTo": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "datasources": { - "properties": { - "id": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "package": { - "properties": { - "assets": { - "properties": { - "id": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - } - }, - "description": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "title": { - "type": "keyword" - }, - "version": { - "type": "keyword" - } - } - }, - "read_alias": { - "type": "keyword" - }, - "streams": { - "properties": { - "config": { - "type": "flattened" - }, - "id": { - "type": "keyword" - }, - "input": { - "properties": { - "config": { - "type": "flattened" - }, - "fields": { - "type": "flattened" - }, - "id": { - "type": "keyword" - }, - "ilm_policy": { - "type": "keyword" - }, - "index_template": { - "type": "keyword" - }, - "ingest_pipelines": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - } - }, - "output_id": { - "type": "keyword" - }, - "processors": { - "type": "keyword" - } - } - } - } - }, - "enrollment_api_keys": { - "properties": { - "active": { - "type": "boolean" - }, - "api_key": { - "type": "binary" - }, - "api_key_id": { - "type": "keyword" - }, - "created_at": { - "type": "date" - }, - "enrollment_rules": { - "type": "nested", - "properties": { - "created_at": { - "type": "date" - }, - "id": { - "type": "keyword" - }, - "ip_ranges": { - "type": "keyword" - }, - "types": { - "type": "keyword" - }, - "updated_at": { - "type": "date" - }, - "window_duration": { - "type": "nested", - "properties": { - "from": { - "type": "date" - }, - "to": { - "type": "date" - } - } - } - } - }, - "expire_at": { - "type": "date" - }, - "name": { - "type": "keyword" - }, - "config_id": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "updated_at": { - "type": "date" - } - } - }, - "epm": { - "properties": { - "installed": { - "type": "nested", - "properties": { - "id": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - } - } - } - }, - "file-upload-telemetry": { - "properties": { - "filesUploadedTotalCount": { - "type": "long" - } - } - }, - "graph-workspace": { - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "numLinks": { - "type": "integer" - }, - "numVertices": { - "type": "integer" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "wsState": { - "type": "text" - } - } - }, - "index-pattern": { - "properties": { - "fieldFormatMap": { - "type": "text" - }, - "fields": { - "type": "text" - }, - "intervalName": { - "type": "keyword" - }, - "notExpandable": { - "type": "boolean" - }, - "sourceFilters": { - "type": "text" - }, - "timeFieldName": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "type": { - "type": "keyword" - }, - "typeMeta": { - "type": "keyword" - } - } - }, - "infrastructure-ui-source": { - "properties": { - "description": { - "type": "text" - }, - "fields": { - "properties": { - "container": { - "type": "keyword" - }, - "host": { - "type": "keyword" - }, - "pod": { - "type": "keyword" - }, - "tiebreaker": { - "type": "keyword" - }, - "timestamp": { - "type": "keyword" - } - } - }, - "logAlias": { - "type": "keyword" - }, - "logColumns": { - "type": "nested", - "properties": { - "fieldColumn": { - "properties": { - "field": { - "type": "keyword" - }, - "id": { - "type": "keyword" - } - } - }, - "messageColumn": { - "properties": { - "id": { - "type": "keyword" - } - } - }, - "timestampColumn": { - "properties": { - "id": { - "type": "keyword" - } - } - } - } - }, - "metricAlias": { - "type": "keyword" - }, - "name": { - "type": "text" - } - } - }, - "inventory-view": { - "properties": { - "autoBounds": { - "type": "boolean" - }, - "autoReload": { - "type": "boolean" - }, - "boundsOverride": { - "properties": { - "max": { - "type": "integer" - }, - "min": { - "type": "integer" - } - } - }, - "customOptions": { - "type": "nested", - "properties": { - "field": { - "type": "keyword" - }, - "text": { - "type": "keyword" - } - } - }, - "filterQuery": { - "properties": { - "expression": { - "type": "keyword" - }, - "kind": { - "type": "keyword" - } - } - }, - "groupBy": { - "type": "nested", - "properties": { - "field": { - "type": "keyword" - }, - "label": { - "type": "keyword" - } - } - }, - "metric": { - "properties": { - "type": { - "type": "keyword" - } - } - }, - "name": { - "type": "keyword" - }, - "nodeType": { - "type": "keyword" - }, - "time": { - "type": "integer" - }, - "view": { - "type": "keyword" - } - } - }, - "kql-telemetry": { - "properties": { - "optInCount": { - "type": "long" - }, - "optOutCount": { - "type": "long" - } - } - }, - "lens": { - "properties": { - "expression": { - "type": "keyword", - "index": false - }, - "state": { - "type": "flattened" - }, - "title": { - "type": "text" - }, - "visualizationType": { - "type": "keyword" - } - } - }, - "lens-ui-telemetry": { - "properties": { - "count": { - "type": "integer" - }, - "date": { - "type": "date" - }, - "name": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - } - }, - "map": { - "properties": { - "bounds": { - "type": "geo_shape" - }, - "description": { - "type": "text" - }, - "layerListJSON": { - "type": "text" - }, - "mapStateJSON": { - "type": "text" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "maps-telemetry": { - "properties": { - "attributesPerMap": { - "properties": { - "dataSourcesCount": { - "properties": { - "avg": { - "type": "long" - }, - "max": { - "type": "long" - }, - "min": { - "type": "long" - } - } - }, - "emsVectorLayersCount": { - "type": "object", - "dynamic": "true" - }, - "layerTypesCount": { - "type": "object", - "dynamic": "true" - }, - "layersCount": { - "properties": { - "avg": { - "type": "long" - }, - "max": { - "type": "long" - }, - "min": { - "type": "long" - } - } - } - } - }, - "mapsTotalCount": { - "type": "long" - }, - "timeCaptured": { - "type": "date" - } - } - }, - "metrics-explorer-view": { - "properties": { - "chartOptions": { - "properties": { - "stack": { - "type": "boolean" - }, - "type": { - "type": "keyword" - }, - "yAxisMode": { - "type": "keyword" - } - } - }, - "currentTimerange": { - "properties": { - "from": { - "type": "keyword" - }, - "interval": { - "type": "keyword" - }, - "to": { - "type": "keyword" - } - } - }, - "name": { - "type": "keyword" - }, - "options": { - "properties": { - "aggregation": { - "type": "keyword" - }, - "filterQuery": { - "type": "keyword" - }, - "groupBy": { - "type": "keyword" - }, - "limit": { - "type": "integer" - }, - "metrics": { - "type": "nested", - "properties": { - "aggregation": { - "type": "keyword" - }, - "color": { - "type": "keyword" - }, - "field": { - "type": "keyword" - }, - "label": { - "type": "keyword" - } - } - } - } - } - } - }, - "migrationVersion": { - "dynamic": "true", - "properties": { - "space": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - } - } - }, - "ml-telemetry": { - "properties": { - "file_data_visualizer": { - "properties": { - "index_creation_count": { - "type": "long" - } - } - } - } - }, - "namespace": { - "type": "keyword" - }, - "policies": { - "properties": { - "datasources": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "id": { - "type": "keyword" - }, - "label": { - "type": "keyword" - }, - "name": { - "type": "text" - }, - "status": { - "type": "keyword" - }, - "updated_by": { - "type": "keyword" - }, - "updated_on": { - "type": "keyword" - } - } - }, - "query": { - "properties": { - "description": { - "type": "text" - }, - "filters": { - "type": "object", - "enabled": false - }, - "query": { - "properties": { - "language": { - "type": "keyword" - }, - "query": { - "type": "keyword", - "index": false - } - } - }, - "timefilter": { - "type": "object", - "enabled": false - }, - "title": { - "type": "text" - } - } - }, - "references": { - "type": "nested", - "properties": { - "id": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "type": { - "type": "keyword" - } - } - }, - "sample-data-telemetry": { - "properties": { - "installCount": { - "type": "long" - }, - "unInstallCount": { - "type": "long" - } - } - }, - "search": { - "properties": { - "columns": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "sort": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "server": { - "properties": { - "uuid": { - "type": "keyword" - } - } - }, - "siem-ui-timeline": { - "properties": { - "columns": { - "properties": { - "aggregatable": { - "type": "boolean" - }, - "category": { - "type": "keyword" - }, - "columnHeaderType": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "example": { - "type": "text" - }, - "id": { - "type": "keyword" - }, - "indexes": { - "type": "keyword" - }, - "name": { - "type": "text" - }, - "placeholder": { - "type": "text" - }, - "searchable": { - "type": "boolean" - }, - "type": { - "type": "keyword" - } - } - }, - "created": { - "type": "date" - }, - "createdBy": { - "type": "text" - }, - "dataProviders": { - "properties": { - "and": { - "properties": { - "enabled": { - "type": "boolean" - }, - "excluded": { - "type": "boolean" - }, - "id": { - "type": "keyword" - }, - "kqlQuery": { - "type": "text" - }, - "name": { - "type": "text" - }, - "queryMatch": { - "properties": { - "displayField": { - "type": "text" - }, - "displayValue": { - "type": "text" - }, - "field": { - "type": "text" - }, - "operator": { - "type": "text" - }, - "value": { - "type": "text" - } - } - } - } - }, - "enabled": { - "type": "boolean" - }, - "excluded": { - "type": "boolean" - }, - "id": { - "type": "keyword" - }, - "kqlQuery": { - "type": "text" - }, - "name": { - "type": "text" - }, - "queryMatch": { - "properties": { - "displayField": { - "type": "text" - }, - "displayValue": { - "type": "text" - }, - "field": { - "type": "text" - }, - "operator": { - "type": "text" - }, - "value": { - "type": "text" - } - } - } - } - }, - "dateRange": { - "properties": { - "end": { - "type": "date" - }, - "start": { - "type": "date" - } - } - }, - "description": { - "type": "text" - }, - "favorite": { - "properties": { - "favoriteDate": { - "type": "date" - }, - "fullName": { - "type": "text" - }, - "keySearch": { - "type": "text" - }, - "userName": { - "type": "text" - } - } - }, - "filters": { - "properties": { - "exists": { - "type": "text" - }, - "match_all": { - "type": "text" - }, - "meta": { - "properties": { - "alias": { - "type": "text" - }, - "controlledBy": { - "type": "text" - }, - "disabled": { - "type": "boolean" - }, - "field": { - "type": "text" - }, - "formattedValue": { - "type": "text" - }, - "index": { - "type": "keyword" - }, - "key": { - "type": "keyword" - }, - "negate": { - "type": "boolean" - }, - "params": { - "type": "text" - }, - "type": { - "type": "keyword" - }, - "value": { - "type": "text" - } - } - }, - "missing": { - "type": "text" - }, - "query": { - "type": "text" - }, - "range": { - "type": "text" - }, - "script": { - "type": "text" - } - } - }, - "kqlMode": { - "type": "keyword" - }, - "kqlQuery": { - "properties": { - "filterQuery": { - "properties": { - "kuery": { - "properties": { - "expression": { - "type": "text" - }, - "kind": { - "type": "keyword" - } - } - }, - "serializedQuery": { - "type": "text" - } - } - } - } - }, - "savedQueryId": { - "type": "keyword" - }, - "sort": { - "properties": { - "columnId": { - "type": "keyword" - }, - "sortDirection": { - "type": "keyword" - } - } - }, - "title": { - "type": "text" - }, - "updated": { - "type": "date" - }, - "updatedBy": { - "type": "text" - } - } - }, - "siem-ui-timeline-note": { - "properties": { - "created": { - "type": "date" - }, - "createdBy": { - "type": "text" - }, - "eventId": { - "type": "keyword" - }, - "note": { - "type": "text" - }, - "timelineId": { - "type": "keyword" - }, - "updated": { - "type": "date" - }, - "updatedBy": { - "type": "text" - } - } - }, - "siem-ui-timeline-pinned-event": { - "properties": { - "created": { - "type": "date" - }, - "createdBy": { - "type": "text" - }, - "eventId": { - "type": "keyword" - }, - "timelineId": { - "type": "keyword" - }, - "updated": { - "type": "date" - }, - "updatedBy": { - "type": "text" - } - } - }, - "space": { - "properties": { - "_reserved": { - "type": "boolean" - }, - "color": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "disabledFeatures": { - "type": "keyword" - }, - "imageUrl": { - "type": "text", - "index": false - }, - "initials": { - "type": "keyword" - }, - "name": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 2048 - } - } - } - } - }, - "telemetry": { - "properties": { - "enabled": { - "type": "boolean" - }, - "lastReported": { - "type": "date" - }, - "lastVersionChecked": { - "type": "keyword", - "ignore_above": 256 - }, - "sendUsageFrom": { - "type": "keyword", - "ignore_above": 256 - }, - "userHasSeenNotice": { - "type": "boolean" - } - } - }, - "timelion-sheet": { - "properties": { - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "timelion_chart_height": { - "type": "integer" - }, - "timelion_columns": { - "type": "integer" - }, - "timelion_interval": { - "type": "keyword" - }, - "timelion_other_interval": { - "type": "keyword" - }, - "timelion_rows": { - "type": "integer" - }, - "timelion_sheet": { - "type": "text" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "type": { - "type": "keyword" - }, - "ui-metric": { - "properties": { - "count": { - "type": "integer" - } - } - }, - "updated_at": { - "type": "date" - }, - "upgrade-assistant-reindex-operation": { - "dynamic": "true", - "properties": { - "indexName": { - "type": "keyword" - }, - "status": { - "type": "integer" - } - } - }, - "upgrade-assistant-telemetry": { - "properties": { - "features": { - "properties": { - "deprecation_logging": { - "properties": { - "enabled": { - "type": "boolean", - "null_value": true - } - } - } - } - }, - "ui_open": { - "properties": { - "cluster": { - "type": "long", - "null_value": 0 - }, - "indices": { - "type": "long", - "null_value": 0 - }, - "overview": { - "type": "long", - "null_value": 0 - } - } - }, - "ui_reindex": { - "properties": { - "close": { - "type": "long", - "null_value": 0 - }, - "open": { - "type": "long", - "null_value": 0 - }, - "start": { - "type": "long", - "null_value": 0 - }, - "stop": { - "type": "long", - "null_value": 0 - } - } - } - } - }, - "url": { - "properties": { - "accessCount": { - "type": "long" - }, - "accessDate": { - "type": "date" - }, - "createDate": { - "type": "date" - }, - "url": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 2048 - } - } - } - } - }, - "visualization": { - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "savedSearchRefName": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "uiStateJSON": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "visState": { - "type": "text" - } - } - } - } - }, - "settings": { - "index": { - "number_of_replicas": "0", - "number_of_shards": "1" - } - } - } -} diff --git a/x-pack/test/security_solution_cypress/es_archives/custom_rule_with_timeline/mappings.json b/x-pack/test/security_solution_cypress/es_archives/custom_rule_with_timeline/mappings.json index d01e6344bcfaf..693878a88f899 100644 --- a/x-pack/test/security_solution_cypress/es_archives/custom_rule_with_timeline/mappings.json +++ b/x-pack/test/security_solution_cypress/es_archives/custom_rule_with_timeline/mappings.json @@ -34,7 +34,7 @@ "index-pattern": "66eccb05066c5a89924f48a9e9736499", "infrastructure-ui-source": "ddc0ecb18383f6b26101a2fadb2dab0c", "ingest-agent-configs": "d9a5cbdce8e937f674a7b376c47a34a1", - "ingest-datasources": "c0fe6347b0eebcbf421841669e3acd31", + "ingest-package-configs": "c0fe6347b0eebcbf421841669e3acd31", "ingest-outputs": "0e57221778a7153c8292edf154099036", "ingest_manager_settings": "c5b0749b4ab03c582efd4c14cb8f132c", "inventory-view": "88fc7e12fd1b45b6f0787323ce4f18d2", @@ -2103,7 +2103,7 @@ } } }, - "ingest-datasources": { + "ingest-package-configs": { "properties": { "config_id": { "type": "keyword" @@ -2606,7 +2606,7 @@ }, "type": "text" }, - "ingest-datasources": { + "ingest-package-configs": { "fields": { "keyword": { "ignore_above": 256, diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts index f0e47c6886601..45ea82c59bf97 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts @@ -14,7 +14,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { 'endpoint', 'policy', 'endpointPageUtils', - 'ingestManagerCreateDatasource', + 'ingestManagerCreatePackageConfig', ]); const testSubjects = getService('testSubjects'); const policyTestResources = getService('policyTestResources'); @@ -27,7 +27,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await pageObjects.policy.navigateToPolicyDetails('invalid-id'); await testSubjects.existOrFail('policyDetailsIdNotFoundMessage'); expect(await testSubjects.getVisibleText('policyDetailsIdNotFoundMessage')).to.equal( - 'Saved object [ingest-datasources/invalid-id] not found' + 'Saved object [ingest-package-configs/invalid-id] not found' ); }); }); @@ -37,7 +37,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { before(async () => { policyInfo = await policyTestResources.createPolicy(); - await pageObjects.policy.navigateToPolicyDetails(policyInfo.datasource.id); + await pageObjects.policy.navigateToPolicyDetails(policyInfo.packageConfig.id); }); after(async () => { @@ -48,7 +48,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('should display policy view', async () => { expect(await testSubjects.getVisibleText('pageViewHeaderLeftTitle')).to.equal( - policyInfo.datasource.name + policyInfo.packageConfig.name ); }); }); @@ -58,7 +58,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { beforeEach(async () => { policyInfo = await policyTestResources.createPolicy(); - await pageObjects.policy.navigateToPolicyDetails(policyInfo.datasource.id); + await pageObjects.policy.navigateToPolicyDetails(policyInfo.packageConfig.id); }); afterEach(async () => { @@ -73,7 +73,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await testSubjects.existOrFail('policyDetailsSuccessMessage'); expect(await testSubjects.getVisibleText('policyDetailsSuccessMessage')).to.equal( - `Policy ${policyInfo.datasource.name} has been updated.` + `Policy ${policyInfo.packageConfig.name} has been updated.` ); }); it('should persist update on the screen', async () => { @@ -82,7 +82,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await testSubjects.existOrFail('policyDetailsSuccessMessage'); await pageObjects.policy.navigateToPolicyList(); - await pageObjects.policy.navigateToPolicyDetails(policyInfo.datasource.id); + await pageObjects.policy.navigateToPolicyDetails(policyInfo.packageConfig.id); expect(await (await testSubjects.find('policyWindowsEvent_process')).isSelected()).to.equal( false @@ -107,7 +107,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expect(agentFullConfig).to.eql({ inputs: [ { - id: policyInfo.datasource.id, + id: policyInfo.packageConfig.id, dataset: { namespace: 'default' }, name: 'Protect East Coast', meta: { @@ -194,14 +194,14 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); }); - describe('when on Ingest Configurations Edit Datasource page', async () => { + describe('when on Ingest Configurations Edit Package Config page', async () => { let policyInfo: PolicyTestResourceInfo; beforeEach(async () => { // Create a policy and navigate to Ingest app policyInfo = await policyTestResources.createPolicy(); - await pageObjects.ingestManagerCreateDatasource.navigateToAgentConfigEditDatasource( + await pageObjects.ingestManagerCreatePackageConfig.navigateToAgentConfigEditPackageConfig( policyInfo.agentConfig.id, - policyInfo.datasource.id + policyInfo.packageConfig.id ); }); afterEach(async () => { diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts index 941a100416740..57321ab4cd911 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts @@ -13,7 +13,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { 'endpoint', 'policy', 'endpointPageUtils', - 'ingestManagerCreateDatasource', + 'ingestManagerCreatePackageConfig', ]); const testSubjects = getService('testSubjects'); const policyTestResources = getService('policyTestResources'); @@ -78,7 +78,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { 'Protect East Coastrev. 1', 'elastic', 'elastic', - `${policyInfo.datasource.package?.title} v${policyInfo.datasource.package?.version}`, + `${policyInfo.packageConfig.package?.title} v${policyInfo.packageConfig.package?.version}`, '', ]); [policyRow[2], policyRow[4]].forEach((relativeDate) => { @@ -111,42 +111,42 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await (await pageObjects.policy.findHeaderCreateNewButton()).click(); }); - it('should redirect to ingest management integrations add datasource', async () => { - await pageObjects.ingestManagerCreateDatasource.ensureOnCreatePageOrFail(); + it('should redirect to ingest management integrations add package config', async () => { + await pageObjects.ingestManagerCreatePackageConfig.ensureOnCreatePageOrFail(); }); it('should redirect user back to Policy List if Cancel button is clicked', async () => { - await (await pageObjects.ingestManagerCreateDatasource.findCancelButton()).click(); + await (await pageObjects.ingestManagerCreatePackageConfig.findCancelButton()).click(); await pageObjects.policy.ensureIsOnPolicyPage(); }); it('should redirect user back to Policy List if Back link is clicked', async () => { - await (await pageObjects.ingestManagerCreateDatasource.findBackLink()).click(); + await (await pageObjects.ingestManagerCreatePackageConfig.findBackLink()).click(); await pageObjects.policy.ensureIsOnPolicyPage(); }); it('should display custom endpoint configuration message', async () => { - await pageObjects.ingestManagerCreateDatasource.selectAgentConfig(); - const endpointConfig = await pageObjects.policy.findDatasourceEndpointCustomConfiguration(); + await pageObjects.ingestManagerCreatePackageConfig.selectAgentConfig(); + const endpointConfig = await pageObjects.policy.findPackageConfigEndpointCustomConfiguration(); expect(endpointConfig).not.to.be(undefined); }); it('should redirect user back to Policy List after a successful save', async () => { const newPolicyName = `endpoint policy ${Date.now()}`; - await pageObjects.ingestManagerCreateDatasource.selectAgentConfig(); - await pageObjects.ingestManagerCreateDatasource.setDatasourceName(newPolicyName); - await (await pageObjects.ingestManagerCreateDatasource.findDSaveButton()).click(); - await pageObjects.ingestManagerCreateDatasource.waitForSaveSuccessNotification(); + await pageObjects.ingestManagerCreatePackageConfig.selectAgentConfig(); + await pageObjects.ingestManagerCreatePackageConfig.setPackageConfigName(newPolicyName); + await (await pageObjects.ingestManagerCreatePackageConfig.findDSaveButton()).click(); + await pageObjects.ingestManagerCreatePackageConfig.waitForSaveSuccessNotification(); await pageObjects.policy.ensureIsOnPolicyPage(); await policyTestResources.deletePolicyByName(newPolicyName); }); }); describe('and user clicks on page header create button', () => { - it('should direct users to the ingest management integrations add datasource', async () => { + it('should direct users to the ingest management integrations add package config', async () => { await pageObjects.policy.navigateToPolicyList(); await (await pageObjects.policy.findOnboardingStartButton()).click(); - await pageObjects.ingestManagerCreateDatasource.ensureOnCreatePageOrFail(); + await pageObjects.ingestManagerCreatePackageConfig.ensureOnCreatePageOrFail(); }); }); }); diff --git a/x-pack/test/security_solution_endpoint/page_objects/index.ts b/x-pack/test/security_solution_endpoint/page_objects/index.ts index 2d27082ff5949..68e1ad00619c7 100644 --- a/x-pack/test/security_solution_endpoint/page_objects/index.ts +++ b/x-pack/test/security_solution_endpoint/page_objects/index.ts @@ -8,12 +8,12 @@ import { pageObjects as xpackFunctionalPageObjects } from '../../functional/page import { EndpointPageProvider } from './endpoint_page'; import { EndpointPolicyPageProvider } from './policy_page'; import { EndpointPageUtils } from './page_utils'; -import { IngestManagerCreateDatasource } from './ingest_manager_create_datasource_page'; +import { IngestManagerCreatePackageConfig } from './ingest_manager_create_package_config_page'; export const pageObjects = { ...xpackFunctionalPageObjects, endpoint: EndpointPageProvider, policy: EndpointPolicyPageProvider, endpointPageUtils: EndpointPageUtils, - ingestManagerCreateDatasource: IngestManagerCreateDatasource, + ingestManagerCreatePackageConfig: IngestManagerCreatePackageConfig, }; diff --git a/x-pack/test/security_solution_endpoint/page_objects/ingest_manager_create_datasource_page.ts b/x-pack/test/security_solution_endpoint/page_objects/ingest_manager_create_package_config_page.ts similarity index 66% rename from x-pack/test/security_solution_endpoint/page_objects/ingest_manager_create_datasource_page.ts rename to x-pack/test/security_solution_endpoint/page_objects/ingest_manager_create_package_config_page.ts index e104b8701276c..dd3fc637a3d6c 100644 --- a/x-pack/test/security_solution_endpoint/page_objects/ingest_manager_create_datasource_page.ts +++ b/x-pack/test/security_solution_endpoint/page_objects/ingest_manager_create_package_config_page.ts @@ -6,38 +6,41 @@ import { FtrProviderContext } from '../ftr_provider_context'; -export function IngestManagerCreateDatasource({ getService, getPageObjects }: FtrProviderContext) { +export function IngestManagerCreatePackageConfig({ + getService, + getPageObjects, +}: FtrProviderContext) { const testSubjects = getService('testSubjects'); const find = getService('find'); const pageObjects = getPageObjects(['common']); return { /** - * Validates that the page shown is the Datasource Create Page + * Validates that the page shown is the Package Config Create Page */ async ensureOnCreatePageOrFail() { - await testSubjects.existOrFail('createDataSource_header'); + await testSubjects.existOrFail('createPackageConfig_header'); }, /** * Finds and returns the Cancel button on the sticky bottom bar */ async findCancelButton() { - return await testSubjects.find('createDatasourceCancelButton'); + return await testSubjects.find('createPackageConfigCancelButton'); }, /** * Finds and returns the Cancel back link at the top of the create page */ async findBackLink() { - return await testSubjects.find('createDataSource_cancelBackLink'); + return await testSubjects.find('createPackageConfig_cancelBackLink'); }, /** * Finds and returns the save button on the sticky bottom bar */ async findDSaveButton() { - return await testSubjects.find('createDatasourceSaveButton'); + return await testSubjects.find('createPackageConfigSaveButton'); }, /** @@ -60,36 +63,36 @@ export function IngestManagerCreateDatasource({ getService, getPageObjects }: Ft }, /** - * Set the name of the datasource on the input field + * Set the name of the package config on the input field * @param name */ - async setDatasourceName(name: string) { + async setPackageConfigName(name: string) { // Because of the bottom sticky bar, we need to scroll section 2 into view // so that `setValue()` enters the data on the input field. await testSubjects.scrollIntoView('dataCollectionSetupStep'); - await testSubjects.setValue('datasourceNameInput', name); + await testSubjects.setValue('packageConfigNameInput', name); }, /** * Waits for the save Notification toast to be visible */ async waitForSaveSuccessNotification() { - await testSubjects.existOrFail('datasourceCreateSuccessToast'); + await testSubjects.existOrFail('packageConfigCreateSuccessToast'); }, /** - * Validates that the page shown is the Datasource Edit Page + * Validates that the page shown is the Package Config Edit Page */ async ensureOnEditPageOrFail() { - await testSubjects.existOrFail('editDataSource_header'); + await testSubjects.existOrFail('editPackageConfig_header'); }, /** - * Navigates to the Ingest Agent configuration Edit Datasource page + * Navigates to the Ingest Agent configuration Edit Package Config page */ - async navigateToAgentConfigEditDatasource(agentConfigId: string, datasourceId: string) { + async navigateToAgentConfigEditPackageConfig(agentConfigId: string, packageConfigId: string) { await pageObjects.common.navigateToApp('ingestManager', { - hash: `/configs/${agentConfigId}/edit-datasource/${datasourceId}`, + hash: `/configs/${agentConfigId}/edit-integration/${packageConfigId}`, }); await this.ensureOnEditPageOrFail(); }, diff --git a/x-pack/test/security_solution_endpoint/page_objects/policy_page.ts b/x-pack/test/security_solution_endpoint/page_objects/policy_page.ts index d07c4e70f2687..b20e7c7c05e64 100644 --- a/x-pack/test/security_solution_endpoint/page_objects/policy_page.ts +++ b/x-pack/test/security_solution_endpoint/page_objects/policy_page.ts @@ -98,12 +98,12 @@ export function EndpointPolicyPageProvider({ getService, getPageObjects }: FtrPr }, /** - * Used when looking a the Ingest create/edit datasource pages. Finds the endpoint + * Used when looking a the Ingest create/edit package config pages. Finds the endpoint * custom configuaration component * @param onEditPage */ - async findDatasourceEndpointCustomConfiguration(onEditPage: boolean = false) { - return await testSubjects.find(`endpointDatasourceConfig_${onEditPage ? 'edit' : 'create'}`); + async findPackageConfigEndpointCustomConfiguration(onEditPage: boolean = false) { + return await testSubjects.find(`endpointPackageConfig_${onEditPage ? 'edit' : 'create'}`); }, /** diff --git a/x-pack/test/security_solution_endpoint/services/endpoint_policy.ts b/x-pack/test/security_solution_endpoint/services/endpoint_policy.ts index fbed7dcc663ec..df7d117e91dcb 100644 --- a/x-pack/test/security_solution_endpoint/services/endpoint_policy.ts +++ b/x-pack/test/security_solution_endpoint/services/endpoint_policy.ts @@ -3,17 +3,16 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - import { FtrProviderContext } from '../ftr_provider_context'; import { CreateAgentConfigRequest, CreateAgentConfigResponse, - CreateDatasourceRequest, - CreateDatasourceResponse, - DATASOURCE_SAVED_OBJECT_TYPE, + CreatePackageConfigRequest, + CreatePackageConfigResponse, + PACKAGE_CONFIG_SAVED_OBJECT_TYPE, DeleteAgentConfigRequest, - DeleteDatasourcesRequest, - GetDatasourcesResponse, + DeletePackageConfigsRequest, + GetPackageConfigsResponse, GetFullAgentConfigResponse, GetPackagesResponse, } from '../../../plugins/ingest_manager/common'; @@ -23,8 +22,8 @@ import { Immutable } from '../../../plugins/security_solution/common/endpoint/ty const INGEST_API_ROOT = '/api/ingest_manager'; const INGEST_API_AGENT_CONFIGS = `${INGEST_API_ROOT}/agent_configs`; const INGEST_API_AGENT_CONFIGS_DELETE = `${INGEST_API_AGENT_CONFIGS}/delete`; -const INGEST_API_DATASOURCES = `${INGEST_API_ROOT}/datasources`; -const INGEST_API_DATASOURCES_DELETE = `${INGEST_API_DATASOURCES}/delete`; +const INGEST_API_PACKAGE_CONFIGS = `${INGEST_API_ROOT}/package_configs`; +const INGEST_API_PACKAGE_CONFIGS_DELETE = `${INGEST_API_PACKAGE_CONFIGS}/delete`; const INGEST_API_EPM_PACKAGES = `${INGEST_API_ROOT}/epm/packages`; const SECURITY_PACKAGES_ROUTE = `${INGEST_API_EPM_PACKAGES}?category=security`; @@ -35,15 +34,15 @@ const SECURITY_PACKAGES_ROUTE = `${INGEST_API_EPM_PACKAGES}?category=security`; export interface PolicyTestResourceInfo { /** The Ingest agent configuration created */ agentConfig: Immutable; - /** The Ingest datasource created and added to agent configuration. + /** The Ingest Package Config created and added to agent configuration. * This is where Endpoint Policy is stored. */ - datasource: Immutable; + packageConfig: Immutable; /** * Information about the endpoint package */ packageInfo: Immutable; - /** will clean up (delete) the objects created (agent config + datasource) */ + /** will clean up (delete) the objects created (agent config + Package Config) */ cleanup: () => Promise; } @@ -127,7 +126,7 @@ export function EndpointPolicyTestResourcesProvider({ getService }: FtrProviderC }, /** - * Creates an Ingest Agent Configuration and adds to it the Endpoint Datasource that + * Creates an Ingest Agent Configuration and adds to it the Endpoint Package Config that * stores the Policy configuration data */ async createPolicy(): Promise { @@ -152,10 +151,10 @@ export function EndpointPolicyTestResourcesProvider({ getService }: FtrProviderC // Retrieve the Endpoint package information const endpointPackageInfo = await retrieveEndpointPackageInfo(); - // create datasource and associated it to agent config - let datasource: CreateDatasourceResponse['item']; + // create Package Config and associated it to agent config + let packageConfig: CreatePackageConfigResponse['item']; try { - const newDatasourceData: CreateDatasourceRequest['body'] = { + const newPackageConfigData: CreatePackageConfigRequest['body'] = { name: 'Protect East Coast', description: 'Protect the worlds data - but in the East Coast', config_id: agentConfig!.id, @@ -180,33 +179,35 @@ export function EndpointPolicyTestResourcesProvider({ getService }: FtrProviderC version: endpointPackageInfo?.version ?? '', }, }; - const { body: createResponse }: { body: CreateDatasourceResponse } = await supertest - .post(INGEST_API_DATASOURCES) + const { + body: createResponse, + }: { body: CreatePackageConfigResponse } = await supertest + .post(INGEST_API_PACKAGE_CONFIGS) .set('kbn-xsrf', 'xxx') - .send(newDatasourceData) + .send(newPackageConfigData) .expect(200); - datasource = createResponse.item; + packageConfig = createResponse.item; } catch (error) { - return logSupertestApiErrorAndThrow(`Unable to create Datasource via Ingest!`, error); + return logSupertestApiErrorAndThrow(`Unable to create Package Config via Ingest!`, error); } return { agentConfig, - datasource, + packageConfig, packageInfo: endpointPackageInfo!, async cleanup() { - // Delete Datasource + // Delete Package Config try { - const deleteDatasourceData: DeleteDatasourcesRequest['body'] = { - datasourceIds: [datasource.id], + const deletePackageConfigData: DeletePackageConfigsRequest['body'] = { + packageConfigIds: [packageConfig.id], }; await supertest - .post(INGEST_API_DATASOURCES_DELETE) + .post(INGEST_API_PACKAGE_CONFIGS_DELETE) .set('kbn-xsrf', 'xxx') - .send(deleteDatasourceData) + .send(deletePackageConfigData) .expect(200); } catch (error) { - logSupertestApiErrorAndThrow('Unable to delete Datasource via Ingest!', error); + logSupertestApiErrorAndThrow('Unable to delete Package Config via Ingest!', error); } // Delete Agent config @@ -227,45 +228,47 @@ export function EndpointPolicyTestResourcesProvider({ getService }: FtrProviderC }, /** - * Deletes a policy (Datasource) by using the policy name + * Deletes a policy (Package Config) by using the policy name * @param name */ async deletePolicyByName(name: string) { - let datasourceList: GetDatasourcesResponse['items']; + let packageConfigList: GetPackageConfigsResponse['items']; try { - const { body: datasourcesResponse }: { body: GetDatasourcesResponse } = await supertest - .get(INGEST_API_DATASOURCES) + const { + body: packageConfigsResponse, + }: { body: GetPackageConfigsResponse } = await supertest + .get(INGEST_API_PACKAGE_CONFIGS) .set('kbn-xsrf', 'xxx') - .query({ kuery: `${DATASOURCE_SAVED_OBJECT_TYPE}.name: ${name}` }) + .query({ kuery: `${PACKAGE_CONFIG_SAVED_OBJECT_TYPE}.name: ${name}` }) .send() .expect(200); - datasourceList = datasourcesResponse.items; + packageConfigList = packageConfigsResponse.items; } catch (error) { return logSupertestApiErrorAndThrow( - `Unable to get list of datasources with name=${name}`, + `Unable to get list of Package Configs with name=${name}`, error ); } - if (datasourceList.length === 0) { + if (packageConfigList.length === 0) { throw new Error(`Policy named '${name}' was not found!`); } - if (datasourceList.length > 1) { - throw new Error(`Found ${datasourceList.length} Policies - was expecting only one!`); + if (packageConfigList.length > 1) { + throw new Error(`Found ${packageConfigList.length} Policies - was expecting only one!`); } try { - const deleteDatasourceData: DeleteDatasourcesRequest['body'] = { - datasourceIds: [datasourceList[0].id], + const deletePackageConfigData: DeletePackageConfigsRequest['body'] = { + packageConfigIds: [packageConfigList[0].id], }; await supertest - .post(INGEST_API_DATASOURCES_DELETE) + .post(INGEST_API_PACKAGE_CONFIGS_DELETE) .set('kbn-xsrf', 'xxx') - .send(deleteDatasourceData) + .send(deletePackageConfigData) .expect(200); } catch (error) { - logSupertestApiErrorAndThrow('Unable to delete Datasource via Ingest!', error); + logSupertestApiErrorAndThrow('Unable to delete Package Config via Ingest!', error); } }, }; From 6581450449c092c25a6deebb02b1f56fce3f244d Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Wed, 1 Jul 2020 20:33:57 -0400 Subject: [PATCH 16/34] [SIEM][Exceptions] - Exception builder component (#67013) ### Summary This PR creates the bulk functionality of the exception builder. The exception builder is the component that will be used to create exception list items. It does not deal with the actual API creation/deletion/update of exceptions, it does contain an `onChange` handler that can be used to access the exceptions. The builder is able to: - accept `ExceptionListItem` and render them correctly - allow user to add exception list item and exception list item entries - accept an `indexPattern` and use it to fetch relevant field and autocomplete field values - disable `Or` button if user is only allowed to edit/add to exception list item (not add additional exception list items) - displays `Add new exception` button if no exception items exist - An exception item can be created without entries, the `add new exception` button will show in the case that an exception list contains exception list item(s) with an empty `entries` array (as long as there is one exception list item with an item in `entries`, button does not show) - debounces field value autocomplete searches - bubble up exceptions to parent component, stripping out any empty entries --- .../common/schemas/common/schemas.test.ts | 33 +- .../lists/common/schemas/common/schemas.ts | 4 + ...est.tsx => persist_exception_item.test.ts} | 0 ...ion_item.tsx => persist_exception_item.ts} | 0 ...est.tsx => persist_exception_list.test.ts} | 0 ...ion_list.tsx => persist_exception_list.ts} | 0 .../{use_api.test.tsx => use_api.test.ts} | 0 .../hooks/{use_api.tsx => use_api.ts} | 0 ...st.test.tsx => use_exception_list.test.ts} | 0 ...ception_list.tsx => use_exception_list.ts} | 0 .../and_or_badge/rounded_badge_antenna.tsx | 9 +- .../components/autocomplete/field.test.tsx | 156 +++++++ .../common/components/autocomplete/field.tsx | 74 ++++ .../autocomplete/field_value_exists.test.tsx | 27 ++ .../autocomplete/field_value_exists.tsx | 27 ++ .../autocomplete/field_value_lists.test.tsx | 161 ++++++++ .../autocomplete/field_value_lists.tsx | 105 +++++ .../autocomplete/field_value_match.test.tsx | 238 +++++++++++ .../autocomplete/field_value_match.tsx | 106 +++++ .../field_value_match_any.test.tsx | 238 +++++++++++ .../autocomplete/field_value_match_any.tsx | 104 +++++ .../components/autocomplete/helpers.test.ts | 192 +++++++++ .../common/components/autocomplete/helpers.ts | 81 ++++ .../use_field_value_autocomplete.test.ts | 221 ++++++++++ .../hooks/use_field_value_autocomplete.ts | 102 +++++ .../components/autocomplete/operator.test.tsx | 197 +++++++++ .../components/autocomplete/operator.tsx | 77 ++++ .../{exceptions => autocomplete}/operators.ts | 18 +- .../common/components/autocomplete/readme.md | 122 ++++++ .../components/autocomplete/translations.ts | 11 + .../common/components/autocomplete/types.ts | 22 + .../__examples__/index.stories.tsx | 34 -- .../__snapshots__/index.test.tsx.snap | 26 -- .../autocomplete_field/index.test.tsx | 388 ------------------ .../components/autocomplete_field/index.tsx | 335 --------------- .../autocomplete_field/suggestion_item.tsx | 131 ------ .../builder_button_options.stories.tsx | 83 ++++ .../builder/builder_button_options.test.tsx | 167 ++++++++ .../builder/builder_button_options.tsx | 89 ++++ .../exceptions/builder/entry_item.tsx | 243 +++++++++++ .../exceptions/builder/exception_item.tsx | 137 +++++++ .../components/exceptions/builder/index.tsx | 248 +++++++++++ .../components/exceptions/helpers.test.tsx | 48 ++- .../common/components/exceptions/helpers.tsx | 207 ++++++++-- .../components/exceptions/translations.ts | 63 +++ .../common/components/exceptions/types.ts | 66 ++- .../exception_item/exception_entries.test.tsx | 4 +- .../public/lists_plugin_deps.ts | 9 + 48 files changed, 3623 insertions(+), 980 deletions(-) rename x-pack/plugins/lists/public/exceptions/hooks/{persist_exception_item.test.tsx => persist_exception_item.test.ts} (100%) rename x-pack/plugins/lists/public/exceptions/hooks/{persist_exception_item.tsx => persist_exception_item.ts} (100%) rename x-pack/plugins/lists/public/exceptions/hooks/{persist_exception_list.test.tsx => persist_exception_list.test.ts} (100%) rename x-pack/plugins/lists/public/exceptions/hooks/{persist_exception_list.tsx => persist_exception_list.ts} (100%) rename x-pack/plugins/lists/public/exceptions/hooks/{use_api.test.tsx => use_api.test.ts} (100%) rename x-pack/plugins/lists/public/exceptions/hooks/{use_api.tsx => use_api.ts} (100%) rename x-pack/plugins/lists/public/exceptions/hooks/{use_exception_list.test.tsx => use_exception_list.test.ts} (100%) rename x-pack/plugins/lists/public/exceptions/hooks/{use_exception_list.tsx => use_exception_list.ts} (100%) create mode 100644 x-pack/plugins/security_solution/public/common/components/autocomplete/field.test.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/autocomplete/field.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_exists.test.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_exists.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.test.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.test.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.test.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts create mode 100644 x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.ts create mode 100644 x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.test.ts create mode 100644 x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.ts create mode 100644 x-pack/plugins/security_solution/public/common/components/autocomplete/operator.test.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/autocomplete/operator.tsx rename x-pack/plugins/security_solution/public/common/components/{exceptions => autocomplete}/operators.ts (87%) create mode 100644 x-pack/plugins/security_solution/public/common/components/autocomplete/readme.md create mode 100644 x-pack/plugins/security_solution/public/common/components/autocomplete/translations.ts create mode 100644 x-pack/plugins/security_solution/public/common/components/autocomplete/types.ts delete mode 100644 x-pack/plugins/security_solution/public/common/components/autocomplete_field/__examples__/index.stories.tsx delete mode 100644 x-pack/plugins/security_solution/public/common/components/autocomplete_field/__snapshots__/index.test.tsx.snap delete mode 100644 x-pack/plugins/security_solution/public/common/components/autocomplete_field/index.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/common/components/autocomplete_field/index.tsx delete mode 100644 x-pack/plugins/security_solution/public/common/components/autocomplete_field/suggestion_item.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_button_options.stories.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_button_options.test.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_button_options.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_item.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/builder/exception_item.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.tsx diff --git a/x-pack/plugins/lists/common/schemas/common/schemas.test.ts b/x-pack/plugins/lists/common/schemas/common/schemas.test.ts index eed5be39b7a03..d426a91e71b9e 100644 --- a/x-pack/plugins/lists/common/schemas/common/schemas.test.ts +++ b/x-pack/plugins/lists/common/schemas/common/schemas.test.ts @@ -9,7 +9,7 @@ import { left } from 'fp-ts/lib/Either'; import { foldLeftRight, getPaths } from '../../siem_common_deps'; -import { operator_type as operatorType } from './schemas'; +import { operator, operator_type as operatorType } from './schemas'; describe('Common schemas', () => { describe('operatorType', () => { @@ -60,4 +60,35 @@ describe('Common schemas', () => { expect(keys.length).toEqual(4); }); }); + + describe('operator', () => { + test('it should validate for "included"', () => { + const payload = 'included'; + const decoded = operator.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate for "excluded"', () => { + const payload = 'excluded'; + const decoded = operator.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should contain 2 keys', () => { + // Might seem like a weird test, but its meant to + // ensure that if operator is updated, you + // also update the operatorEnum, a workaround + // for io-ts not yet supporting enums + // https://github.com/gcanti/io-ts/issues/67 + const keys = Object.keys(operator.keys); + + expect(keys.length).toEqual(2); + }); + }); }); diff --git a/x-pack/plugins/lists/common/schemas/common/schemas.ts b/x-pack/plugins/lists/common/schemas/common/schemas.ts index fea8a219bc774..a91f487cfa274 100644 --- a/x-pack/plugins/lists/common/schemas/common/schemas.ts +++ b/x-pack/plugins/lists/common/schemas/common/schemas.ts @@ -130,6 +130,10 @@ export type NamespaceType = t.TypeOf; export const operator = t.keyof({ excluded: null, included: null }); export type Operator = t.TypeOf; +export enum OperatorEnum { + INCLUDED = 'included', + EXCLUDED = 'excluded', +} export const operator_type = t.keyof({ exists: null, diff --git a/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_item.test.tsx b/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_item.test.ts similarity index 100% rename from x-pack/plugins/lists/public/exceptions/hooks/persist_exception_item.test.tsx rename to x-pack/plugins/lists/public/exceptions/hooks/persist_exception_item.test.ts diff --git a/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_item.tsx b/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_item.ts similarity index 100% rename from x-pack/plugins/lists/public/exceptions/hooks/persist_exception_item.tsx rename to x-pack/plugins/lists/public/exceptions/hooks/persist_exception_item.ts diff --git a/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_list.test.tsx b/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_list.test.ts similarity index 100% rename from x-pack/plugins/lists/public/exceptions/hooks/persist_exception_list.test.tsx rename to x-pack/plugins/lists/public/exceptions/hooks/persist_exception_list.test.ts diff --git a/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_list.tsx b/x-pack/plugins/lists/public/exceptions/hooks/persist_exception_list.ts similarity index 100% rename from x-pack/plugins/lists/public/exceptions/hooks/persist_exception_list.tsx rename to x-pack/plugins/lists/public/exceptions/hooks/persist_exception_list.ts diff --git a/x-pack/plugins/lists/public/exceptions/hooks/use_api.test.tsx b/x-pack/plugins/lists/public/exceptions/hooks/use_api.test.ts similarity index 100% rename from x-pack/plugins/lists/public/exceptions/hooks/use_api.test.tsx rename to x-pack/plugins/lists/public/exceptions/hooks/use_api.test.ts diff --git a/x-pack/plugins/lists/public/exceptions/hooks/use_api.tsx b/x-pack/plugins/lists/public/exceptions/hooks/use_api.ts similarity index 100% rename from x-pack/plugins/lists/public/exceptions/hooks/use_api.tsx rename to x-pack/plugins/lists/public/exceptions/hooks/use_api.ts diff --git a/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list.test.tsx b/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list.test.ts similarity index 100% rename from x-pack/plugins/lists/public/exceptions/hooks/use_exception_list.test.tsx rename to x-pack/plugins/lists/public/exceptions/hooks/use_exception_list.test.ts diff --git a/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list.tsx b/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list.ts similarity index 100% rename from x-pack/plugins/lists/public/exceptions/hooks/use_exception_list.tsx rename to x-pack/plugins/lists/public/exceptions/hooks/use_exception_list.ts diff --git a/x-pack/plugins/security_solution/public/common/components/and_or_badge/rounded_badge_antenna.tsx b/x-pack/plugins/security_solution/public/common/components/and_or_badge/rounded_badge_antenna.tsx index 1076d8b41b955..4a0e9ee416aaf 100644 --- a/x-pack/plugins/security_solution/public/common/components/and_or_badge/rounded_badge_antenna.tsx +++ b/x-pack/plugins/security_solution/public/common/components/and_or_badge/rounded_badge_antenna.tsx @@ -15,7 +15,6 @@ const antennaStyles = css` background: ${({ theme }) => theme.eui.euiColorLightShade}; position: relative; width: 2px; - margin: 0 12px 0 0; &:after { background: ${({ theme }) => theme.eui.euiColorLightShade}; content: ''; @@ -40,10 +39,6 @@ const BottomAntenna = styled(EuiFlexItem)` } `; -const EuiFlexItemWrapper = styled(EuiFlexItem)` - margin: 0 12px 0 0; -`; - export const RoundedBadgeAntenna: React.FC<{ type: AndOr }> = ({ type }) => ( = ({ type }) => ( alignItems="center" > - + - + ); diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field.test.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field.test.tsx new file mode 100644 index 0000000000000..30864f246071b --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field.test.tsx @@ -0,0 +1,156 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { ThemeProvider } from 'styled-components'; +import { mount } from 'enzyme'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; + +import { + fields, + getField, +} from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks.ts'; +import { FieldComponent } from './field'; + +describe('FieldComponent', () => { + test('it renders disabled if "isDisabled" is true', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect( + wrapper.find(`[data-test-subj="fieldAutocompleteComboBox"] input`).prop('disabled') + ).toBeTruthy(); + }); + + test('it renders loading if "isLoading" is true', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + wrapper.find(`[data-test-subj="fieldAutocompleteComboBox"] button`).at(0).simulate('click'); + expect( + wrapper + .find(`EuiComboBoxOptionsList[data-test-subj="fieldAutocompleteComboBox-optionsList"]`) + .prop('isLoading') + ).toBeTruthy(); + }); + + test('it allows user to clear values if "isClearable" is true', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect( + wrapper + .find(`[data-test-subj="comboBoxInput"]`) + .hasClass('euiComboBox__inputWrap-isClearable') + ).toBeTruthy(); + }); + + test('it correctly displays selected field', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect( + wrapper.find(`[data-test-subj="fieldAutocompleteComboBox"] EuiComboBoxPill`).at(0).text() + ).toEqual('machine.os.raw'); + }); + + test('it invokes "onChange" when option selected', () => { + const mockOnChange = jest.fn(); + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + ((wrapper.find(EuiComboBox).props() as unknown) as { + onChange: (a: EuiComboBoxOptionOption[]) => void; + }).onChange([{ label: 'machine.os' }]); + + expect(mockOnChange).toHaveBeenCalledWith([ + { + aggregatable: true, + count: 0, + esTypes: ['text'], + name: 'machine.os', + readFromDocValues: false, + scripted: false, + searchable: true, + type: 'string', + }, + ]); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field.tsx new file mode 100644 index 0000000000000..8a6f049c96037 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { useMemo, useCallback } from 'react'; +import { EuiComboBoxOptionOption, EuiComboBox } from '@elastic/eui'; + +import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/common'; +import { getGenericComboBoxProps } from './helpers'; +import { GetGenericComboBoxPropsReturn } from './types'; + +interface OperatorProps { + placeholder: string; + selectedField: IFieldType | undefined; + indexPattern: IIndexPattern | undefined; + isLoading: boolean; + isDisabled: boolean; + isClearable: boolean; + fieldInputWidth?: number; + onChange: (a: IFieldType[]) => void; +} + +export const FieldComponent: React.FC = ({ + placeholder, + selectedField, + indexPattern, + isLoading = false, + isDisabled = false, + isClearable = false, + fieldInputWidth = 190, + onChange, +}): JSX.Element => { + const getLabel = useCallback((field): string => field.name, []); + const optionsMemo = useMemo((): IFieldType[] => (indexPattern ? indexPattern.fields : []), [ + indexPattern, + ]); + const selectedOptionsMemo = useMemo((): IFieldType[] => (selectedField ? [selectedField] : []), [ + selectedField, + ]); + const { comboOptions, labels, selectedComboOptions } = useMemo( + (): GetGenericComboBoxPropsReturn => + getGenericComboBoxProps({ + options: optionsMemo, + selectedOptions: selectedOptionsMemo, + getLabel, + }), + [optionsMemo, selectedOptionsMemo, getLabel] + ); + + const handleValuesChange = (newOptions: EuiComboBoxOptionOption[]): void => { + const newValues: IFieldType[] = newOptions.map( + ({ label }) => optionsMemo[labels.indexOf(label)] + ); + onChange(newValues); + }; + + return ( + + ); +}; + +FieldComponent.displayName = 'Field'; diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_exists.test.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_exists.test.tsx new file mode 100644 index 0000000000000..c4904df3a135c --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_exists.test.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { ThemeProvider } from 'styled-components'; +import { mount } from 'enzyme'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; + +import { AutocompleteFieldExistsComponent } from './field_value_exists'; + +describe('AutocompleteFieldExistsComponent', () => { + test('it renders field disabled', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect( + wrapper + .find(`[data-test-subj="valuesAutocompleteComboBox existsComboxBox"] input`) + .prop('disabled') + ).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_exists.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_exists.tsx new file mode 100644 index 0000000000000..f2161e376eab5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_exists.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { EuiComboBox } from '@elastic/eui'; + +interface AutocompleteFieldExistsProps { + placeholder: string; +} + +export const AutocompleteFieldExistsComponent: React.FC = ({ + placeholder, +}): JSX.Element => ( + +); + +AutocompleteFieldExistsComponent.displayName = 'AutocompleteFieldExists'; diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.test.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.test.tsx new file mode 100644 index 0000000000000..7734344d193b8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.test.tsx @@ -0,0 +1,161 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { ThemeProvider } from 'styled-components'; +import { mount } from 'enzyme'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; + +import { getField } from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks.ts'; +import { AutocompleteFieldListsComponent } from './field_value_lists'; +import { getFoundListSchemaMock } from '../../../../../lists/common/schemas/response/found_list_schema.mock'; + +const mockStart = jest.fn(); +const mockResult = getFoundListSchemaMock(); +jest.mock('../../../common/lib/kibana'); +jest.mock('../../../lists_plugin_deps', () => { + const originalModule = jest.requireActual('../../../lists_plugin_deps'); + + return { + ...originalModule, + useFindLists: () => ({ + loading: false, + start: mockStart.mockReturnValue(mockResult), + result: mockResult, + error: undefined, + }), + }; +}); + +describe('AutocompleteFieldListsComponent', () => { + test('it renders disabled if "isDisabled" is true', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect( + wrapper + .find(`[data-test-subj="valuesAutocompleteComboBox listsComboxBox"] input`) + .prop('disabled') + ).toBeTruthy(); + }); + + test('it renders loading if "isLoading" is true', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + wrapper + .find(`[data-test-subj="valuesAutocompleteComboBox listsComboxBox"] button`) + .at(0) + .simulate('click'); + expect( + wrapper + .find( + `EuiComboBoxOptionsList[data-test-subj="valuesAutocompleteComboBox listsComboxBox-optionsList"]` + ) + .prop('isLoading') + ).toBeTruthy(); + }); + + test('it allows user to clear values if "isClearable" is true', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect( + wrapper + .find(`[data-test-subj="comboBoxInput"]`) + .hasClass('euiComboBox__inputWrap-isClearable') + ).toBeTruthy(); + }); + + test('it correctly displays selected list', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect( + wrapper + .find(`[data-test-subj="valuesAutocompleteComboBox listsComboxBox"] EuiComboBoxPill`) + .at(0) + .text() + ).toEqual('some name'); + }); + + test('it invokes "onChange" when option selected', async () => { + const mockOnChange = jest.fn(); + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + ((wrapper.find(EuiComboBox).props() as unknown) as { + onChange: (a: EuiComboBoxOptionOption[]) => void; + }).onChange([{ label: 'some name' }]); + + expect(mockOnChange).toHaveBeenCalledWith({ + created_at: '2020-04-20T15:25:31.830Z', + created_by: 'some user', + description: 'some description', + id: 'some-list-id', + meta: {}, + name: 'some name', + tie_breaker_id: '6a76b69d-80df-4ab2-8c3e-85f466b06a0e', + type: 'ip', + updated_at: '2020-04-20T15:25:31.830Z', + updated_by: 'some user', + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.tsx new file mode 100644 index 0000000000000..d8ce27e97874d --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { useState, useEffect, useCallback, useMemo } from 'react'; +import { EuiComboBoxOptionOption, EuiComboBox } from '@elastic/eui'; + +import { IFieldType } from '../../../../../../../src/plugins/data/common'; +import { useFindLists, ListSchema } from '../../../lists_plugin_deps'; +import { useKibana } from '../../../common/lib/kibana'; +import { getGenericComboBoxProps } from './helpers'; + +interface AutocompleteFieldListsProps { + placeholder: string; + selectedField: IFieldType | undefined; + selectedValue: string | undefined; + isLoading: boolean; + isDisabled: boolean; + isClearable: boolean; + onChange: (arg: ListSchema) => void; +} + +export const AutocompleteFieldListsComponent: React.FC = ({ + placeholder, + selectedField, + selectedValue, + isLoading = false, + isDisabled = false, + isClearable = false, + onChange, +}): JSX.Element => { + const { http } = useKibana().services; + const [lists, setLists] = useState([]); + const { loading, result, start } = useFindLists(); + const getLabel = useCallback(({ name }) => name, []); + + const optionsMemo = useMemo(() => { + if (selectedField != null) { + return lists.filter(({ type }) => type === selectedField.type); + } else { + return []; + } + }, [lists, selectedField]); + const selectedOptionsMemo = useMemo(() => { + if (selectedValue != null) { + const list = lists.filter(({ id }) => id === selectedValue); + return list ?? []; + } else { + return []; + } + }, [selectedValue, lists]); + const { comboOptions, labels, selectedComboOptions } = useMemo( + () => + getGenericComboBoxProps({ + options: optionsMemo, + selectedOptions: selectedOptionsMemo, + getLabel, + }), + [optionsMemo, selectedOptionsMemo, getLabel] + ); + + const handleValuesChange = useCallback( + (newOptions: EuiComboBoxOptionOption[]) => { + const [newValue] = newOptions.map(({ label }) => optionsMemo[labels.indexOf(label)]); + onChange(newValue ?? ''); + }, + [labels, optionsMemo, onChange] + ); + + useEffect(() => { + if (result != null) { + setLists(result.data); + } + }, [result]); + + useEffect(() => { + if (selectedField != null) { + start({ + http, + pageIndex: 1, + pageSize: 500, + }); + } + }, [selectedField, start, http]); + + return ( + + ); +}; + +AutocompleteFieldListsComponent.displayName = 'AutocompleteFieldList'; diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.test.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.test.tsx new file mode 100644 index 0000000000000..72467a62f57c1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.test.tsx @@ -0,0 +1,238 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { ThemeProvider } from 'styled-components'; +import { mount } from 'enzyme'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; + +import { + fields, + getField, +} from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks.ts'; +import { AutocompleteFieldMatchComponent } from './field_value_match'; +import { useFieldValueAutocomplete } from './hooks/use_field_value_autocomplete'; +jest.mock('./hooks/use_field_value_autocomplete'); + +describe('AutocompleteFieldMatchComponent', () => { + const getValueSuggestionsMock = jest + .fn() + .mockResolvedValue([false, ['value 3', 'value 4'], jest.fn()]); + + beforeAll(() => { + (useFieldValueAutocomplete as jest.Mock).mockReturnValue([ + false, + ['value 1', 'value 2'], + getValueSuggestionsMock, + ]); + }); + test('it renders disabled if "isDisabled" is true', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect( + wrapper + .find(`[data-test-subj="valuesAutocompleteComboBox matchComboxBox"] input`) + .prop('disabled') + ).toBeTruthy(); + }); + + test('it renders loading if "isLoading" is true', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + wrapper + .find(`[data-test-subj="valuesAutocompleteComboBox matchComboxBox"] button`) + .at(0) + .simulate('click'); + expect( + wrapper + .find( + `EuiComboBoxOptionsList[data-test-subj="valuesAutocompleteComboBox matchComboxBox-optionsList"]` + ) + .prop('isLoading') + ).toBeTruthy(); + }); + + test('it allows user to clear values if "isClearable" is true', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect( + wrapper + .find(`[data-test-subj="comboBoxInput"]`) + .hasClass('euiComboBox__inputWrap-isClearable') + ).toBeTruthy(); + }); + + test('it correctly displays selected value', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect( + wrapper + .find(`[data-test-subj="valuesAutocompleteComboBox matchComboxBox"] EuiComboBoxPill`) + .at(0) + .text() + ).toEqual('126.45.211.34'); + }); + + test('it invokes "onChange" when new value created', async () => { + const mockOnChange = jest.fn(); + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + ((wrapper.find(EuiComboBox).props() as unknown) as { + onCreateOption: (a: string) => void; + }).onCreateOption('126.45.211.34'); + + expect(mockOnChange).toHaveBeenCalledWith('126.45.211.34'); + }); + + test('it invokes "onChange" when new value selected', async () => { + const mockOnChange = jest.fn(); + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + ((wrapper.find(EuiComboBox).props() as unknown) as { + onChange: (a: EuiComboBoxOptionOption[]) => void; + }).onChange([{ label: 'value 1' }]); + + expect(mockOnChange).toHaveBeenCalledWith('value 1'); + }); + + test('it invokes updateSuggestions when new value searched', async () => { + const mockOnChange = jest.fn(); + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + ((wrapper.find(EuiComboBox).props() as unknown) as { + onSearchChange: (a: string) => void; + }).onSearchChange('value 1'); + + expect(getValueSuggestionsMock).toHaveBeenCalledWith({ + fieldSelected: getField('machine.os.raw'), + patterns: { + id: '1234', + title: 'logstash-*', + fields, + }, + value: 'value 1', + signal: new AbortController().signal, + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx new file mode 100644 index 0000000000000..4d96d6638132b --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { useCallback, useMemo } from 'react'; +import { EuiComboBoxOptionOption, EuiComboBox } from '@elastic/eui'; +import { uniq } from 'lodash'; + +import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/common'; +import { useFieldValueAutocomplete } from './hooks/use_field_value_autocomplete'; +import { validateParams, getGenericComboBoxProps } from './helpers'; +import { OperatorTypeEnum } from '../../../lists_plugin_deps'; +import { GetGenericComboBoxPropsReturn } from './types'; +import * as i18n from './translations'; + +interface AutocompleteFieldMatchProps { + placeholder: string; + selectedField: IFieldType | undefined; + selectedValue: string | undefined; + indexPattern: IIndexPattern | undefined; + isLoading: boolean; + isDisabled: boolean; + isClearable: boolean; + onChange: (arg: string) => void; +} + +export const AutocompleteFieldMatchComponent: React.FC = ({ + placeholder, + selectedField, + selectedValue, + indexPattern, + isLoading, + isDisabled = false, + isClearable = false, + onChange, +}): JSX.Element => { + const [isLoadingSuggestions, suggestions, updateSuggestions] = useFieldValueAutocomplete({ + selectedField, + operatorType: OperatorTypeEnum.MATCH, + fieldValue: selectedValue, + indexPattern, + }); + const getLabel = useCallback((option: string): string => option, []); + const optionsMemo = useMemo((): string[] => { + const valueAsStr = String(selectedValue); + return selectedValue ? uniq([valueAsStr, ...suggestions]) : suggestions; + }, [suggestions, selectedValue]); + const selectedOptionsMemo = useMemo((): string[] => { + const valueAsStr = String(selectedValue); + return selectedValue ? [valueAsStr] : []; + }, [selectedValue]); + + const { comboOptions, labels, selectedComboOptions } = useMemo( + (): GetGenericComboBoxPropsReturn => + getGenericComboBoxProps({ + options: optionsMemo, + selectedOptions: selectedOptionsMemo, + getLabel, + }), + [optionsMemo, selectedOptionsMemo, getLabel] + ); + + const handleValuesChange = (newOptions: EuiComboBoxOptionOption[]): void => { + const [newValue] = newOptions.map(({ label }) => optionsMemo[labels.indexOf(label)]); + onChange(newValue ?? ''); + }; + + const onSearchChange = (searchVal: string): void => { + const signal = new AbortController().signal; + + updateSuggestions({ + fieldSelected: selectedField, + value: `${searchVal}`, + patterns: indexPattern, + signal, + }); + }; + + const isValid = useMemo( + (): boolean => validateParams(selectedValue, selectedField ? selectedField.type : ''), + [selectedField, selectedValue] + ); + + return ( + + ); +}; + +AutocompleteFieldMatchComponent.displayName = 'AutocompleteFieldMatch'; diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.test.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.test.tsx new file mode 100644 index 0000000000000..f3f0f2e2a44b1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.test.tsx @@ -0,0 +1,238 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { ThemeProvider } from 'styled-components'; +import { mount } from 'enzyme'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; + +import { + fields, + getField, +} from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks.ts'; +import { AutocompleteFieldMatchAnyComponent } from './field_value_match_any'; +import { useFieldValueAutocomplete } from './hooks/use_field_value_autocomplete'; +jest.mock('./hooks/use_field_value_autocomplete'); + +describe('AutocompleteFieldMatchAnyComponent', () => { + const getValueSuggestionsMock = jest + .fn() + .mockResolvedValue([false, ['value 3', 'value 4'], jest.fn()]); + + beforeAll(() => { + (useFieldValueAutocomplete as jest.Mock).mockReturnValue([ + false, + ['value 1', 'value 2'], + getValueSuggestionsMock, + ]); + }); + test('it renders disabled if "isDisabled" is true', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect( + wrapper + .find(`[data-test-subj="valuesAutocompleteComboBox matchAnyComboxBox"] input`) + .prop('disabled') + ).toBeTruthy(); + }); + + test('it renders loading if "isLoading" is true', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + wrapper + .find(`[data-test-subj="valuesAutocompleteComboBox matchAnyComboxBox"] button`) + .at(0) + .simulate('click'); + expect( + wrapper + .find( + `EuiComboBoxOptionsList[data-test-subj="valuesAutocompleteComboBox matchAnyComboxBox-optionsList"]` + ) + .prop('isLoading') + ).toBeTruthy(); + }); + + test('it allows user to clear values if "isClearable" is true', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect( + wrapper + .find(`[data-test-subj="comboBoxInput"]`) + .hasClass('euiComboBox__inputWrap-isClearable') + ).toBeTruthy(); + }); + + test('it correctly displays selected value', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect( + wrapper + .find(`[data-test-subj="valuesAutocompleteComboBox matchAnyComboxBox"] EuiComboBoxPill`) + .at(0) + .text() + ).toEqual('126.45.211.34'); + }); + + test('it invokes "onChange" when new value created', async () => { + const mockOnChange = jest.fn(); + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + ((wrapper.find(EuiComboBox).props() as unknown) as { + onCreateOption: (a: string) => void; + }).onCreateOption('126.45.211.34'); + + expect(mockOnChange).toHaveBeenCalledWith(['126.45.211.34']); + }); + + test('it invokes "onChange" when new value selected', async () => { + const mockOnChange = jest.fn(); + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + ((wrapper.find(EuiComboBox).props() as unknown) as { + onChange: (a: EuiComboBoxOptionOption[]) => void; + }).onChange([{ label: 'value 1' }]); + + expect(mockOnChange).toHaveBeenCalledWith(['value 1']); + }); + + test('it invokes updateSuggestions when new value searched', async () => { + const mockOnChange = jest.fn(); + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + ((wrapper.find(EuiComboBox).props() as unknown) as { + onSearchChange: (a: string) => void; + }).onSearchChange('value 1'); + + expect(getValueSuggestionsMock).toHaveBeenCalledWith({ + fieldSelected: getField('machine.os.raw'), + patterns: { + id: '1234', + title: 'logstash-*', + fields, + }, + value: 'value 1', + signal: new AbortController().signal, + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.tsx new file mode 100644 index 0000000000000..080c89ff013cd --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.tsx @@ -0,0 +1,104 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { useCallback, useMemo } from 'react'; +import { EuiComboBoxOptionOption, EuiComboBox } from '@elastic/eui'; +import { uniq } from 'lodash'; + +import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/common'; +import { useFieldValueAutocomplete } from './hooks/use_field_value_autocomplete'; +import { getGenericComboBoxProps, validateParams } from './helpers'; +import { OperatorTypeEnum } from '../../../lists_plugin_deps'; +import { GetGenericComboBoxPropsReturn } from './types'; +import * as i18n from './translations'; + +interface AutocompleteFieldMatchAnyProps { + placeholder: string; + selectedField: IFieldType | undefined; + selectedValue: string[]; + indexPattern: IIndexPattern | undefined; + isLoading: boolean; + isDisabled: boolean; + isClearable: boolean; + onChange: (arg: string[]) => void; +} + +export const AutocompleteFieldMatchAnyComponent: React.FC = ({ + placeholder, + selectedField, + selectedValue, + indexPattern, + isLoading, + isDisabled = false, + isClearable = false, + onChange, +}): JSX.Element => { + const [isLoadingSuggestions, suggestions, updateSuggestions] = useFieldValueAutocomplete({ + selectedField, + operatorType: OperatorTypeEnum.MATCH_ANY, + fieldValue: selectedValue, + indexPattern, + }); + const getLabel = useCallback((option: string): string => option, []); + const optionsMemo = useMemo( + (): string[] => (selectedValue ? uniq([...selectedValue, ...suggestions]) : suggestions), + [suggestions, selectedValue] + ); + const { comboOptions, labels, selectedComboOptions } = useMemo( + (): GetGenericComboBoxPropsReturn => + getGenericComboBoxProps({ + options: optionsMemo, + selectedOptions: selectedValue, + getLabel, + }), + [optionsMemo, selectedValue, getLabel] + ); + + const handleValuesChange = (newOptions: EuiComboBoxOptionOption[]): void => { + const newValues: string[] = newOptions.map(({ label }) => optionsMemo[labels.indexOf(label)]); + onChange(newValues); + }; + + const onSearchChange = (searchVal: string) => { + const signal = new AbortController().signal; + + updateSuggestions({ + fieldSelected: selectedField, + value: `${searchVal}`, + patterns: indexPattern, + signal, + }); + }; + + const onCreateOption = (option: string) => onChange([...(selectedValue || []), option]); + + const isValid = useMemo((): boolean => { + const areAnyInvalid = selectedComboOptions.filter( + ({ label }) => !validateParams(label, selectedField ? selectedField.type : '') + ); + return areAnyInvalid.length === 0; + }, [selectedComboOptions, selectedField]); + + return ( + + ); +}; + +AutocompleteFieldMatchAnyComponent.displayName = 'AutocompleteFieldMatchAny'; diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts b/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts new file mode 100644 index 0000000000000..c2e8e56084452 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts @@ -0,0 +1,192 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getField } from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks.ts'; + +import { + EXCEPTION_OPERATORS, + isOperator, + isNotOperator, + existsOperator, + doesNotExistOperator, +} from './operators'; +import { getOperators, validateParams, getGenericComboBoxProps } from './helpers'; + +describe('helpers', () => { + describe('#getOperators', () => { + test('it returns "isOperator" if passed in field is "undefined"', () => { + const operator = getOperators(undefined); + + expect(operator).toEqual([isOperator]); + }); + + test('it returns expected operators when field type is "boolean"', () => { + const operator = getOperators(getField('ssl')); + + expect(operator).toEqual([isOperator, isNotOperator, existsOperator, doesNotExistOperator]); + }); + + test('it returns "isOperator" when field type is "nested"', () => { + const operator = getOperators({ + name: 'nestedField', + type: 'nested', + esTypes: ['text'], + count: 0, + scripted: false, + searchable: true, + aggregatable: false, + readFromDocValues: false, + subType: { nested: { path: 'nestedField' } }, + }); + + expect(operator).toEqual([isOperator]); + }); + + test('it returns all operator types when field type is not null, boolean, or nested', () => { + const operator = getOperators(getField('machine.os.raw')); + + expect(operator).toEqual(EXCEPTION_OPERATORS); + }); + }); + + describe('#validateParams', () => { + test('returns true if value is undefined', () => { + const isValid = validateParams(undefined, 'date'); + + expect(isValid).toBeTruthy(); + }); + + test('returns true if value is empty string', () => { + const isValid = validateParams('', 'date'); + + expect(isValid).toBeTruthy(); + }); + + test('returns true if type is "date" and value is valid', () => { + const isValid = validateParams('1994-11-05T08:15:30-05:00', 'date'); + + expect(isValid).toBeTruthy(); + }); + + test('returns false if type is "date" and value is not valid', () => { + const isValid = validateParams('1593478826', 'date'); + + expect(isValid).toBeFalsy(); + }); + + test('returns true if type is "ip" and value is valid', () => { + const isValid = validateParams('126.45.211.34', 'ip'); + + expect(isValid).toBeTruthy(); + }); + + test('returns false if type is "ip" and value is not valid', () => { + const isValid = validateParams('hellooo', 'ip'); + + expect(isValid).toBeFalsy(); + }); + + test('returns true if type is "number" and value is valid', () => { + const isValid = validateParams('123', 'number'); + + expect(isValid).toBeTruthy(); + }); + + test('returns false if type is "number" and value is not valid', () => { + const isValid = validateParams('not a number', 'number'); + + expect(isValid).toBeFalsy(); + }); + }); + + describe('#getGenericComboBoxProps', () => { + test('it returns empty arrays if "options" is empty array', () => { + const result = getGenericComboBoxProps({ + options: [], + selectedOptions: ['option1'], + getLabel: (t: string) => t, + }); + + expect(result).toEqual({ comboOptions: [], labels: [], selectedComboOptions: [] }); + }); + + test('it returns formatted props if "options" array is not empty', () => { + const result = getGenericComboBoxProps({ + options: ['option1', 'option2', 'option3'], + selectedOptions: [], + getLabel: (t: string) => t, + }); + + expect(result).toEqual({ + comboOptions: [ + { + label: 'option1', + }, + { + label: 'option2', + }, + { + label: 'option3', + }, + ], + labels: ['option1', 'option2', 'option3'], + selectedComboOptions: [], + }); + }); + + test('it does not return "selectedOptions" items that do not appear in "options"', () => { + const result = getGenericComboBoxProps({ + options: ['option1', 'option2', 'option3'], + selectedOptions: ['option4'], + getLabel: (t: string) => t, + }); + + expect(result).toEqual({ + comboOptions: [ + { + label: 'option1', + }, + { + label: 'option2', + }, + { + label: 'option3', + }, + ], + labels: ['option1', 'option2', 'option3'], + selectedComboOptions: [], + }); + }); + + test('it return "selectedOptions" items that do appear in "options"', () => { + const result = getGenericComboBoxProps({ + options: ['option1', 'option2', 'option3'], + selectedOptions: ['option2'], + getLabel: (t: string) => t, + }); + + expect(result).toEqual({ + comboOptions: [ + { + label: 'option1', + }, + { + label: 'option2', + }, + { + label: 'option3', + }, + ], + labels: ['option1', 'option2', 'option3'], + selectedComboOptions: [ + { + label: 'option2', + }, + ], + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.ts b/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.ts new file mode 100644 index 0000000000000..888c881f45ce4 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.ts @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import dateMath from '@elastic/datemath'; +import { EuiComboBoxOptionOption } from '@elastic/eui'; + +import { IFieldType } from '../../../../../../../src/plugins/data/common'; +import { Ipv4Address } from '../../../../../../../src/plugins/kibana_utils/public'; +import { + EXCEPTION_OPERATORS, + isOperator, + isNotOperator, + existsOperator, + doesNotExistOperator, +} from './operators'; +import { GetGenericComboBoxPropsReturn, OperatorOption } from './types'; + +export const getOperators = (field: IFieldType | undefined): OperatorOption[] => { + if (field == null) { + return [isOperator]; + } else if (field.type === 'boolean') { + return [isOperator, isNotOperator, existsOperator, doesNotExistOperator]; + } else if (field.type === 'nested') { + return [isOperator]; + } else { + return EXCEPTION_OPERATORS; + } +}; + +export function validateParams(params: string | undefined, type: string) { + // Box would show error state if empty otherwise + if (params == null || params === '') { + return true; + } + + switch (type) { + case 'date': + const moment = dateMath.parse(params); + return Boolean(moment && moment.isValid()); + case 'ip': + try { + return Boolean(new Ipv4Address(params)); + } catch (e) { + return false; + } + case 'number': + const val = parseFloat(params); + return typeof val === 'number' && !isNaN(val); + default: + return true; + } +} + +export function getGenericComboBoxProps({ + options, + selectedOptions, + getLabel, +}: { + options: T[]; + selectedOptions: T[]; + getLabel: (value: T) => string; +}): GetGenericComboBoxPropsReturn { + const newLabels = options.map(getLabel); + const newComboOptions: EuiComboBoxOptionOption[] = newLabels.map((label) => ({ label })); + const newSelectedComboOptions = selectedOptions + .filter((option) => { + return options.indexOf(option) !== -1; + }) + .map((option) => { + return newComboOptions[options.indexOf(option)]; + }); + + return { + comboOptions: newComboOptions, + labels: newLabels, + selectedComboOptions: newSelectedComboOptions, + }; +} diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.test.ts b/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.test.ts new file mode 100644 index 0000000000000..def2a303f6038 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.test.ts @@ -0,0 +1,221 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { act, renderHook } from '@testing-library/react-hooks'; + +import { + UseFieldValueAutocompleteProps, + UseFieldValueAutocompleteReturn, + useFieldValueAutocomplete, +} from './use_field_value_autocomplete'; +import { useKibana } from '../../../../common/lib/kibana'; +import { stubIndexPatternWithFields } from '../../../../../../../../src/plugins/data/common/index_patterns/index_pattern.stub'; +import { getField } from '../../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks.ts'; +import { OperatorTypeEnum } from '../../../../lists_plugin_deps'; + +jest.mock('../../../../common/lib/kibana'); + +describe('useFieldValueAutocomplete', () => { + const onErrorMock = jest.fn(); + const getValueSuggestionsMock = jest.fn().mockResolvedValue(['value 1', 'value 2']); + + beforeAll(() => { + (useKibana as jest.Mock).mockReturnValue({ + services: { + data: { + autocomplete: { + getValueSuggestions: getValueSuggestionsMock, + }, + }, + }, + }); + }); + + afterEach(() => { + onErrorMock.mockClear(); + getValueSuggestionsMock.mockClear(); + }); + + test('initializes hook', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook< + UseFieldValueAutocompleteProps, + UseFieldValueAutocompleteReturn + >(() => + useFieldValueAutocomplete({ + selectedField: undefined, + operatorType: OperatorTypeEnum.MATCH, + fieldValue: '', + indexPattern: undefined, + }) + ); + await waitForNextUpdate(); + + expect(result.current).toEqual([false, [], result.current[2]]); + }); + }); + + test('does not call autocomplete service if "operatorType" is "exists"', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook< + UseFieldValueAutocompleteProps, + UseFieldValueAutocompleteReturn + >(() => + useFieldValueAutocomplete({ + selectedField: getField('machine.os'), + operatorType: OperatorTypeEnum.EXISTS, + fieldValue: '', + indexPattern: stubIndexPatternWithFields, + }) + ); + await waitForNextUpdate(); + + const expectedResult: UseFieldValueAutocompleteReturn = [false, [], result.current[2]]; + + expect(getValueSuggestionsMock).not.toHaveBeenCalled(); + expect(result.current).toEqual(expectedResult); + }); + }); + + test('does not call autocomplete service if "selectedField" is undefined', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook< + UseFieldValueAutocompleteProps, + UseFieldValueAutocompleteReturn + >(() => + useFieldValueAutocomplete({ + selectedField: undefined, + operatorType: OperatorTypeEnum.EXISTS, + fieldValue: '', + indexPattern: stubIndexPatternWithFields, + }) + ); + await waitForNextUpdate(); + + const expectedResult: UseFieldValueAutocompleteReturn = [false, [], result.current[2]]; + + expect(getValueSuggestionsMock).not.toHaveBeenCalled(); + expect(result.current).toEqual(expectedResult); + }); + }); + + test('does not call autocomplete service if "indexPattern" is undefined', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook< + UseFieldValueAutocompleteProps, + UseFieldValueAutocompleteReturn + >(() => + useFieldValueAutocomplete({ + selectedField: getField('machine.os'), + operatorType: OperatorTypeEnum.EXISTS, + fieldValue: '', + indexPattern: undefined, + }) + ); + await waitForNextUpdate(); + + const expectedResult: UseFieldValueAutocompleteReturn = [false, [], result.current[2]]; + + expect(getValueSuggestionsMock).not.toHaveBeenCalled(); + expect(result.current).toEqual(expectedResult); + }); + }); + + test('returns suggestions of "true" and "false" if field type is boolean', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook< + UseFieldValueAutocompleteProps, + UseFieldValueAutocompleteReturn + >(() => + useFieldValueAutocomplete({ + selectedField: getField('ssl'), + operatorType: OperatorTypeEnum.MATCH, + fieldValue: '', + indexPattern: stubIndexPatternWithFields, + }) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + + const expectedResult: UseFieldValueAutocompleteReturn = [ + false, + ['true', 'false'], + result.current[2], + ]; + + expect(getValueSuggestionsMock).not.toHaveBeenCalled(); + expect(result.current).toEqual(expectedResult); + }); + }); + + test('returns suggestions', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook< + UseFieldValueAutocompleteProps, + UseFieldValueAutocompleteReturn + >(() => + useFieldValueAutocomplete({ + selectedField: getField('@tags'), + operatorType: OperatorTypeEnum.MATCH, + fieldValue: '', + indexPattern: stubIndexPatternWithFields, + }) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + + const expectedResult: UseFieldValueAutocompleteReturn = [ + false, + ['value 1', 'value 2'], + result.current[2], + ]; + + expect(getValueSuggestionsMock).toHaveBeenCalledWith({ + field: getField('@tags'), + indexPattern: stubIndexPatternWithFields, + query: '', + signal: new AbortController().signal, + }); + expect(result.current).toEqual(expectedResult); + }); + }); + + test('returns new suggestions on subsequent calls', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook< + UseFieldValueAutocompleteProps, + UseFieldValueAutocompleteReturn + >(() => + useFieldValueAutocomplete({ + selectedField: getField('@tags'), + operatorType: OperatorTypeEnum.MATCH, + fieldValue: '', + indexPattern: stubIndexPatternWithFields, + }) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + + result.current[2]({ + fieldSelected: getField('@tags'), + value: 'hello', + patterns: stubIndexPatternWithFields, + signal: new AbortController().signal, + }); + + await waitForNextUpdate(); + + const expectedResult: UseFieldValueAutocompleteReturn = [ + false, + ['value 1', 'value 2'], + result.current[2], + ]; + + expect(getValueSuggestionsMock).toHaveBeenCalledTimes(2); + expect(result.current).toEqual(expectedResult); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.ts b/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.ts new file mode 100644 index 0000000000000..541c0a8d3fbae --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useEffect, useState, useRef } from 'react'; +import { debounce } from 'lodash'; + +import { IFieldType, IIndexPattern } from '../../../../../../../../src/plugins/data/common'; +import { useKibana } from '../../../../common/lib/kibana'; +import { OperatorTypeEnum } from '../../../../lists_plugin_deps'; + +export type UseFieldValueAutocompleteReturn = [ + boolean, + string[], + (args: { + fieldSelected: IFieldType | undefined; + value: string | string[] | undefined; + patterns: IIndexPattern | undefined; + signal: AbortSignal; + }) => void +]; + +export interface UseFieldValueAutocompleteProps { + selectedField: IFieldType | undefined; + operatorType: OperatorTypeEnum; + fieldValue: string | string[] | undefined; + indexPattern: IIndexPattern | undefined; +} +/** + * Hook for using the field value autocomplete service + * + */ +export const useFieldValueAutocomplete = ({ + selectedField, + operatorType, + fieldValue, + indexPattern, +}: UseFieldValueAutocompleteProps): UseFieldValueAutocompleteReturn => { + const { services } = useKibana(); + const [isLoading, setIsLoading] = useState(false); + const [suggestions, setSuggestions] = useState([]); + const updateSuggestions = useRef( + debounce( + async ({ + fieldSelected, + value, + patterns, + signal, + }: { + fieldSelected: IFieldType | undefined; + value: string | string[] | undefined; + patterns: IIndexPattern | undefined; + signal: AbortSignal; + }) => { + if (fieldSelected == null || patterns == null) { + return; + } + + setIsLoading(true); + + // Fields of type boolean should only display two options + if (fieldSelected.type === 'boolean') { + setIsLoading(false); + setSuggestions(['true', 'false']); + return; + } + + const newSuggestions = await services.data.autocomplete.getValueSuggestions({ + indexPattern: patterns, + field: fieldSelected, + query: '', + signal, + }); + + setIsLoading(false); + setSuggestions(newSuggestions); + }, + 500 + ) + ); + + useEffect(() => { + const abortCtrl = new AbortController(); + + if (operatorType !== OperatorTypeEnum.EXISTS) { + updateSuggestions.current({ + fieldSelected: selectedField, + value: fieldValue, + patterns: indexPattern, + signal: abortCtrl.signal, + }); + } + + return (): void => { + abortCtrl.abort(); + }; + }, [updateSuggestions, selectedField, operatorType, fieldValue, indexPattern]); + + return [isLoading, suggestions, updateSuggestions.current]; +}; diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/operator.test.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/operator.test.tsx new file mode 100644 index 0000000000000..45fe6be78ace6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/operator.test.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { ThemeProvider } from 'styled-components'; +import { mount } from 'enzyme'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; + +import { getField } from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks.ts'; +import { OperatorComponent } from './operator'; +import { isOperator, isNotOperator } from './operators'; + +describe('OperatorComponent', () => { + test('it renders disabled if "isDisabled" is true', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect( + wrapper.find(`[data-test-subj="operatorAutocompleteComboBox"] input`).prop('disabled') + ).toBeTruthy(); + }); + + test('it renders loading if "isLoading" is true', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + wrapper.find(`[data-test-subj="operatorAutocompleteComboBox"] button`).at(0).simulate('click'); + expect( + wrapper + .find(`EuiComboBoxOptionsList[data-test-subj="operatorAutocompleteComboBox-optionsList"]`) + .prop('isLoading') + ).toBeTruthy(); + }); + + test('it allows user to clear values if "isClearable" is true', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect(wrapper.find(`button[data-test-subj="comboBoxClearButton"]`).exists()).toBeTruthy(); + }); + + test('it displays "operatorOptions" if param is passed in', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect( + wrapper.find(`[data-test-subj="operatorAutocompleteComboBox"]`).at(0).prop('options') + ).toEqual([{ label: 'is not' }]); + }); + + test('it correctly displays selected operator', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect( + wrapper.find(`[data-test-subj="operatorAutocompleteComboBox"] EuiComboBoxPill`).at(0).text() + ).toEqual('is'); + }); + + test('it only displays subset of operators if field type is nested', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect( + wrapper.find(`[data-test-subj="operatorAutocompleteComboBox"]`).at(0).prop('options') + ).toEqual([{ label: 'is' }]); + }); + + test('it only displays subset of operators if field type is boolean', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + expect( + wrapper.find(`[data-test-subj="operatorAutocompleteComboBox"]`).at(0).prop('options') + ).toEqual([ + { label: 'is' }, + { label: 'is not' }, + { label: 'exists' }, + { label: 'does not exist' }, + ]); + }); + + test('it invokes "onChange" when option selected', () => { + const mockOnChange = jest.fn(); + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + ((wrapper.find(EuiComboBox).props() as unknown) as { + onChange: (a: EuiComboBoxOptionOption[]) => void; + }).onChange([{ label: 'is not' }]); + + expect(mockOnChange).toHaveBeenCalledWith([ + { message: 'is not', operator: 'excluded', type: 'match', value: 'is_not' }, + ]); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/operator.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/operator.tsx new file mode 100644 index 0000000000000..6d9a684aab2de --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/operator.tsx @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { useCallback, useMemo } from 'react'; +import { EuiComboBoxOptionOption, EuiComboBox } from '@elastic/eui'; + +import { IFieldType } from '../../../../../../../src/plugins/data/common'; +import { getOperators, getGenericComboBoxProps } from './helpers'; +import { GetGenericComboBoxPropsReturn, OperatorOption } from './types'; + +interface OperatorState { + placeholder: string; + selectedField: IFieldType | undefined; + operator: OperatorOption; + isLoading: boolean; + isDisabled: boolean; + isClearable: boolean; + operatorInputWidth?: number; + operatorOptions?: OperatorOption[]; + onChange: (arg: OperatorOption[]) => void; +} + +export const OperatorComponent: React.FC = ({ + placeholder, + selectedField, + operator, + isLoading = false, + isDisabled = false, + isClearable = false, + operatorOptions, + operatorInputWidth = 150, + onChange, +}): JSX.Element => { + const getLabel = useCallback(({ message }): string => message, []); + const optionsMemo = useMemo( + (): OperatorOption[] => (operatorOptions ? operatorOptions : getOperators(selectedField)), + [operatorOptions, selectedField] + ); + const selectedOptionsMemo = useMemo((): OperatorOption[] => (operator ? [operator] : []), [ + operator, + ]); + const { comboOptions, labels, selectedComboOptions } = useMemo( + (): GetGenericComboBoxPropsReturn => + getGenericComboBoxProps({ + options: optionsMemo, + selectedOptions: selectedOptionsMemo, + getLabel, + }), + [optionsMemo, selectedOptionsMemo, getLabel] + ); + + const handleValuesChange = (newOptions: EuiComboBoxOptionOption[]): void => { + const newValues: OperatorOption[] = newOptions.map( + ({ label }) => optionsMemo[labels.indexOf(label)] + ); + onChange(newValues); + }; + + return ( + + ); +}; + +OperatorComponent.displayName = 'Operator'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/operators.ts b/x-pack/plugins/security_solution/public/common/components/autocomplete/operators.ts similarity index 87% rename from x-pack/plugins/security_solution/public/common/components/exceptions/operators.ts rename to x-pack/plugins/security_solution/public/common/components/autocomplete/operators.ts index 2c18d7447d5f6..a81d8cde94e34 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/operators.ts +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/operators.ts @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; import { OperatorOption } from './types'; -import { OperatorTypeEnum } from '../../../lists_plugin_deps'; +import { OperatorEnum, OperatorTypeEnum } from '../../../lists_plugin_deps'; export const isOperator: OperatorOption = { message: i18n.translate('xpack.securitySolution.exceptions.isOperatorLabel', { @@ -14,7 +14,7 @@ export const isOperator: OperatorOption = { }), value: 'is', type: OperatorTypeEnum.MATCH, - operator: 'included', + operator: OperatorEnum.INCLUDED, }; export const isNotOperator: OperatorOption = { @@ -23,7 +23,7 @@ export const isNotOperator: OperatorOption = { }), value: 'is_not', type: OperatorTypeEnum.MATCH, - operator: 'excluded', + operator: OperatorEnum.EXCLUDED, }; export const isOneOfOperator: OperatorOption = { @@ -32,7 +32,7 @@ export const isOneOfOperator: OperatorOption = { }), value: 'is_one_of', type: OperatorTypeEnum.MATCH_ANY, - operator: 'included', + operator: OperatorEnum.INCLUDED, }; export const isNotOneOfOperator: OperatorOption = { @@ -41,7 +41,7 @@ export const isNotOneOfOperator: OperatorOption = { }), value: 'is_not_one_of', type: OperatorTypeEnum.MATCH_ANY, - operator: 'excluded', + operator: OperatorEnum.EXCLUDED, }; export const existsOperator: OperatorOption = { @@ -50,7 +50,7 @@ export const existsOperator: OperatorOption = { }), value: 'exists', type: OperatorTypeEnum.EXISTS, - operator: 'included', + operator: OperatorEnum.INCLUDED, }; export const doesNotExistOperator: OperatorOption = { @@ -59,7 +59,7 @@ export const doesNotExistOperator: OperatorOption = { }), value: 'does_not_exist', type: OperatorTypeEnum.EXISTS, - operator: 'excluded', + operator: OperatorEnum.EXCLUDED, }; export const isInListOperator: OperatorOption = { @@ -68,7 +68,7 @@ export const isInListOperator: OperatorOption = { }), value: 'is_in_list', type: OperatorTypeEnum.LIST, - operator: 'included', + operator: OperatorEnum.INCLUDED, }; export const isNotInListOperator: OperatorOption = { @@ -77,7 +77,7 @@ export const isNotInListOperator: OperatorOption = { }), value: 'is_not_in_list', type: OperatorTypeEnum.LIST, - operator: 'excluded', + operator: OperatorEnum.EXCLUDED, }; export const EXCEPTION_OPERATORS: OperatorOption[] = [ diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/readme.md b/x-pack/plugins/security_solution/public/common/components/autocomplete/readme.md new file mode 100644 index 0000000000000..2bf1867c008d2 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/readme.md @@ -0,0 +1,122 @@ +# Autocomplete Fields + +Need an input that shows available index fields? Or an input that autocompletes based on a selected indexPattern field? Bingo! That's what these components are for. They are generalized enough so that they can be reused throughout and repurposed based on your needs. + +All three of the available components rely on Eui's combo box. + +## useFieldValueAutocomplete + +This hook uses the kibana `services.data.autocomplete.getValueSuggestions()` service to return possible autocomplete fields based on the passed in `indexPattern` and `selectedField`. + +## FieldComponent + +This component can be used to display available indexPattern fields. It requires an indexPattern to be passed in and will show an error state if value is not one of the available indexPattern fields. Users will be able to select only one option. + +The `onChange` handler is passed `IFieldType[]`. + +```js + +``` + +## OperatorComponent + +This component can be used to display available operators. If you want to pass in your own operators, you can use `operatorOptions` prop. If a `operatorOptions` is provided, those will be used and it will ignore any of the built in logic that determines which operators to show. The operators within `operatorOptions` will still need to be of type `OperatorOption`. + +If no `operatorOptions` is provided, then the following behavior is observed: + +- if `selectedField` type is `boolean`, only `is`, `is not`, `exists`, `does not exist` operators will show +- if `selectedField` type is `nested`, only `is` operator will show +- if not one of the above, all operators will show (see `operators.ts`) + +The `onChange` handler is passed `OperatorOption[]`. + +```js + +``` + +## AutocompleteFieldExistsComponent + +This field value component is used when the selected operator is `exists` or `does not exist`. When these operators are selected, they are equivalent to using a wildcard. The combo box will be displayed as disabled. + +```js + +``` + +## AutocompleteFieldListsComponent + +This component can be used to display available large value lists - when operator selected is `is in list` or `is not in list`. It relies on hooks from the `lists` plugin. Users can only select one list and an error is shown if value is not one of available lists. + +The `selectedValue` should be the `id` of the selected list. + +This component relies on `selectedField` to render available lists. The reason being that it relies on the `selectedField` type to determine which lists to show as each large value list has a type as well. So if a user selects a field of type `ip`, it will only display lists of type `ip`. + +The `onChange` handler is passed `ListSchema`. + +```js + +``` + +## AutocompleteFieldMatchComponent + +This component can be used to allow users to select one single value. It uses the autocomplete hook to display any autocomplete options based on the passed in `indexPattern`, but also allows a user to add their own value. + +It does some minor validation, assuring that field value is a date if `selectedField` type is `date`, a number if `selectedField` type is `number`, an ip if `selectedField` type is `ip`. + +The `onChange` handler is passed selected `string`. + +```js + +``` + +## AutocompleteFieldMatchAnyComponent + +This component can be used to allow users to select multiple values. It uses the autocomplete hook to display any autocomplete options based on the passed in `indexPattern`, but also allows a user to add their own values. + +It does some minor validation, assuring that field values are a date if `selectedField` type is `date`, numbers if `selectedField` type is `number`, ips if `selectedField` type is `ip`. + +The `onChange` handler is passed selected `string[]`. + +```js + +``` diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/translations.ts b/x-pack/plugins/security_solution/public/common/components/autocomplete/translations.ts new file mode 100644 index 0000000000000..6d83086b15e6a --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/translations.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const LOADING = i18n.translate('xpack.securitySolution.autocomplete.loadingDescription', { + defaultMessage: 'Loading...', +}); diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/types.ts b/x-pack/plugins/security_solution/public/common/components/autocomplete/types.ts new file mode 100644 index 0000000000000..78a7b8aeb61eb --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/types.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiComboBoxOptionOption } from '@elastic/eui'; + +import { OperatorEnum, OperatorTypeEnum } from '../../../lists_plugin_deps'; + +export interface GetGenericComboBoxPropsReturn { + comboOptions: EuiComboBoxOptionOption[]; + labels: string[]; + selectedComboOptions: EuiComboBoxOptionOption[]; +} + +export interface OperatorOption { + message: string; + value: string; + operator: OperatorEnum; + type: OperatorTypeEnum; +} diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete_field/__examples__/index.stories.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete_field/__examples__/index.stories.tsx deleted file mode 100644 index 8f261da629f94..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete_field/__examples__/index.stories.tsx +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import * as React from 'react'; -import { storiesOf } from '@storybook/react'; -import { ThemeProvider } from 'styled-components'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; -import { - QuerySuggestion, - QuerySuggestionTypes, -} from '../../../../../../../../src/plugins/data/public'; -import { SuggestionItem } from '../suggestion_item'; - -const suggestion: QuerySuggestion = { - description: 'Description...', - end: 3, - start: 1, - text: 'Text...', - type: QuerySuggestionTypes.Value, -}; - -storiesOf('components/SuggestionItem', module).add('example', () => ( - ({ - eui: euiLightVars, - darkMode: false, - })} - > - - -)); diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete_field/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/autocomplete_field/__snapshots__/index.test.tsx.snap deleted file mode 100644 index dfd9612d52443..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete_field/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,26 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Autocomplete rendering it renders against snapshot 1`] = ` - - - - - -`; diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete_field/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete_field/index.test.tsx deleted file mode 100644 index 55e114818ffea..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete_field/index.test.tsx +++ /dev/null @@ -1,388 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiFieldSearch } from '@elastic/eui'; -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; -import { mount, shallow } from 'enzyme'; -import { noop } from 'lodash/fp'; -import React from 'react'; -import { ThemeProvider } from 'styled-components'; -import { - QuerySuggestion, - QuerySuggestionTypes, -} from '../../../../../../../src/plugins/data/public'; - -import { TestProviders } from '../../mock'; - -import { AutocompleteField } from '.'; - -const mockAutoCompleteData: QuerySuggestion[] = [ - { - type: QuerySuggestionTypes.Field, - text: 'agent.ephemeral_id ', - description: - '

Filter results that contain agent.ephemeral_id

', - start: 0, - end: 1, - }, - { - type: QuerySuggestionTypes.Field, - text: 'agent.hostname ', - description: - '

Filter results that contain agent.hostname

', - start: 0, - end: 1, - }, - { - type: QuerySuggestionTypes.Field, - text: 'agent.id ', - description: - '

Filter results that contain agent.id

', - start: 0, - end: 1, - }, - { - type: QuerySuggestionTypes.Field, - text: 'agent.name ', - description: - '

Filter results that contain agent.name

', - start: 0, - end: 1, - }, - { - type: QuerySuggestionTypes.Field, - text: 'agent.type ', - description: - '

Filter results that contain agent.type

', - start: 0, - end: 1, - }, - { - type: QuerySuggestionTypes.Field, - text: 'agent.version ', - description: - '

Filter results that contain agent.version

', - start: 0, - end: 1, - }, - { - type: QuerySuggestionTypes.Field, - text: 'agent.test1 ', - description: - '

Filter results that contain agent.test1

', - start: 0, - end: 1, - }, - { - type: QuerySuggestionTypes.Field, - text: 'agent.test2 ', - description: - '

Filter results that contain agent.test2

', - start: 0, - end: 1, - }, - { - type: QuerySuggestionTypes.Field, - text: 'agent.test3 ', - description: - '

Filter results that contain agent.test3

', - start: 0, - end: 1, - }, - { - type: QuerySuggestionTypes.Field, - text: 'agent.test4 ', - description: - '

Filter results that contain agent.test4

', - start: 0, - end: 1, - }, -]; - -describe('Autocomplete', () => { - describe('rendering', () => { - test('it renders against snapshot', () => { - const placeholder = 'myPlaceholder'; - - const wrapper = shallow( - - ); - expect(wrapper).toMatchSnapshot(); - }); - - test('it is rendering with placeholder', () => { - const placeholder = 'myPlaceholder'; - - const wrapper = mount( - - ); - const input = wrapper.find('input[type="search"]'); - expect(input.find('[placeholder]').props().placeholder).toEqual(placeholder); - }); - - test('Rendering suggested items', () => { - const wrapper = mount( - ({ eui: euiDarkVars, darkMode: true })}> - - - ); - const wrapperAutocompleteField = wrapper.find(AutocompleteField); - wrapperAutocompleteField.setState({ areSuggestionsVisible: true }); - wrapper.update(); - - expect(wrapper.find('.euiPanel div[data-test-subj="suggestion-item"]').length).toEqual(10); - }); - - test('Should Not render suggested items if loading new suggestions', () => { - const wrapper = mount( - ({ eui: euiDarkVars, darkMode: true })}> - - - ); - const wrapperAutocompleteField = wrapper.find(AutocompleteField); - wrapperAutocompleteField.setState({ areSuggestionsVisible: true }); - wrapper.update(); - - expect(wrapper.find('.euiPanel div[data-test-subj="suggestion-item"]').length).toEqual(0); - }); - }); - - describe('events', () => { - test('OnChange should have been called', () => { - const onChange = jest.fn((value: string) => value); - - const wrapper = mount( - - ); - const wrapperFixedEuiFieldSearch = wrapper.find('input'); - wrapperFixedEuiFieldSearch.simulate('change', { target: { value: 'test' } }); - expect(onChange).toHaveBeenCalled(); - }); - }); - - test('OnSubmit should have been called by keying enter on the search input', () => { - const onSubmit = jest.fn((value: string) => value); - - const wrapper = mount( - - ); - const wrapperAutocompleteField = wrapper.find(AutocompleteField); - wrapperAutocompleteField.setState({ selectedIndex: null }); - const wrapperFixedEuiFieldSearch = wrapper.find('input'); - wrapperFixedEuiFieldSearch.simulate('keydown', { key: 'Enter', preventDefault: noop }); - expect(onSubmit).toHaveBeenCalled(); - }); - - test('OnSubmit should have been called by onSearch event on the input', () => { - const onSubmit = jest.fn((value: string) => value); - - const wrapper = mount( - - ); - const wrapperAutocompleteField = wrapper.find(AutocompleteField); - wrapperAutocompleteField.setState({ selectedIndex: null }); - const wrapperFixedEuiFieldSearch = wrapper.find(EuiFieldSearch); - // TODO: FixedEuiFieldSearch fails to import - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (wrapperFixedEuiFieldSearch as any).props().onSearch(); - expect(onSubmit).toHaveBeenCalled(); - }); - - test('OnChange should have been called if keying enter on a suggested item selected', () => { - const onChange = jest.fn((value: string) => value); - - const wrapper = mount( - - ); - const wrapperAutocompleteField = wrapper.find(AutocompleteField); - wrapperAutocompleteField.setState({ selectedIndex: 1 }); - const wrapperFixedEuiFieldSearch = wrapper.find('input'); - wrapperFixedEuiFieldSearch.simulate('keydown', { key: 'Enter', preventDefault: noop }); - expect(onChange).toHaveBeenCalled(); - }); - - test('OnChange should be called if tab is pressed when a suggested item is selected', () => { - const onChange = jest.fn((value: string) => value); - - const wrapper = mount( - - ); - const wrapperAutocompleteField = wrapper.find(AutocompleteField); - wrapperAutocompleteField.setState({ selectedIndex: 1 }); - const wrapperFixedEuiFieldSearch = wrapper.find('input'); - wrapperFixedEuiFieldSearch.simulate('keydown', { key: 'Tab', preventDefault: noop }); - expect(onChange).toHaveBeenCalled(); - }); - - test('OnChange should NOT be called if tab is pressed when more than one item is suggested, and no selection has been made', () => { - const onChange = jest.fn((value: string) => value); - - const wrapper = mount( - - ); - - const wrapperFixedEuiFieldSearch = wrapper.find('input'); - wrapperFixedEuiFieldSearch.simulate('keydown', { key: 'Tab', preventDefault: noop }); - expect(onChange).not.toHaveBeenCalled(); - }); - - test('OnChange should be called if tab is pressed when only one item is suggested, even though that item is NOT selected', () => { - const onChange = jest.fn((value: string) => value); - const onlyOneSuggestion = [mockAutoCompleteData[0]]; - - const wrapper = mount( - - - - ); - - const wrapperAutocompleteField = wrapper.find(AutocompleteField); - wrapperAutocompleteField.setState({ areSuggestionsVisible: true }); - const wrapperFixedEuiFieldSearch = wrapper.find('input'); - wrapperFixedEuiFieldSearch.simulate('keydown', { key: 'Tab', preventDefault: noop }); - expect(onChange).toHaveBeenCalled(); - }); - - test('OnChange should NOT be called if tab is pressed when 0 items are suggested', () => { - const onChange = jest.fn((value: string) => value); - - const wrapper = mount( - - ); - - const wrapperFixedEuiFieldSearch = wrapper.find('input'); - wrapperFixedEuiFieldSearch.simulate('keydown', { key: 'Tab', preventDefault: noop }); - expect(onChange).not.toHaveBeenCalled(); - }); - - test('Load more suggestions when arrowdown on the search bar', () => { - const loadSuggestions = jest.fn(noop); - - const wrapper = mount( - - ); - const wrapperFixedEuiFieldSearch = wrapper.find('input'); - wrapperFixedEuiFieldSearch.simulate('keydown', { key: 'ArrowDown', preventDefault: noop }); - expect(loadSuggestions).toHaveBeenCalled(); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete_field/index.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete_field/index.tsx deleted file mode 100644 index f1b7da522fbd0..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete_field/index.tsx +++ /dev/null @@ -1,335 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - EuiFieldSearch, - EuiFieldSearchProps, - EuiOutsideClickDetector, - EuiPanel, -} from '@elastic/eui'; -import React from 'react'; -import { QuerySuggestion } from '../../../../../../../src/plugins/data/public'; - -import euiStyled from '../../../../../../legacy/common/eui_styled_components'; - -import { SuggestionItem } from './suggestion_item'; - -interface AutocompleteFieldProps { - 'data-test-subj'?: string; - isLoadingSuggestions: boolean; - isValid: boolean; - loadSuggestions: (value: string, cursorPosition: number, maxCount?: number) => void; - onSubmit?: (value: string) => void; - onChange?: (value: string) => void; - placeholder?: string; - suggestions: QuerySuggestion[]; - value: string; -} - -interface AutocompleteFieldState { - areSuggestionsVisible: boolean; - isFocused: boolean; - selectedIndex: number | null; -} - -export class AutocompleteField extends React.PureComponent< - AutocompleteFieldProps, - AutocompleteFieldState -> { - public readonly state: AutocompleteFieldState = { - areSuggestionsVisible: false, - isFocused: false, - selectedIndex: null, - }; - - private inputElement: HTMLInputElement | null = null; - - public render() { - const { - 'data-test-subj': dataTestSubj, - suggestions, - isLoadingSuggestions, - isValid, - placeholder, - value, - } = this.props; - const { areSuggestionsVisible, selectedIndex } = this.state; - return ( - - - - {areSuggestionsVisible && !isLoadingSuggestions && suggestions.length > 0 ? ( - - {suggestions.map((suggestion, suggestionIndex) => ( - - ))} - - ) : null} - - - ); - } - - public componentDidUpdate(prevProps: AutocompleteFieldProps, prevState: AutocompleteFieldState) { - const hasNewValue = prevProps.value !== this.props.value; - const hasNewSuggestions = prevProps.suggestions !== this.props.suggestions; - - if (hasNewValue) { - this.updateSuggestions(); - } - - if (hasNewSuggestions && this.state.isFocused) { - this.showSuggestions(); - } - } - - private handleChangeInputRef = (element: HTMLInputElement | null) => { - this.inputElement = element; - }; - - private handleChange = (evt: React.ChangeEvent) => { - this.changeValue(evt.currentTarget.value); - }; - - private handleKeyDown = (evt: React.KeyboardEvent) => { - const { suggestions } = this.props; - switch (evt.key) { - case 'ArrowUp': - evt.preventDefault(); - if (suggestions.length > 0) { - this.setState( - composeStateUpdaters(withSuggestionsVisible, withPreviousSuggestionSelected) - ); - } - break; - case 'ArrowDown': - evt.preventDefault(); - if (suggestions.length > 0) { - this.setState(composeStateUpdaters(withSuggestionsVisible, withNextSuggestionSelected)); - } else { - this.updateSuggestions(); - } - break; - case 'Enter': - evt.preventDefault(); - if (this.state.selectedIndex !== null) { - this.applySelectedSuggestion(); - } else { - this.submit(); - } - break; - case 'Tab': - evt.preventDefault(); - if (this.state.areSuggestionsVisible && this.props.suggestions.length === 1) { - this.applySuggestionAt(0)(); - } else if (this.state.selectedIndex !== null) { - this.applySelectedSuggestion(); - } - break; - case 'Escape': - evt.preventDefault(); - evt.stopPropagation(); - this.setState(withSuggestionsHidden); - break; - } - }; - - private handleKeyUp = (evt: React.KeyboardEvent) => { - switch (evt.key) { - case 'ArrowLeft': - case 'ArrowRight': - case 'Home': - case 'End': - this.updateSuggestions(); - break; - } - }; - - private handleFocus = () => { - this.setState(composeStateUpdaters(withSuggestionsVisible, withFocused)); - }; - - private handleBlur = () => { - this.setState(composeStateUpdaters(withSuggestionsHidden, withUnfocused)); - }; - - private selectSuggestionAt = (index: number) => () => { - this.setState(withSuggestionAtIndexSelected(index)); - }; - - private applySelectedSuggestion = () => { - if (this.state.selectedIndex !== null) { - this.applySuggestionAt(this.state.selectedIndex)(); - } - }; - - private applySuggestionAt = (index: number) => () => { - const { value, suggestions } = this.props; - const selectedSuggestion = suggestions[index]; - - if (!selectedSuggestion) { - return; - } - - const newValue = - value.substr(0, selectedSuggestion.start) + - selectedSuggestion.text + - value.substr(selectedSuggestion.end); - - this.setState(withSuggestionsHidden); - this.changeValue(newValue); - this.focusInputElement(); - }; - - private changeValue = (value: string) => { - const { onChange } = this.props; - - if (onChange) { - onChange(value); - } - }; - - private focusInputElement = () => { - if (this.inputElement) { - this.inputElement.focus(); - } - }; - - private showSuggestions = () => { - this.setState(withSuggestionsVisible); - }; - - private submit = () => { - const { isValid, onSubmit, value } = this.props; - - if (isValid && onSubmit) { - onSubmit(value); - } - - this.setState(withSuggestionsHidden); - }; - - private updateSuggestions = () => { - const inputCursorPosition = this.inputElement ? this.inputElement.selectionStart || 0 : 0; - this.props.loadSuggestions(this.props.value, inputCursorPosition, 10); - }; -} - -type StateUpdater = ( - prevState: Readonly, - prevProps: Readonly -) => State | null; - -function composeStateUpdaters(...updaters: Array>) { - return (state: State, props: Props) => - updaters.reduce((currentState, updater) => updater(currentState, props) || currentState, state); -} - -const withPreviousSuggestionSelected = ( - state: AutocompleteFieldState, - props: AutocompleteFieldProps -): AutocompleteFieldState => ({ - ...state, - selectedIndex: - props.suggestions.length === 0 - ? null - : state.selectedIndex !== null - ? (state.selectedIndex + props.suggestions.length - 1) % props.suggestions.length - : Math.max(props.suggestions.length - 1, 0), -}); - -const withNextSuggestionSelected = ( - state: AutocompleteFieldState, - props: AutocompleteFieldProps -): AutocompleteFieldState => ({ - ...state, - selectedIndex: - props.suggestions.length === 0 - ? null - : state.selectedIndex !== null - ? (state.selectedIndex + 1) % props.suggestions.length - : 0, -}); - -const withSuggestionAtIndexSelected = (suggestionIndex: number) => ( - state: AutocompleteFieldState, - props: AutocompleteFieldProps -): AutocompleteFieldState => ({ - ...state, - selectedIndex: - props.suggestions.length === 0 - ? null - : suggestionIndex >= 0 && suggestionIndex < props.suggestions.length - ? suggestionIndex - : 0, -}); - -const withSuggestionsVisible = (state: AutocompleteFieldState) => ({ - ...state, - areSuggestionsVisible: true, -}); - -const withSuggestionsHidden = (state: AutocompleteFieldState) => ({ - ...state, - areSuggestionsVisible: false, - selectedIndex: null, -}); - -const withFocused = (state: AutocompleteFieldState) => ({ - ...state, - isFocused: true, -}); - -const withUnfocused = (state: AutocompleteFieldState) => ({ - ...state, - isFocused: false, -}); - -export const FixedEuiFieldSearch: React.FC< - React.InputHTMLAttributes & - EuiFieldSearchProps & { - inputRef?: (element: HTMLInputElement | null) => void; - onSearch: (value: string) => void; - } -> = EuiFieldSearch as any; // eslint-disable-line @typescript-eslint/no-explicit-any - -const AutocompleteContainer = euiStyled.div` - position: relative; -`; - -AutocompleteContainer.displayName = 'AutocompleteContainer'; - -const SuggestionsPanel = euiStyled(EuiPanel).attrs(() => ({ - paddingSize: 'none', - hasShadow: true, -}))` - position: absolute; - width: 100%; - margin-top: 2px; - overflow: hidden; - z-index: ${(props) => props.theme.eui.euiZLevel1}; -`; - -SuggestionsPanel.displayName = 'SuggestionsPanel'; diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete_field/suggestion_item.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete_field/suggestion_item.tsx deleted file mode 100644 index 56d25cbdda024..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete_field/suggestion_item.tsx +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiIcon } from '@elastic/eui'; -import { transparentize } from 'polished'; -import React from 'react'; -import styled from 'styled-components'; -import euiStyled from '../../../../../../legacy/common/eui_styled_components'; -import { QuerySuggestion } from '../../../../../../../src/plugins/data/public'; - -interface SuggestionItemProps { - isSelected?: boolean; - onClick?: React.MouseEventHandler; - onMouseEnter?: React.MouseEventHandler; - suggestion: QuerySuggestion; -} - -export const SuggestionItem = React.memo( - ({ isSelected = false, onClick, onMouseEnter, suggestion }) => { - return ( - - - - - {suggestion.text} - {suggestion.description} - - ); - } -); - -SuggestionItem.displayName = 'SuggestionItem'; - -const SuggestionItemContainer = euiStyled.div<{ - isSelected?: boolean; -}>` - display: flex; - flex-direction: row; - font-size: ${(props) => props.theme.eui.euiFontSizeS}; - height: ${(props) => props.theme.eui.euiSizeXL}; - white-space: nowrap; - background-color: ${(props) => - props.isSelected ? props.theme.eui.euiColorLightestShade : 'transparent'}; -`; - -SuggestionItemContainer.displayName = 'SuggestionItemContainer'; - -const SuggestionItemField = euiStyled.div` - align-items: center; - cursor: pointer; - display: flex; - flex-direction: row; - height: ${(props) => props.theme.eui.euiSizeXL}; - padding: ${(props) => props.theme.eui.euiSizeXS}; -`; - -SuggestionItemField.displayName = 'SuggestionItemField'; - -const SuggestionItemIconField = styled(SuggestionItemField)<{ suggestionType: string }>` - background-color: ${(props) => - transparentize(0.9, getEuiIconColor(props.theme, props.suggestionType))}; - color: ${(props) => getEuiIconColor(props.theme, props.suggestionType)}; - flex: 0 0 auto; - justify-content: center; - width: ${(props) => props.theme.eui.euiSizeXL}; -`; - -SuggestionItemIconField.displayName = 'SuggestionItemIconField'; - -const SuggestionItemTextField = styled(SuggestionItemField)` - flex: 2 0 0; - font-family: ${(props) => props.theme.eui.euiCodeFontFamily}; -`; - -SuggestionItemTextField.displayName = 'SuggestionItemTextField'; - -const SuggestionItemDescriptionField = styled(SuggestionItemField)` - flex: 3 0 0; - - p { - display: inline; - - span { - font-family: ${(props) => props.theme.eui.euiCodeFontFamily}; - } - } -`; - -SuggestionItemDescriptionField.displayName = 'SuggestionItemDescriptionField'; - -const getEuiIconType = (suggestionType: string) => { - switch (suggestionType) { - case 'field': - return 'kqlField'; - case 'value': - return 'kqlValue'; - case 'recentSearch': - return 'search'; - case 'conjunction': - return 'kqlSelector'; - case 'operator': - return 'kqlOperand'; - default: - return 'empty'; - } -}; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const getEuiIconColor = (theme: any, suggestionType: string): string => { - switch (suggestionType) { - case 'field': - return theme.eui.euiColorVis7; - case 'value': - return theme.eui.euiColorVis0; - case 'operator': - return theme.eui.euiColorVis1; - case 'conjunction': - return theme.eui.euiColorVis2; - case 'recentSearch': - default: - return theme.eui.euiColorMediumShade; - } -}; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_button_options.stories.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_button_options.stories.tsx new file mode 100644 index 0000000000000..7e4cbe34f9a64 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_button_options.stories.tsx @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { storiesOf, addDecorator } from '@storybook/react'; +import { action } from '@storybook/addon-actions'; +import React from 'react'; +import { ThemeProvider } from 'styled-components'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; + +import { BuilderButtonOptions } from './builder_button_options'; + +addDecorator((storyFn) => ( + ({ eui: euiLightVars, darkMode: false })}>{storyFn()} +)); + +storiesOf('Components|Exceptions|BuilderButtonOptions', module) + .add('init button', () => { + return ( + + ); + }) + .add('and/or buttons', () => { + return ( + + ); + }) + .add('nested button', () => { + return ( + + ); + }) + .add('and disabled', () => { + return ( + + ); + }) + .add('or disabled', () => { + return ( + + ); + }); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_button_options.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_button_options.test.tsx new file mode 100644 index 0000000000000..59306b5343743 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_button_options.test.tsx @@ -0,0 +1,167 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { mount } from 'enzyme'; +import React from 'react'; + +import { BuilderButtonOptions } from './builder_button_options'; + +describe('BuilderButtonOptions', () => { + test('it renders "and" and "or" buttons', () => { + const wrapper = mount( + + ); + + expect(wrapper.find('[data-test-subj="exceptionsAndButton"] button')).toHaveLength(1); + expect(wrapper.find('[data-test-subj="exceptionsOrButton"] button')).toHaveLength(1); + expect(wrapper.find('[data-test-subj="exceptionsAddNewExceptionButton"] button')).toHaveLength( + 0 + ); + expect(wrapper.find('[data-test-subj="exceptionsNestedButton"] button')).toHaveLength(0); + }); + + test('it renders "add exception" button if "displayInitButton" is true', () => { + const wrapper = mount( + + ); + + expect(wrapper.find('[data-test-subj="exceptionsAddNewExceptionButton"] button')).toHaveLength( + 1 + ); + }); + + test('it invokes "onAddExceptionClicked" when "add exception" button is clicked', () => { + const onOrClicked = jest.fn(); + + const wrapper = mount( + + ); + + wrapper.find('[data-test-subj="exceptionsAddNewExceptionButton"] button').simulate('click'); + + expect(onOrClicked).toHaveBeenCalledTimes(1); + }); + + test('it invokes "onOrClicked" when "or" button is clicked', () => { + const onOrClicked = jest.fn(); + + const wrapper = mount( + + ); + + wrapper.find('[data-test-subj="exceptionsOrButton"] button').simulate('click'); + + expect(onOrClicked).toHaveBeenCalledTimes(1); + }); + + test('it invokes "onAndClicked" when "and" button is clicked', () => { + const onAndClicked = jest.fn(); + + const wrapper = mount( + + ); + + wrapper.find('[data-test-subj="exceptionsAndButton"] button').simulate('click'); + + expect(onAndClicked).toHaveBeenCalledTimes(1); + }); + + test('it disables "and" button if "isAndDisabled" is true', () => { + const wrapper = mount( + + ); + + const andButton = wrapper.find('[data-test-subj="exceptionsAndButton"] button').at(0); + + expect(andButton.prop('disabled')).toBeTruthy(); + }); + + test('it disables "or" button if "isOrDisabled" is true', () => { + const wrapper = mount( + + ); + + const orButton = wrapper.find('[data-test-subj="exceptionsOrButton"] button').at(0); + + expect(orButton.prop('disabled')).toBeTruthy(); + }); + + test('it invokes "onNestedClicked" when "and" button is clicked', () => { + const onNestedClicked = jest.fn(); + + const wrapper = mount( + + ); + + wrapper.find('[data-test-subj="exceptionsNestedButton"] button').simulate('click'); + + expect(onNestedClicked).toHaveBeenCalledTimes(1); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_button_options.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_button_options.tsx new file mode 100644 index 0000000000000..ff1556bcc4d25 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/builder_button_options.tsx @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui'; +import styled from 'styled-components'; + +import * as i18n from '../translations'; + +const MyEuiButton = styled(EuiButton)` + min-width: 95px; +`; + +interface BuilderButtonOptionsProps { + isOrDisabled: boolean; + isAndDisabled: boolean; + displayInitButton: boolean; + showNestedButton: boolean; + onAndClicked: () => void; + onOrClicked: () => void; + onNestedClicked: () => void; +} + +export const BuilderButtonOptions: React.FC = ({ + isOrDisabled = false, + isAndDisabled = false, + displayInitButton, + showNestedButton = false, + onAndClicked, + onOrClicked, + onNestedClicked, +}) => ( + + {displayInitButton ? ( + + + {i18n.ADD_EXCEPTION_TITLE} + + + ) : ( + <> + + + {i18n.AND} + + + + + {i18n.OR} + + + {showNestedButton && ( + + + {i18n.ADD_NESTED_DESCRIPTION} + + + )} + + )} + +); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_item.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_item.tsx new file mode 100644 index 0000000000000..39a1e1bdbad5a --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_item.tsx @@ -0,0 +1,243 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { useCallback } from 'react'; +import { EuiFormRow, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +import { IFieldType, IIndexPattern } from '../../../../../../../../src/plugins/data/common'; +import { FieldComponent } from '../../autocomplete/field'; +import { OperatorComponent } from '../../autocomplete/operator'; +import { isOperator } from '../../autocomplete/operators'; +import { OperatorOption } from '../../autocomplete/types'; +import { AutocompleteFieldMatchComponent } from '../../autocomplete/field_value_match'; +import { AutocompleteFieldMatchAnyComponent } from '../../autocomplete/field_value_match_any'; +import { AutocompleteFieldExistsComponent } from '../../autocomplete/field_value_exists'; +import { FormattedBuilderEntry, BuilderEntry } from '../types'; +import { AutocompleteFieldListsComponent } from '../../autocomplete/field_value_lists'; +import { ListSchema, OperatorTypeEnum } from '../../../../lists_plugin_deps'; +import { getValueFromOperator } from '../helpers'; +import { getEmptyValue } from '../../empty_value'; +import * as i18n from '../translations'; + +interface EntryItemProps { + entry: FormattedBuilderEntry; + entryIndex: number; + indexPattern: IIndexPattern; + isLoading: boolean; + showLabel: boolean; + onChange: (arg: BuilderEntry, i: number) => void; +} + +export const EntryItemComponent: React.FC = ({ + entry, + entryIndex, + indexPattern, + isLoading, + showLabel, + onChange, +}): JSX.Element => { + const handleFieldChange = useCallback( + ([newField]: IFieldType[]): void => { + onChange( + { + field: newField.name, + type: OperatorTypeEnum.MATCH, + operator: isOperator.operator, + value: undefined, + }, + entryIndex + ); + }, + [onChange, entryIndex] + ); + + const handleOperatorChange = useCallback( + ([newOperator]: OperatorOption[]): void => { + const newEntry = getValueFromOperator(entry.field, newOperator); + onChange(newEntry, entryIndex); + }, + [onChange, entryIndex, entry.field] + ); + + const handleFieldMatchValueChange = useCallback( + (newField: string): void => { + onChange( + { + field: entry.field != null ? entry.field.name : undefined, + type: OperatorTypeEnum.MATCH, + operator: isOperator.operator, + value: newField, + }, + entryIndex + ); + }, + [onChange, entryIndex, entry.field] + ); + + const handleFieldMatchAnyValueChange = useCallback( + (newField: string[]): void => { + onChange( + { + field: entry.field != null ? entry.field.name : undefined, + type: OperatorTypeEnum.MATCH_ANY, + operator: isOperator.operator, + value: newField, + }, + entryIndex + ); + }, + [onChange, entryIndex, entry.field] + ); + + const handleFieldListValueChange = useCallback( + (newField: ListSchema): void => { + onChange( + { + field: entry.field != null ? entry.field.name : undefined, + type: OperatorTypeEnum.LIST, + operator: isOperator.operator, + list: { id: newField.id, type: newField.type }, + }, + entryIndex + ); + }, + [onChange, entryIndex, entry.field] + ); + + const renderFieldInput = (isFirst: boolean): JSX.Element => { + const comboBox = ( + + ); + + if (isFirst) { + return ( + + {comboBox} + + ); + } else { + return comboBox; + } + }; + + const renderOperatorInput = (isFirst: boolean): JSX.Element => { + const comboBox = ( + + ); + + if (isFirst) { + return ( + + {comboBox} + + ); + } else { + return comboBox; + } + }; + + const getFieldValueComboBox = (type: OperatorTypeEnum): JSX.Element => { + switch (type) { + case OperatorTypeEnum.MATCH: + const value = typeof entry.value === 'string' ? entry.value : undefined; + return ( + + ); + case OperatorTypeEnum.MATCH_ANY: + const values: string[] = Array.isArray(entry.value) ? entry.value : []; + return ( + + ); + case OperatorTypeEnum.LIST: + const id = typeof entry.value === 'string' ? entry.value : undefined; + return ( + + ); + case OperatorTypeEnum.EXISTS: + return ( + + ); + default: + return <>; + } + }; + + const renderFieldValueInput = (isFirst: boolean, entryType: OperatorTypeEnum): JSX.Element => { + if (isFirst) { + return ( + + {getFieldValueComboBox(entryType)} + + ); + } else { + return getFieldValueComboBox(entryType); + } + }; + + return ( + + {renderFieldInput(showLabel)} + {renderOperatorInput(showLabel)} + {renderFieldValueInput(showLabel, entry.operator.type)} + + ); +}; + +EntryItemComponent.displayName = 'EntryItem'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/exception_item.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/exception_item.tsx new file mode 100644 index 0000000000000..3afdf43ec7dfa --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/exception_item.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useMemo } from 'react'; +import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import styled from 'styled-components'; + +import { IIndexPattern } from '../../../../../../../../src/plugins/data/common'; +import { AndOrBadge } from '../../and_or_badge'; +import { EntryItemComponent } from './entry_item'; +import { getFormattedBuilderEntries } from '../helpers'; +import { FormattedBuilderEntry, ExceptionsBuilderExceptionItem, BuilderEntry } from '../types'; + +const MyInvisibleAndBadge = styled(EuiFlexItem)` + visibility: hidden; +`; + +const MyFirstRowContainer = styled(EuiFlexItem)` + padding-top: 20px; +`; + +interface ExceptionListItemProps { + exceptionItem: ExceptionsBuilderExceptionItem; + exceptionId: string; + exceptionItemIndex: number; + isLoading: boolean; + indexPattern: IIndexPattern; + andLogicIncluded: boolean; + onDeleteExceptionItem: (item: ExceptionsBuilderExceptionItem, index: number) => void; + onExceptionItemChange: (item: ExceptionsBuilderExceptionItem, index: number) => void; +} + +export const ExceptionListItemComponent = React.memo( + ({ + exceptionItem, + exceptionId, + exceptionItemIndex, + indexPattern, + isLoading, + andLogicIncluded, + onDeleteExceptionItem, + onExceptionItemChange, + }) => { + const handleEntryChange = (entry: BuilderEntry, entryIndex: number): void => { + const updatedEntries: BuilderEntry[] = [ + ...exceptionItem.entries.slice(0, entryIndex), + { ...entry }, + ...exceptionItem.entries.slice(entryIndex + 1), + ]; + const updatedExceptionItem: ExceptionsBuilderExceptionItem = { + ...exceptionItem, + entries: updatedEntries, + }; + onExceptionItemChange(updatedExceptionItem, exceptionItemIndex); + }; + + const handleDeleteEntry = (entryIndex: number): void => { + const updatedEntries: BuilderEntry[] = [ + ...exceptionItem.entries.slice(0, entryIndex), + ...exceptionItem.entries.slice(entryIndex + 1), + ]; + const updatedExceptionItem: ExceptionsBuilderExceptionItem = { + ...exceptionItem, + entries: updatedEntries, + }; + + onDeleteExceptionItem(updatedExceptionItem, exceptionItemIndex); + }; + + const entries = useMemo( + (): FormattedBuilderEntry[] => + indexPattern != null ? getFormattedBuilderEntries(indexPattern, exceptionItem.entries) : [], + [indexPattern, exceptionItem.entries] + ); + + const andBadge = useMemo((): JSX.Element => { + const badge = ; + if (entries.length > 1 && exceptionItemIndex === 0) { + return {badge}; + } else if (entries.length > 1) { + return {badge}; + } else { + return {badge}; + } + }, [entries.length, exceptionItemIndex]); + + const getDeleteButton = (index: number): JSX.Element => { + const button = ( + handleDeleteEntry(index)} + aria-label="entryDeleteButton" + className="exceptionItemEntryDeleteButton" + data-test-subj="exceptionItemEntryDeleteButton" + /> + ); + if (index === 0 && exceptionItemIndex === 0) { + return {button}; + } else { + return {button}; + } + }; + + return ( + + {andLogicIncluded && andBadge} + + + {entries.map((item, index) => ( + + + + + + {getDeleteButton(index)} + + + ))} + + + + ); + } +); + +ExceptionListItemComponent.displayName = 'ExceptionListItem'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.tsx new file mode 100644 index 0000000000000..d7e438f49af36 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.tsx @@ -0,0 +1,248 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { useMemo, useCallback, useEffect, useState } from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import styled from 'styled-components'; + +import { ExceptionListItemComponent } from './exception_item'; +import { useFetchIndexPatterns } from '../../../../alerts/containers/detection_engine/rules/fetch_index_patterns'; +import { + ExceptionListItemSchema, + NamespaceType, + exceptionListItemSchema, + OperatorTypeEnum, + OperatorEnum, + CreateExceptionListItemSchema, +} from '../../../../../public/lists_plugin_deps'; +import { AndOrBadge } from '../../and_or_badge'; +import { BuilderButtonOptions } from './builder_button_options'; +import { getNewExceptionItem, filterExceptionItems } from '../helpers'; +import { ExceptionsBuilderExceptionItem, CreateExceptionListItemBuilderSchema } from '../types'; +import { Loader } from '../../loader'; + +const MyInvisibleAndBadge = styled(EuiFlexItem)` + visibility: hidden; +`; + +const MyAndBadge = styled(AndOrBadge)` + & > .euiFlexItem { + margin: 0; + } +`; + +const MyButtonsContainer = styled(EuiFlexItem)` + margin: 16px 0; +`; + +interface OnChangeProps { + exceptionItems: Array; + exceptionsToDelete: ExceptionListItemSchema[]; +} + +interface ExceptionBuilderProps { + exceptionListItems: ExceptionListItemSchema[]; + listType: 'detection' | 'endpoint'; + listId: string; + listNamespaceType: NamespaceType; + ruleName: string; + indexPatternConfig: string[]; + isLoading: boolean; + isOrDisabled: boolean; + isAndDisabled: boolean; + onChange: (arg: OnChangeProps) => void; +} + +export const ExceptionBuilder = ({ + exceptionListItems, + listType, + listId, + listNamespaceType, + ruleName, + indexPatternConfig, + isLoading, + isOrDisabled, + isAndDisabled, + onChange, +}: ExceptionBuilderProps) => { + const [andLogicIncluded, setAndLogicIncluded] = useState(false); + const [exceptions, setExceptions] = useState( + exceptionListItems + ); + const [exceptionsToDelete, setExceptionsToDelete] = useState([]); + const [{ isLoading: indexPatternLoading, indexPatterns }] = useFetchIndexPatterns( + indexPatternConfig ?? [] + ); + + // Bubble up changes to parent + useEffect(() => { + onChange({ exceptionItems: filterExceptionItems(exceptions), exceptionsToDelete }); + }, [onChange, exceptionsToDelete, exceptions]); + + const checkAndLogic = (items: ExceptionsBuilderExceptionItem[]): void => { + setAndLogicIncluded(items.filter(({ entries }) => entries.length > 1).length > 0); + }; + + const handleDeleteExceptionItem = ( + item: ExceptionsBuilderExceptionItem, + itemIndex: number + ): void => { + if (item.entries.length === 0) { + if (exceptionListItemSchema.is(item)) { + setExceptionsToDelete((items) => [...items, item]); + } + + setExceptions((existingExceptions) => { + const updatedExceptions = [ + ...existingExceptions.slice(0, itemIndex), + ...existingExceptions.slice(itemIndex + 1), + ]; + checkAndLogic(updatedExceptions); + + return updatedExceptions; + }); + } else { + handleExceptionItemChange(item, itemIndex); + } + }; + + const handleExceptionItemChange = (item: ExceptionsBuilderExceptionItem, index: number): void => { + const updatedExceptions = [ + ...exceptions.slice(0, index), + { + ...item, + }, + ...exceptions.slice(index + 1), + ]; + + checkAndLogic(updatedExceptions); + setExceptions(updatedExceptions); + }; + + const handleAddNewExceptionItemEntry = useCallback((): void => { + setExceptions((existingExceptions): ExceptionsBuilderExceptionItem[] => { + const lastException = existingExceptions[existingExceptions.length - 1]; + const { entries } = lastException; + const updatedException: ExceptionsBuilderExceptionItem = { + ...lastException, + entries: [ + ...entries, + { field: '', type: OperatorTypeEnum.MATCH, operator: OperatorEnum.INCLUDED, value: '' }, + ], + }; + + setAndLogicIncluded(updatedException.entries.length > 1); + + return [ + ...existingExceptions.slice(0, existingExceptions.length - 1), + { ...updatedException }, + ]; + }); + }, [setExceptions, setAndLogicIncluded]); + + const handleAddNewExceptionItem = useCallback((): void => { + // There is a case where there are numerous exception list items, all with + // empty `entries` array. Thought about appending an entry item to one, but that + // would then be arbitrary, decided to just create a new exception list item + const newException = getNewExceptionItem({ + listType, + listId, + namespaceType: listNamespaceType, + ruleName, + }); + setExceptions((existingExceptions) => [...existingExceptions, { ...newException }]); + }, [setExceptions, listType, listId, listNamespaceType, ruleName]); + + // An exception item can have an empty array for `entries` + const displayInitialAddExceptionButton = useMemo((): boolean => { + return ( + exceptions.length === 0 || + (exceptions.length === 1 && + exceptions[0].entries != null && + exceptions[0].entries.length === 0) + ); + }, [exceptions]); + + // The builder can have existing exception items, or new exception items that have yet + // to be created (and thus lack an id), this was creating some React bugs with relying + // on the index, as a result, created a temporary id when new exception items are first + // instantiated that is stored in `meta` that gets stripped on it's way out + const getExceptionListItemId = (item: ExceptionsBuilderExceptionItem, index: number): string => { + if ((item as ExceptionListItemSchema).id != null) { + return (item as ExceptionListItemSchema).id; + } else if ((item as CreateExceptionListItemBuilderSchema).meta.temporaryUuid != null) { + return (item as CreateExceptionListItemBuilderSchema).meta.temporaryUuid; + } else { + return `${index}`; + } + }; + + return ( + + {(isLoading || indexPatternLoading) && ( + + )} + {exceptions.map((exceptionListItem, index) => ( + + + {index !== 0 && + (andLogicIncluded ? ( + + + + + + + + + + + ) : ( + + + + ))} + + + + + + ))} + + + + {andLogicIncluded && ( + + + + )} + + {}} + /> + + + + + ); +}; + +ExceptionBuilder.displayName = 'ExceptionBuilder'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx index b936aea047690..3e3b86cc60585 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx @@ -16,8 +16,10 @@ import { getTagsInclude, getDescriptionListContent, getFormattedComments, + filterExceptionItems, + getNewExceptionItem, } from './helpers'; -import { FormattedEntry, DescriptionListItem } from './types'; +import { FormattedEntry, DescriptionListItem, EmptyEntry } from './types'; import { isOperator, isNotOperator, @@ -27,8 +29,8 @@ import { isNotInListOperator, existsOperator, doesNotExistOperator, -} from './operators'; -import { OperatorTypeEnum } from '../../../lists_plugin_deps'; +} from '../autocomplete/operators'; +import { OperatorTypeEnum, OperatorEnum } from '../../../lists_plugin_deps'; import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; import { getEntryExistsMock, @@ -169,7 +171,7 @@ describe('Exception helpers', () => { fieldName: 'host.name', isNested: false, operator: 'exists', - value: null, + value: undefined, }, ]; expect(result).toEqual(expected); @@ -221,13 +223,13 @@ describe('Exception helpers', () => { fieldName: 'host.name', isNested: false, operator: 'exists', - value: null, + value: undefined, }, { fieldName: 'host.name', isNested: false, - operator: null, - value: null, + operator: undefined, + value: undefined, }, { fieldName: 'host.name.host.name', @@ -407,4 +409,36 @@ describe('Exception helpers', () => { expect(wrapper.text()).toEqual('some old comment'); }); }); + + describe('#filterExceptionItems', () => { + test('it removes empty entry items', () => { + const { entries, ...rest } = getExceptionListItemSchemaMock(); + const mockEmptyException: EmptyEntry = { + field: 'host.name', + type: OperatorTypeEnum.MATCH, + operator: OperatorEnum.INCLUDED, + value: undefined, + }; + const exceptions = filterExceptionItems([ + { + ...rest, + entries: [...entries, mockEmptyException], + }, + ]); + + expect(exceptions).toEqual([getExceptionListItemSchemaMock()]); + }); + + test('it removes `temporaryId` from items', () => { + const { meta, ...rest } = getNewExceptionItem({ + listType: 'detection', + listId: '123', + namespaceType: 'single', + ruleName: 'rule name', + }); + const exceptions = filterExceptionItems([{ ...rest, meta }]); + + expect(exceptions).toEqual([{ ...rest, meta: undefined }]); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx index ae4131f9f62c2..c8b3d3f527270 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx @@ -8,28 +8,44 @@ import React from 'react'; import { EuiText, EuiCommentProps, EuiAvatar } from '@elastic/eui'; import { capitalize } from 'lodash'; import moment from 'moment'; +import uuid from 'uuid'; import * as i18n from './translations'; -import { FormattedEntry, OperatorOption, DescriptionListItem } from './types'; -import { EXCEPTION_OPERATORS, isOperator } from './operators'; +import { + FormattedEntry, + BuilderEntry, + EmptyListEntry, + DescriptionListItem, + FormattedBuilderEntry, + CreateExceptionListItemBuilderSchema, + ExceptionsBuilderExceptionItem, +} from './types'; +import { EXCEPTION_OPERATORS, isOperator } from '../autocomplete/operators'; +import { OperatorOption } from '../autocomplete/types'; import { CommentsArray, Entry, - EntriesArray, ExceptionListItemSchema, + NamespaceType, OperatorTypeEnum, + CreateExceptionListItemSchema, + entry, entriesNested, - entriesExists, - entriesList, + createExceptionListItemSchema, + exceptionListItemSchema, } from '../../../lists_plugin_deps'; +import { IFieldType, IIndexPattern } from '../../../../../../../src/plugins/data/common'; + +export const isListType = (item: BuilderEntry): item is EmptyListEntry => + item.type === OperatorTypeEnum.LIST; /** * Returns the operator type, may not need this if using io-ts types * - * @param entry a single ExceptionItem entry + * @param item a single ExceptionItem entry */ -export const getOperatorType = (entry: Entry): OperatorTypeEnum => { - switch (entry.type) { +export const getOperatorType = (item: BuilderEntry): OperatorTypeEnum => { + switch (item.type) { case 'match': return OperatorTypeEnum.MATCH; case 'match_any': @@ -45,36 +61,46 @@ export const getOperatorType = (entry: Entry): OperatorTypeEnum => { * Determines operator selection (is/is not/is one of, etc.) * Default operator is "is" * - * @param entry a single ExceptionItem entry + * @param item a single ExceptionItem entry */ -export const getExceptionOperatorSelect = (entry: Entry): OperatorOption => { - if (entriesNested.is(entry)) { +export const getExceptionOperatorSelect = (item: BuilderEntry): OperatorOption => { + if (entriesNested.is(item)) { return isOperator; } else { - const operatorType = getOperatorType(entry); + const operatorType = getOperatorType(item); const foundOperator = EXCEPTION_OPERATORS.find((operatorOption) => { - return entry.operator === operatorOption.operator && operatorType === operatorOption.type; + return item.operator === operatorOption.operator && operatorType === operatorOption.type; }); return foundOperator ?? isOperator; } }; +export const getExceptionOperatorFromSelect = (value: string): OperatorOption => { + const operator = EXCEPTION_OPERATORS.filter(({ message }) => message === value); + return operator[0] ?? isOperator; +}; + /** * Formats ExceptionItem entries into simple field, operator, value * for use in rendering items in table * * @param entries an ExceptionItem's entries */ -export const getFormattedEntries = (entries: EntriesArray): FormattedEntry[] => { - const formattedEntries = entries.map((entry) => { - if (entriesNested.is(entry)) { - const parent = { fieldName: entry.field, operator: null, value: null, isNested: false }; - return entry.entries.reduce( +export const getFormattedEntries = (entries: BuilderEntry[]): FormattedEntry[] => { + const formattedEntries = entries.map((item) => { + if (entriesNested.is(item)) { + const parent = { + fieldName: item.field, + operator: undefined, + value: undefined, + isNested: false, + }; + return item.entries.reduce( (acc, nestedEntry) => { const formattedEntry = formatEntry({ isNested: true, - parent: entry.field, + parent: item.field, item: nestedEntry, }); return [...acc, { ...formattedEntry }]; @@ -82,20 +108,24 @@ export const getFormattedEntries = (entries: EntriesArray): FormattedEntry[] => [parent] ); } else { - return formatEntry({ isNested: false, item: entry }); + return formatEntry({ isNested: false, item }); } }); return formattedEntries.flat(); }; -export const getEntryValue = (entry: Entry): string | string[] | null => { - if (entriesList.is(entry)) { - return entry.list.id; - } else if (entriesExists.is(entry)) { - return null; - } else { - return entry.value; +export const getEntryValue = (item: BuilderEntry): string | string[] | undefined => { + switch (item.type) { + case OperatorTypeEnum.MATCH: + case OperatorTypeEnum.MATCH_ANY: + return item.value; + case OperatorTypeEnum.EXISTS: + return undefined; + case OperatorTypeEnum.LIST: + return item.list.id; + default: + return undefined; } }; @@ -109,13 +139,13 @@ export const formatEntry = ({ }: { isNested: boolean; parent?: string; - item: Entry; + item: BuilderEntry; }): FormattedEntry => { const operator = getExceptionOperatorSelect(item); const value = getEntryValue(item); return { - fieldName: isNested ? `${parent}.${item.field}` : item.field, + fieldName: isNested ? `${parent}.${item.field}` : item.field ?? '', operator: operator.message, value, isNested, @@ -192,3 +222,122 @@ export const getFormattedComments = (comments: CommentsArray): EuiCommentProps[] timelineIcon: , children: {comment.comment}, })); + +export const getFormattedBuilderEntries = ( + indexPattern: IIndexPattern, + entries: BuilderEntry[] +): FormattedBuilderEntry[] => { + const { fields } = indexPattern; + return entries.map((item) => { + if (entriesNested.is(item)) { + return { + parent: item.field, + operator: isOperator, + nested: getFormattedBuilderEntries(indexPattern, item.entries), + field: undefined, + value: undefined, + }; + } else { + const [selectedField] = fields.filter( + ({ name }) => item.field != null && item.field === name + ); + return { + field: selectedField, + operator: getExceptionOperatorSelect(item), + value: getEntryValue(item), + }; + } + }); +}; + +export const getValueFromOperator = ( + field: IFieldType | undefined, + selectedOperator: OperatorOption +): Entry => { + const fieldValue = field != null ? field.name : ''; + switch (selectedOperator.type) { + case 'match': + return { + field: fieldValue, + type: OperatorTypeEnum.MATCH, + operator: selectedOperator.operator, + value: '', + }; + case 'match_any': + return { + field: fieldValue, + type: OperatorTypeEnum.MATCH_ANY, + operator: selectedOperator.operator, + value: [], + }; + case 'list': + return { + field: fieldValue, + type: OperatorTypeEnum.LIST, + operator: selectedOperator.operator, + list: { id: '', type: 'ip' }, + }; + default: + return { + field: fieldValue, + type: OperatorTypeEnum.EXISTS, + operator: selectedOperator.operator, + }; + } +}; + +export const getNewExceptionItem = ({ + listType, + listId, + namespaceType, + ruleName, +}: { + listType: 'detection' | 'endpoint'; + listId: string; + namespaceType: NamespaceType; + ruleName: string; +}): CreateExceptionListItemBuilderSchema => { + return { + _tags: [listType], + comments: [], + description: `${ruleName} - exception list item`, + entries: [ + { + field: '', + operator: 'included', + type: 'match', + value: '', + }, + ], + item_id: undefined, + list_id: listId, + meta: { + temporaryUuid: uuid.v4(), + }, + name: `${ruleName} - exception list item`, + namespace_type: namespaceType, + tags: [], + type: 'simple', + }; +}; + +export const filterExceptionItems = ( + exceptions: ExceptionsBuilderExceptionItem[] +): Array => { + return exceptions.reduce>( + (acc, exception) => { + const entries = exception.entries.filter((t) => entry.is(t) || entriesNested.is(t)); + const item = { ...exception, entries }; + if (exceptionListItemSchema.is(item)) { + return [...acc, item]; + } else if (createExceptionListItemSchema.is(item) && item.meta != null) { + const { meta, ...rest } = item; + const itemSansMetaId: CreateExceptionListItemSchema = { ...rest, meta: undefined }; + return [...acc, itemSansMetaId]; + } else { + return acc; + } + }, + [] + ); +}; 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 27dab7cf9db29..093842f5e6c24 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 @@ -3,6 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + import { i18n } from '@kbn/i18n'; export const DETECTION_LIST = i18n.translate( @@ -137,3 +138,65 @@ export const SHOWING_EXCEPTIONS = (items: number) => values: { items }, defaultMessage: 'Showing {items} {items, plural, =1 {exception} other {exceptions}}', }); + +export const FIELD = i18n.translate('xpack.securitySolution.exceptions.fieldDescription', { + defaultMessage: 'Field', +}); + +export const OPERATOR = i18n.translate('xpack.securitySolution.exceptions.operatorDescription', { + defaultMessage: 'Operator', +}); + +export const VALUE = i18n.translate('xpack.securitySolution.exceptions.valueDescription', { + defaultMessage: 'Value', +}); + +export const EXCEPTION_FIELD_PLACEHOLDER = i18n.translate( + 'xpack.securitySolution.exceptions.exceptionFieldPlaceholderDescription', + { + defaultMessage: 'Search', + } +); + +export const EXCEPTION_OPERATOR_PLACEHOLDER = i18n.translate( + 'xpack.securitySolution.exceptions.exceptionOperatorPlaceholderDescription', + { + defaultMessage: 'Operator', + } +); + +export const EXCEPTION_FIELD_VALUE_PLACEHOLDER = i18n.translate( + 'xpack.securitySolution.exceptions.exceptionFieldValuePlaceholderDescription', + { + defaultMessage: 'Search field value...', + } +); + +export const EXCEPTION_FIELD_LISTS_PLACEHOLDER = i18n.translate( + 'xpack.securitySolution.exceptions.exceptionListsPlaceholderDescription', + { + defaultMessage: 'Search for list...', + } +); + +export const ADD_EXCEPTION_TITLE = i18n.translate( + 'xpack.securitySolution.exceptions.addExceptionTitle', + { + defaultMessage: 'Add exception', + } +); + +export const AND = i18n.translate('xpack.securitySolution.exceptions.andDescription', { + defaultMessage: 'AND', +}); + +export const OR = i18n.translate('xpack.securitySolution.exceptions.orDescription', { + defaultMessage: 'OR', +}); + +export const ADD_NESTED_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.exceptions.addNestedDescription', + { + defaultMessage: 'Add nested condition', + } +); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts index ed2be64b4430f..d5a0afe47c48e 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts @@ -4,20 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ import { ReactNode } from 'react'; - -import { Operator, OperatorType } from '../../../lists_plugin_deps'; - -export interface OperatorOption { - message: string; - value: string; - operator: Operator; - type: OperatorType; -} +import { IFieldType } from '../../../../../../../src/plugins/data/common'; +import { OperatorOption } from '../autocomplete/types'; +import { + EntryNested, + Entry, + ExceptionListItemSchema, + CreateExceptionListItemSchema, + OperatorTypeEnum, + OperatorEnum, +} from '../../../lists_plugin_deps'; export interface FormattedEntry { fieldName: string; - operator: string | null; - value: string | string[] | null; + operator: string | undefined; + value: string | string[] | undefined; isNested: boolean; } @@ -49,3 +50,46 @@ export interface ExceptionsPagination { totalItemCount: number; pageSizeOptions: number[]; } + +export interface FormattedBuilderEntryBase { + field: IFieldType | undefined; + operator: OperatorOption; + value: string | string[] | undefined; +} + +export interface FormattedBuilderEntry extends FormattedBuilderEntryBase { + parent?: string; + nested?: FormattedBuilderEntryBase[]; +} + +export interface EmptyEntry { + field: string | undefined; + operator: OperatorEnum; + type: OperatorTypeEnum.MATCH | OperatorTypeEnum.MATCH_ANY; + value: string | string[] | undefined; +} + +export interface EmptyListEntry { + field: string | undefined; + operator: OperatorEnum; + type: OperatorTypeEnum.LIST; + list: { id: string | undefined; type: string | undefined }; +} + +export type BuilderEntry = Entry | EmptyListEntry | EmptyEntry | EntryNested; + +export type ExceptionListItemBuilderSchema = Omit & { + entries: BuilderEntry[]; +}; + +export type CreateExceptionListItemBuilderSchema = Omit< + CreateExceptionListItemSchema, + 'meta' | 'entries' +> & { + meta: { temporaryUuid: string }; + entries: BuilderEntry[]; +}; + +export type ExceptionsBuilderExceptionItem = + | ExceptionListItemBuilderSchema + | CreateExceptionListItemBuilderSchema; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_entries.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_entries.test.tsx index c6a779845b190..dedf7f2b22380 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_entries.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_entries.test.tsx @@ -115,8 +115,8 @@ describe('ExceptionEntries', () => { test('it renders nested entry', () => { const parentEntry = getFormattedEntryMock(); - parentEntry.operator = null; - parentEntry.value = null; + parentEntry.operator = undefined; + parentEntry.value = undefined; const wrapper = mount( ({ eui: euiLightVars, darkMode: false })}> diff --git a/x-pack/plugins/security_solution/public/lists_plugin_deps.ts b/x-pack/plugins/security_solution/public/lists_plugin_deps.ts index f3a724a755a48..f1482029b82c9 100644 --- a/x-pack/plugins/security_solution/public/lists_plugin_deps.ts +++ b/x-pack/plugins/security_solution/public/lists_plugin_deps.ts @@ -9,23 +9,32 @@ export { useExceptionList, usePersistExceptionItem, usePersistExceptionList, + useFindLists, ExceptionIdentifiers, ExceptionList, Pagination, UseExceptionListSuccess, } from '../../lists/public'; export { + ListSchema, CommentsArray, ExceptionListSchema, ExceptionListItemSchema, + CreateExceptionListItemSchema, Entry, EntryExists, EntryNested, + EntryList, EntriesArray, NamespaceType, Operator, + OperatorEnum, OperatorType, OperatorTypeEnum, + exceptionListItemSchema, + createExceptionListItemSchema, + listSchema, + entry, entriesNested, entriesExists, entriesList, From b21e43d848df2fbdedcc4e2960195f546f406fed Mon Sep 17 00:00:00 2001 From: John Dorlus Date: Wed, 1 Jul 2020 21:04:03 -0400 Subject: [PATCH 17/34] Fixed assertion in hybrid index pattern test to iterate through indices (#70130) * Fixed assertion to check through all indices before making assertion. * Restored yarn.lock file. * Removed only from describe. * Fixed linting issue. * Fixed nits that were in the PR conversation. Co-authored-by: Elastic Machine --- test/functional/page_objects/settings_page.ts | 7 +++++++ .../apps/rollup_job/hybrid_index_pattern.js | 11 +++++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/test/functional/page_objects/settings_page.ts b/test/functional/page_objects/settings_page.ts index fb40b946d7fa3..4b80647c8749d 100644 --- a/test/functional/page_objects/settings_page.ts +++ b/test/functional/page_objects/settings_page.ts @@ -303,6 +303,13 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider ); } + async getAllIndexPatternNames() { + const indexPatterns = await this.getIndexPatternList(); + return await mapAsync(indexPatterns, async (index) => { + return await index.getVisibleText(); + }); + } + async isIndexPatternListEmpty() { await testSubjects.existOrFail('indexPatternTable', { timeout: 5000 }); const indexPatternList = await this.getIndexPatternList(); diff --git a/x-pack/test/functional/apps/rollup_job/hybrid_index_pattern.js b/x-pack/test/functional/apps/rollup_job/hybrid_index_pattern.js index ea98df7163596..d6ed6ce13391e 100644 --- a/x-pack/test/functional/apps/rollup_job/hybrid_index_pattern.js +++ b/x-pack/test/functional/apps/rollup_job/hybrid_index_pattern.js @@ -81,10 +81,13 @@ export default function ({ getService, getPageObjects }) { await PageObjects.settings.createIndexPattern(rollupIndexPatternName, '@timestamp', false); await PageObjects.settings.clickKibanaIndexPatterns(); - const indexPattern = (await PageObjects.settings.getIndexPatternList()).pop(); - const indexPatternText = await indexPattern.getVisibleText(); - expect(indexPatternText).to.contain(rollupIndexPatternName); - expect(indexPatternText).to.contain('Rollup'); + const indexPatternNames = await PageObjects.settings.getAllIndexPatternNames(); + //The assertion is going to check that the string has the right name and that the text Rollup + //is included (since there is a Rollup tag). + const filteredIndexPatternNames = indexPatternNames.filter( + (i) => i.includes(rollupIndexPatternName) && i.includes('Rollup') + ); + expect(filteredIndexPatternNames.length).to.be(1); }); after(async () => { From e9b81f72ca7f8222c1a97fdad965f441f224e0b7 Mon Sep 17 00:00:00 2001 From: nnamdifrankie <56440728+nnamdifrankie@users.noreply.github.com> Date: Wed, 1 Jul 2020 22:49:56 -0400 Subject: [PATCH 18/34] SECURITY-ENDPOINT: add fields for events to metadata document (#70491) SECURITY-ENDPOINT: EMT-492 add fields for events to metadata document --- .../common/endpoint/generate_data.ts | 11 +++++++++-- .../security_solution/common/endpoint/types.ts | 11 +++++++++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts index c075e1041973b..a6fe12a9b029f 100644 --- a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts @@ -391,6 +391,13 @@ export class EndpointDocGenerator { '@timestamp': ts, event: { created: ts, + id: this.seededUUIDv4(), + kind: 'metric', + category: ['host'], + type: ['info'], + module: 'endpoint', + action: 'endpoint_metadata', + dataset: 'endpoint.metadata', }, ...this.commonInfo, }; @@ -1225,8 +1232,8 @@ export class EndpointDocGenerator { created: ts, id: this.seededUUIDv4(), kind: 'state', - category: 'host', - type: 'change', + category: ['host'], + type: ['change'], module: 'endpoint', action: 'endpoint_policy_response', dataset: 'endpoint.policy', diff --git a/x-pack/plugins/security_solution/common/endpoint/types.ts b/x-pack/plugins/security_solution/common/endpoint/types.ts index ca5cc449a7ad7..f76da977eef85 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types.ts @@ -399,6 +399,13 @@ export type HostMetadata = Immutable<{ '@timestamp': number; event: { created: number; + kind: string; + id: string; + category: string[]; + type: string[]; + module: string; + action: string; + dataset: string; }; elastic: { agent: { @@ -771,8 +778,8 @@ export interface HostPolicyResponse { created: number; kind: string; id: string; - category: string; - type: string; + category: string[]; + type: string[]; module: string; action: string; dataset: string; From 591e10355aa5e9fe590361fd22483257d146b9ba Mon Sep 17 00:00:00 2001 From: Garrett Spong Date: Wed, 1 Jul 2020 22:49:30 -0600 Subject: [PATCH 19/34] [Security] Adds field mapping support to rule creation (#70288) ## Summary Resolves: https://github.com/elastic/kibana/issues/65941, https://github.com/elastic/kibana/issues/66317, and `Add support for "building block" alerts` This PR is `Part I` and adds additional fields to the `rules schema` in supporting the ability to map and override fields when generating alerts. A few bookkeeping fields like `license` and `author` have been added as well. The new fields are as follows: ``` ts export interface TheseAreTheNewFields { author: string[]; building_block_type: string; // 'default' license: string; risk_score_mapping: Array< { field: string; operator: string; // 'equals' value: string; } >; rule_name_override: string; severity_mapping: Array< { field: string; operator: string; // 'equals' value: string; severity: string; // 'low' | 'medium' | 'high' | 'critical' } >; timestamp_override: string; } ``` These new fields are exposed as additional settings on the `About rule` section of the Rule Creation UI. ##### Default collapsed view, no severity or risk score override specified:

##### Severity & risk score override specified:

##### Additional fields in Advanced settings:

Note: This PR adds the fields to the `Rules Schema`, the `signals index mapping`, and creates the UI for adding these fields during Rule Creation/Editing. The follow-up `Part II` will add the business logic for mapping fields during `rule execution`, and also add UI validation/additional tests. ### Checklist Delete any items that are not applicable to this PR. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md) - [ ] [Documentation](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#writing-documentation) was added for features that require explanation or tutorials - Syncing w/ @benskelker - [x] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios - [x] This was checked for [keyboard-only and screenreader accessibility](https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Cross_browser_testing/Accessibility#Accessibility_testing_checklist) ### For maintainers - [x] This was checked for breaking API changes and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process) --- .../schemas/common/schemas.ts | 71 ++++++ .../add_prepackaged_rules_schema.mock.ts | 3 + .../request/add_prepackaged_rules_schema.ts | 22 ++ .../add_prepackged_rules_schema.test.ts | 24 ++ .../request/create_rules_schema.mock.ts | 3 + .../request/create_rules_schema.test.ts | 36 +++ .../schemas/request/create_rules_schema.ts | 22 ++ .../request/import_rules_schema.mock.ts | 3 + .../request/import_rules_schema.test.ts | 30 +++ .../schemas/request/import_rules_schema.ts | 22 ++ .../schemas/request/patch_rules_schema.ts | 14 ++ .../request/update_rules_schema.mock.ts | 3 + .../request/update_rules_schema.test.ts | 30 +++ .../schemas/request/update_rules_schema.ts | 22 ++ .../schemas/response/rules_schema.mocks.ts | 3 + .../schemas/response/rules_schema.ts | 16 ++ .../types/default_risk_score_mapping_array.ts | 24 ++ .../types/default_severity_mapping_array.ts | 24 ++ .../detection_engine/schemas/types/index.ts | 2 + .../rules/description_step/index.test.tsx | 2 +- .../rules/description_step/index.tsx | 19 +- .../rules/risk_score_mapping/index.tsx | 190 ++++++++++++++++ .../rules/risk_score_mapping/translations.tsx | 57 +++++ .../rules/severity_mapping/index.tsx | 214 ++++++++++++++++++ .../rules/severity_mapping/translations.tsx | 57 +++++ .../components/rules/step_about_rule/data.tsx | 2 +- .../rules/step_about_rule/default_value.ts | 9 +- .../rules/step_about_rule/index.test.tsx | 18 +- .../rules/step_about_rule/index.tsx | 184 ++++++++++----- .../rules/step_about_rule/schema.tsx | 123 ++++++++-- .../detection_engine/rules/api.test.ts | 2 +- .../containers/detection_engine/rules/mock.ts | 9 + .../detection_engine/rules/types.ts | 18 ++ .../detection_engine/rules/use_rule.test.tsx | 3 + .../rules/use_rule_status.test.tsx | 3 + .../detection_engine/rules/use_rules.test.tsx | 6 + .../rules/all/__mocks__/mock.ts | 19 +- .../rules/create/helpers.test.ts | 24 ++ .../detection_engine/rules/create/helpers.ts | 27 ++- .../detection_engine/rules/create/index.tsx | 3 + .../detection_engine/rules/helpers.test.tsx | 9 +- .../pages/detection_engine/rules/helpers.tsx | 22 +- .../pages/detection_engine/rules/types.ts | 35 ++- .../routes/__mocks__/request_responses.ts | 7 + .../routes/__mocks__/utils.ts | 4 + .../routes/index/signals_mapping.json | 44 ++++ .../rules/add_prepackaged_rules_route.test.ts | 3 + .../routes/rules/create_rules_bulk_route.ts | 16 +- .../routes/rules/create_rules_route.ts | 16 +- .../routes/rules/import_rules_route.ts | 23 +- .../routes/rules/patch_rules_bulk_route.ts | 14 ++ .../routes/rules/patch_rules_route.ts | 14 ++ .../routes/rules/update_rules_bulk_route.ts | 14 ++ .../routes/rules/update_rules_route.ts | 14 ++ .../detection_engine/routes/rules/utils.ts | 7 + .../routes/rules/validate.test.ts | 4 + .../rules/create_rules.mock.ts | 14 ++ .../detection_engine/rules/create_rules.ts | 14 ++ .../create_rules_stream_from_ndjson.test.ts | 30 +++ .../rules/get_export_all.test.ts | 4 + .../rules/get_export_by_object_ids.test.ts | 8 + .../rules/install_prepacked_rules.ts | 14 ++ .../rules/patch_rules.mock.ts | 14 ++ .../lib/detection_engine/rules/patch_rules.ts | 21 ++ .../lib/detection_engine/rules/types.ts | 31 +++ .../rules/update_prepacked_rules.ts | 14 ++ .../rules/update_rules.mock.ts | 14 ++ .../detection_engine/rules/update_rules.ts | 21 ++ .../lib/detection_engine/rules/utils.test.ts | 21 ++ .../lib/detection_engine/rules/utils.ts | 14 ++ .../lib/detection_engine/scripts/post_rule.sh | 2 +- .../rules/queries/query_with_mappings.json | 44 ++++ .../signals/__mocks__/es_results.ts | 7 + .../signals/build_bulk_body.test.ts | 20 ++ .../signals/build_rule.test.ts | 15 ++ .../detection_engine/signals/build_rule.ts | 13 +- .../signals/signal_params_schema.mock.ts | 7 + .../signals/signal_params_schema.ts | 8 + .../server/lib/detection_engine/types.ts | 14 ++ .../detection_engine_api_integration/utils.ts | 9 + 80 files changed, 1868 insertions(+), 114 deletions(-) create mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_risk_score_mapping_array.ts create mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_severity_mapping_array.ts create mode 100644 x-pack/plugins/security_solution/public/alerts/components/rules/risk_score_mapping/index.tsx create mode 100644 x-pack/plugins/security_solution/public/alerts/components/rules/risk_score_mapping/translations.tsx create mode 100644 x-pack/plugins/security_solution/public/alerts/components/rules/severity_mapping/index.tsx create mode 100644 x-pack/plugins/security_solution/public/alerts/components/rules/severity_mapping/translations.tsx create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/query_with_mappings.json 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 f6b732cd1f64e..6e43bd645fd7b 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 @@ -13,6 +13,18 @@ import { IsoDateString } from '../types/iso_date_string'; import { PositiveIntegerGreaterThanZero } from '../types/positive_integer_greater_than_zero'; import { PositiveInteger } from '../types/positive_integer'; +export const author = t.array(t.string); +export type Author = t.TypeOf; + +export const authorOrUndefined = t.union([author, t.undefined]); +export type AuthorOrUndefined = t.TypeOf; + +export const building_block_type = t.string; +export type BuildingBlockType = t.TypeOf; + +export const buildingBlockTypeOrUndefined = t.union([building_block_type, t.undefined]); +export type BuildingBlockTypeOrUndefined = t.TypeOf; + export const description = t.string; export type Description = t.TypeOf; @@ -111,6 +123,12 @@ export type Language = t.TypeOf; export const languageOrUndefined = t.union([language, t.undefined]); export type LanguageOrUndefined = t.TypeOf; +export const license = t.string; +export type License = t.TypeOf; + +export const licenseOrUndefined = t.union([license, t.undefined]); +export type LicenseOrUndefined = t.TypeOf; + export const objects = t.array(t.type({ rule_id })); export const output_index = t.string; @@ -137,6 +155,12 @@ export type TimelineTitle = t.TypeOf; export const timelineTitleOrUndefined = t.union([timeline_title, t.undefined]); export type TimelineTitleOrUndefined = t.TypeOf; +export const timestamp_override = t.string; +export type TimestampOverride = t.TypeOf; + +export const timestampOverrideOrUndefined = t.union([timestamp_override, t.undefined]); +export type TimestampOverrideOrUndefined = t.TypeOf; + export const throttle = t.string; export type Throttle = t.TypeOf; @@ -179,18 +203,65 @@ export type Name = t.TypeOf; export const nameOrUndefined = t.union([name, t.undefined]); export type NameOrUndefined = t.TypeOf; +export const operator = t.keyof({ + equals: null, +}); +export type Operator = t.TypeOf; +export enum OperatorEnum { + EQUALS = 'equals', +} + export const risk_score = RiskScore; export type RiskScore = t.TypeOf; export const riskScoreOrUndefined = t.union([risk_score, 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, + operator, + value: risk_score_mapping_value, + }) +); + +export const risk_score_mapping = t.array(risk_score_mapping_item); +export type RiskScoreMapping = t.TypeOf; + +export const riskScoreMappingOrUndefined = t.union([risk_score_mapping, t.undefined]); +export type RiskScoreMappingOrUndefined = t.TypeOf; + +export const rule_name_override = t.string; +export type RuleNameOverride = t.TypeOf; + +export const ruleNameOverrideOrUndefined = t.union([rule_name_override, t.undefined]); +export type RuleNameOverrideOrUndefined = t.TypeOf; + 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; +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 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; + export const status = t.keyof({ open: null, closed: null, 'in-progress': null }); export type Status = t.TypeOf; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.mock.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.mock.ts index 52a210f3a01aa..b666b95ea1e97 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.mock.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.mock.ts @@ -23,12 +23,15 @@ export const getAddPrepackagedRulesSchemaMock = (): AddPrepackagedRulesSchema => }); export const getAddPrepackagedRulesSchemaDecodedMock = (): AddPrepackagedRulesSchemaDecoded => ({ + author: [], description: 'some description', name: 'Query with a rule id', query: 'user.name: root or user.name: admin', severity: 'high', + severity_mapping: [], type: 'query', risk_score: 55, + risk_score_mapping: [], language: 'kuery', references: [], actions: [], diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.ts index 43000f6d36f46..bf96be5e688fa 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.ts @@ -37,6 +37,13 @@ import { query, rule_id, version, + building_block_type, + license, + rule_name_override, + timestamp_override, + Author, + RiskScoreMapping, + SeverityMapping, } from '../common/schemas'; /* eslint-enable @typescript-eslint/camelcase */ @@ -52,6 +59,8 @@ import { DefaultThrottleNull, DefaultListArray, ListArray, + DefaultRiskScoreMappingArray, + DefaultSeverityMappingArray, } from '../types'; /** @@ -79,6 +88,8 @@ export const addPrepackagedRulesSchema = t.intersection([ t.partial({ actions: DefaultActionsArray, // defaults to empty actions array if not set during decode anomaly_threshold, // defaults to undefined if not set during decode + author: DefaultStringArray, // defaults to empty array of strings if not set during decode + building_block_type, // defaults to undefined if not set during decode enabled: DefaultBooleanFalse, // defaults to false if not set during decode false_positives: DefaultStringArray, // defaults to empty string array if not set during decode filters, // defaults to undefined if not set during decode @@ -87,16 +98,21 @@ export const addPrepackagedRulesSchema = t.intersection([ interval: DefaultIntervalString, // defaults to "5m" if not set during decode query, // defaults to undefined if not set during decode language, // defaults to undefined if not set during decode + license, // defaults to "undefined" if not set during decode saved_id, // defaults to "undefined" if not set during decode timeline_id, // defaults to "undefined" if not set during decode timeline_title, // defaults to "undefined" if not set during decode meta, // defaults to "undefined" if not set during decode machine_learning_job_id, // defaults to "undefined" if not set during decode max_signals: DefaultMaxSignalsNumber, // defaults to DEFAULT_MAX_SIGNALS (100) if not set during decode + risk_score_mapping: DefaultRiskScoreMappingArray, // defaults to empty risk score mapping array if not set during decode + rule_name_override, // defaults to "undefined" if not set during decode + severity_mapping: DefaultSeverityMappingArray, // defaults to empty actions array if not set during decode tags: DefaultStringArray, // defaults to empty string array if not set during decode to: DefaultToString, // defaults to "now" if not set during decode threat: DefaultThreatArray, // defaults to empty array if not set during decode throttle: DefaultThrottleNull, // defaults to "null" if not set during decode + timestamp_override, // defaults to "undefined" if not set during decode references: DefaultStringArray, // defaults to empty array of strings if not set during decode note, // defaults to "undefined" if not set during decode exceptions_list: DefaultListArray, // defaults to empty array if not set during decode @@ -109,6 +125,7 @@ export type AddPrepackagedRulesSchema = t.TypeOf & { + author: Author; references: References; actions: Actions; enabled: Enabled; @@ -129,6 +149,8 @@ export type AddPrepackagedRulesSchemaDecoded = Omit< from: From; interval: Interval; max_signals: MaxSignals; + risk_score_mapping: RiskScoreMapping; + severity_mapping: SeverityMapping; tags: Tags; to: To; threat: Threat; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackged_rules_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackged_rules_schema.test.ts index 47a98166927b4..0c45a7b1ef6bb 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackged_rules_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackged_rules_schema.test.ts @@ -261,6 +261,9 @@ describe('add prepackaged rules schema', () => { const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); const expected: AddPrepackagedRulesSchemaDecoded = { + author: [], + severity_mapping: [], + risk_score_mapping: [], rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -333,6 +336,9 @@ describe('add prepackaged rules schema', () => { const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); const expected: AddPrepackagedRulesSchemaDecoded = { + author: [], + severity_mapping: [], + risk_score_mapping: [], rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -430,6 +436,9 @@ describe('add prepackaged rules schema', () => { const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); const expected: AddPrepackagedRulesSchemaDecoded = { + author: [], + severity_mapping: [], + risk_score_mapping: [], rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -508,6 +517,9 @@ describe('add prepackaged rules schema', () => { const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); const expected: AddPrepackagedRulesSchemaDecoded = { + author: [], + severity_mapping: [], + risk_score_mapping: [], rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -1354,6 +1366,9 @@ describe('add prepackaged rules schema', () => { const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); const expected: AddPrepackagedRulesSchemaDecoded = { + author: [], + severity_mapping: [], + risk_score_mapping: [], rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1404,6 +1419,9 @@ describe('add prepackaged rules schema', () => { const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); const expected: AddPrepackagedRulesSchemaDecoded = { + author: [], + severity_mapping: [], + risk_score_mapping: [], rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1462,6 +1480,9 @@ describe('add prepackaged rules schema', () => { const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); const expected: AddPrepackagedRulesSchemaDecoded = { + author: [], + severity_mapping: [], + risk_score_mapping: [], rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1539,6 +1560,9 @@ describe('add prepackaged rules schema', () => { const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); const expected: AddPrepackagedRulesSchemaDecoded = { + author: [], + severity_mapping: [], + risk_score_mapping: [], rule_id: 'rule-1', description: 'some description', from: 'now-5m', diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.mock.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.mock.ts index 2847bd32df514..f1e87bdb11e75 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.mock.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.mock.ts @@ -30,6 +30,9 @@ export const getCreateMlRulesSchemaMock = (ruleId = 'rule-1') => { }; export const getCreateRulesSchemaDecodedMock = (): CreateRulesSchemaDecoded => ({ + author: [], + severity_mapping: [], + risk_score_mapping: [], description: 'Detecting root and admin users', name: 'Query with a rule id', query: 'user.name: root or user.name: admin', diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.test.ts index 1648044f5305a..e529cf3fa555c 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.test.ts @@ -248,6 +248,9 @@ describe('create rules schema', () => { const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); const expected: CreateRulesSchemaDecoded = { + author: [], + severity_mapping: [], + risk_score_mapping: [], rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -318,6 +321,9 @@ describe('create rules schema', () => { const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); const expected: CreateRulesSchemaDecoded = { + author: [], + severity_mapping: [], + risk_score_mapping: [], rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -366,6 +372,9 @@ describe('create rules schema', () => { const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); const expected: CreateRulesSchemaDecoded = { + author: [], + severity_mapping: [], + risk_score_mapping: [], rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -412,6 +421,9 @@ describe('create rules schema', () => { const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); const expected: CreateRulesSchemaDecoded = { + author: [], + severity_mapping: [], + risk_score_mapping: [], rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -438,6 +450,9 @@ describe('create rules schema', () => { test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, output_index] does validate', () => { const payload: CreateRulesSchema = { + author: [], + severity_mapping: [], + risk_score_mapping: [], rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -456,6 +471,9 @@ describe('create rules schema', () => { const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); const expected: CreateRulesSchemaDecoded = { + author: [], + severity_mapping: [], + risk_score_mapping: [], rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -535,6 +553,9 @@ describe('create rules schema', () => { const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); const expected: CreateRulesSchemaDecoded = { + author: [], + severity_mapping: [], + risk_score_mapping: [], rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -1228,6 +1249,9 @@ describe('create rules schema', () => { const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); const expected: CreateRulesSchemaDecoded = { + author: [], + severity_mapping: [], + risk_score_mapping: [], rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1399,6 +1423,9 @@ describe('create rules schema', () => { const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); const expected: CreateRulesSchemaDecoded = { + author: [], + severity_mapping: [], + risk_score_mapping: [], type: 'machine_learning', anomaly_threshold: 50, machine_learning_job_id: 'linux_anomalous_network_activity_ecs', @@ -1459,6 +1486,9 @@ describe('create rules schema', () => { const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); const expected: CreateRulesSchemaDecoded = { + author: [], + severity_mapping: [], + risk_score_mapping: [], rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1516,6 +1546,9 @@ describe('create rules schema', () => { const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); const expected: CreateRulesSchemaDecoded = { + author: [], + severity_mapping: [], + risk_score_mapping: [], rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1591,6 +1624,9 @@ describe('create rules schema', () => { const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); const expected: CreateRulesSchemaDecoded = { + author: [], + severity_mapping: [], + risk_score_mapping: [], rule_id: 'rule-1', description: 'some description', from: 'now-5m', diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.ts index d623cff8f1fc3..0debe01e5a4d7 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/create_rules_schema.ts @@ -10,6 +10,7 @@ import * as t from 'io-ts'; import { description, anomaly_threshold, + building_block_type, filters, RuleId, index, @@ -38,6 +39,12 @@ import { Interval, language, query, + license, + rule_name_override, + timestamp_override, + Author, + RiskScoreMapping, + SeverityMapping, } from '../common/schemas'; /* eslint-enable @typescript-eslint/camelcase */ @@ -55,6 +62,8 @@ import { DefaultListArray, ListArray, DefaultUuid, + DefaultRiskScoreMappingArray, + DefaultSeverityMappingArray, } from '../types'; export const createRulesSchema = t.intersection([ @@ -71,6 +80,8 @@ export const createRulesSchema = t.intersection([ t.partial({ actions: DefaultActionsArray, // defaults to empty actions array if not set during decode anomaly_threshold, // defaults to undefined if not set during decode + author: DefaultStringArray, // defaults to empty array of strings if not set during decode + building_block_type, // defaults to undefined if not set during decode enabled: DefaultBooleanTrue, // defaults to true if not set during decode false_positives: DefaultStringArray, // defaults to empty string array if not set during decode filters, // defaults to undefined if not set during decode @@ -80,6 +91,7 @@ export const createRulesSchema = t.intersection([ interval: DefaultIntervalString, // defaults to "5m" if not set during decode query, // defaults to undefined if not set during decode language, // defaults to undefined if not set during decode + license, // defaults to "undefined" if not set during decode // TODO: output_index: This should be removed eventually output_index, // defaults to "undefined" if not set during decode saved_id, // defaults to "undefined" if not set during decode @@ -88,10 +100,14 @@ export const createRulesSchema = t.intersection([ meta, // defaults to "undefined" if not set during decode machine_learning_job_id, // defaults to "undefined" if not set during decode max_signals: DefaultMaxSignalsNumber, // defaults to DEFAULT_MAX_SIGNALS (100) if not set during decode + risk_score_mapping: DefaultRiskScoreMappingArray, // defaults to empty risk score mapping array if not set during decode + rule_name_override, // defaults to "undefined" if not set during decode + severity_mapping: DefaultSeverityMappingArray, // defaults to empty actions array if not set during decode tags: DefaultStringArray, // defaults to empty string array if not set during decode to: DefaultToString, // defaults to "now" if not set during decode threat: DefaultThreatArray, // defaults to empty array if not set during decode throttle: DefaultThrottleNull, // defaults to "null" if not set during decode + timestamp_override, // defaults to "undefined" if not set during decode references: DefaultStringArray, // defaults to empty array of strings if not set during decode note, // defaults to "undefined" if not set during decode version: DefaultVersionNumber, // defaults to 1 if not set during decode @@ -105,6 +121,7 @@ export type CreateRulesSchema = t.TypeOf; // This type is used after a decode since some things are defaults after a decode. export type CreateRulesSchemaDecoded = Omit< CreateRulesSchema, + | 'author' | 'references' | 'actions' | 'enabled' @@ -112,6 +129,8 @@ export type CreateRulesSchemaDecoded = Omit< | 'from' | 'interval' | 'max_signals' + | 'risk_score_mapping' + | 'severity_mapping' | 'tags' | 'to' | 'threat' @@ -120,6 +139,7 @@ export type CreateRulesSchemaDecoded = Omit< | 'exceptions_list' | 'rule_id' > & { + author: Author; references: References; actions: Actions; enabled: Enabled; @@ -127,6 +147,8 @@ export type CreateRulesSchemaDecoded = Omit< from: From; interval: Interval; max_signals: MaxSignals; + risk_score_mapping: RiskScoreMapping; + severity_mapping: SeverityMapping; tags: Tags; to: To; threat: Threat; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.mock.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.mock.ts index aaeb90ffc5bcf..e3b4196c90c6c 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.mock.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.mock.ts @@ -31,12 +31,15 @@ export const getImportRulesWithIdSchemaMock = (ruleId = 'rule-1'): ImportRulesSc }); export const getImportRulesSchemaDecodedMock = (): ImportRulesSchemaDecoded => ({ + author: [], description: 'some description', name: 'Query with a rule id', query: 'user.name: root or user.name: admin', severity: 'high', + severity_mapping: [], type: 'query', risk_score: 55, + risk_score_mapping: [], language: 'kuery', references: [], actions: [], diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.test.ts index 12a13ab1a5ed1..bbf0a8debd651 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.test.ts @@ -253,6 +253,9 @@ describe('import rules schema', () => { const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); const expected: ImportRulesSchemaDecoded = { + author: [], + severity_mapping: [], + risk_score_mapping: [], rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -324,6 +327,9 @@ describe('import rules schema', () => { const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); const expected: ImportRulesSchemaDecoded = { + author: [], + severity_mapping: [], + risk_score_mapping: [], rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -373,6 +379,9 @@ describe('import rules schema', () => { const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); const expected: ImportRulesSchemaDecoded = { + author: [], + severity_mapping: [], + risk_score_mapping: [], rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -420,6 +429,9 @@ describe('import rules schema', () => { const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); const expected: ImportRulesSchemaDecoded = { + author: [], + severity_mapping: [], + risk_score_mapping: [], rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -465,6 +477,9 @@ describe('import rules schema', () => { const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); const expected: ImportRulesSchemaDecoded = { + author: [], + severity_mapping: [], + risk_score_mapping: [], rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -545,6 +560,9 @@ describe('import rules schema', () => { const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); const expected: ImportRulesSchemaDecoded = { + author: [], + severity_mapping: [], + risk_score_mapping: [], rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -1543,6 +1561,9 @@ describe('import rules schema', () => { const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); const expected: ImportRulesSchemaDecoded = { + author: [], + severity_mapping: [], + risk_score_mapping: [], rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1593,6 +1614,9 @@ describe('import rules schema', () => { const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); const expected: ImportRulesSchemaDecoded = { + author: [], + severity_mapping: [], + risk_score_mapping: [], rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1651,6 +1675,9 @@ describe('import rules schema', () => { const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); const expected: ImportRulesSchemaDecoded = { + author: [], + severity_mapping: [], + risk_score_mapping: [], rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1727,6 +1754,9 @@ describe('import rules schema', () => { const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); const expected: ImportRulesSchemaDecoded = { + author: [], + severity_mapping: [], + risk_score_mapping: [], rule_id: 'rule-1', description: 'some description', from: 'now-5m', diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.ts index 7d79861aacf38..f61a1546e3e8a 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.ts @@ -44,6 +44,13 @@ import { updated_at, created_by, updated_by, + building_block_type, + license, + rule_name_override, + timestamp_override, + Author, + RiskScoreMapping, + SeverityMapping, } from '../common/schemas'; /* eslint-enable @typescript-eslint/camelcase */ @@ -62,6 +69,8 @@ import { DefaultStringBooleanFalse, DefaultListArray, ListArray, + DefaultRiskScoreMappingArray, + DefaultSeverityMappingArray, } from '../types'; /** @@ -90,6 +99,8 @@ export const importRulesSchema = t.intersection([ id, // defaults to undefined if not set during decode actions: DefaultActionsArray, // defaults to empty actions array if not set during decode anomaly_threshold, // defaults to undefined if not set during decode + author: DefaultStringArray, // defaults to empty array of strings if not set during decode + building_block_type, // defaults to undefined if not set during decode enabled: DefaultBooleanTrue, // defaults to true if not set during decode false_positives: DefaultStringArray, // defaults to empty string array if not set during decode filters, // defaults to undefined if not set during decode @@ -99,6 +110,7 @@ export const importRulesSchema = t.intersection([ interval: DefaultIntervalString, // defaults to "5m" if not set during decode query, // defaults to undefined if not set during decode language, // defaults to undefined if not set during decode + license, // defaults to "undefined" if not set during decode // TODO: output_index: This should be removed eventually output_index, // defaults to "undefined" if not set during decode saved_id, // defaults to "undefined" if not set during decode @@ -107,10 +119,14 @@ export const importRulesSchema = t.intersection([ meta, // defaults to "undefined" if not set during decode machine_learning_job_id, // defaults to "undefined" if not set during decode max_signals: DefaultMaxSignalsNumber, // defaults to DEFAULT_MAX_SIGNALS (100) if not set during decode + risk_score_mapping: DefaultRiskScoreMappingArray, // defaults to empty risk score mapping array if not set during decode + rule_name_override, // defaults to "undefined" if not set during decode + severity_mapping: DefaultSeverityMappingArray, // defaults to empty actions array if not set during decode tags: DefaultStringArray, // defaults to empty string array if not set during decode to: DefaultToString, // defaults to "now" if not set during decode threat: DefaultThreatArray, // defaults to empty array if not set during decode throttle: DefaultThrottleNull, // defaults to "null" if not set during decode + timestamp_override, // defaults to "undefined" if not set during decode references: DefaultStringArray, // defaults to empty array of strings if not set during decode note, // defaults to "undefined" if not set during decode version: DefaultVersionNumber, // defaults to 1 if not set during decode @@ -128,6 +144,7 @@ export type ImportRulesSchema = t.TypeOf; // This type is used after a decode since some things are defaults after a decode. export type ImportRulesSchemaDecoded = Omit< ImportRulesSchema, + | 'author' | 'references' | 'actions' | 'enabled' @@ -135,6 +152,8 @@ export type ImportRulesSchemaDecoded = Omit< | 'from' | 'interval' | 'max_signals' + | 'risk_score_mapping' + | 'severity_mapping' | 'tags' | 'to' | 'threat' @@ -144,6 +163,7 @@ export type ImportRulesSchemaDecoded = Omit< | 'rule_id' | 'immutable' > & { + author: Author; references: References; actions: Actions; enabled: Enabled; @@ -151,6 +171,8 @@ export type ImportRulesSchemaDecoded = Omit< from: From; interval: Interval; max_signals: MaxSignals; + risk_score_mapping: RiskScoreMapping; + severity_mapping: SeverityMapping; tags: Tags; to: To; threat: Threat; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.ts index 29d5467071a3d..070f3ccfd03b0 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.ts @@ -39,6 +39,13 @@ import { language, query, id, + building_block_type, + author, + license, + rule_name_override, + timestamp_override, + risk_score_mapping, + severity_mapping, } from '../common/schemas'; import { listArrayOrUndefined } from '../types/lists'; /* eslint-enable @typescript-eslint/camelcase */ @@ -48,6 +55,8 @@ import { listArrayOrUndefined } from '../types/lists'; */ export const patchRulesSchema = t.exact( t.partial({ + author, + building_block_type, description, risk_score, name, @@ -65,6 +74,7 @@ export const patchRulesSchema = t.exact( interval, query, language, + license, // TODO: output_index: This should be removed eventually output_index, saved_id, @@ -73,10 +83,14 @@ export const patchRulesSchema = t.exact( meta, machine_learning_job_id, max_signals, + risk_score_mapping, + rule_name_override, + severity_mapping, tags, to, threat, throttle, + timestamp_override, references, note, version, diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.mock.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.mock.ts index b8a99115ba7d5..b3fbf96188352 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.mock.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.mock.ts @@ -19,12 +19,15 @@ export const getUpdateRulesSchemaMock = (): UpdateRulesSchema => ({ }); export const getUpdateRulesSchemaDecodedMock = (): UpdateRulesSchemaDecoded => ({ + author: [], description: 'some description', name: 'Query with a rule id', query: 'user.name: root or user.name: admin', severity: 'high', + severity_mapping: [], type: 'query', risk_score: 55, + risk_score_mapping: [], language: 'kuery', references: [], actions: [], diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.test.ts index 02f8e7bbeb59b..c15803eee874e 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.test.ts @@ -248,6 +248,9 @@ describe('update rules schema', () => { const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); const expected: UpdateRulesSchemaDecoded = { + author: [], + severity_mapping: [], + risk_score_mapping: [], rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -317,6 +320,9 @@ describe('update rules schema', () => { const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); const expected: UpdateRulesSchemaDecoded = { + author: [], + severity_mapping: [], + risk_score_mapping: [], rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -364,6 +370,9 @@ describe('update rules schema', () => { const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); const expected: UpdateRulesSchemaDecoded = { + author: [], + severity_mapping: [], + risk_score_mapping: [], rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -409,6 +418,9 @@ describe('update rules schema', () => { const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); const expected: UpdateRulesSchemaDecoded = { + author: [], + severity_mapping: [], + risk_score_mapping: [], rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -452,6 +464,9 @@ describe('update rules schema', () => { const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); const expected: UpdateRulesSchemaDecoded = { + author: [], + severity_mapping: [], + risk_score_mapping: [], rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -530,6 +545,9 @@ describe('update rules schema', () => { const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); const expected: UpdateRulesSchemaDecoded = { + author: [], + severity_mapping: [], + risk_score_mapping: [], rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -1353,6 +1371,9 @@ describe('update rules schema', () => { const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); const expected: UpdateRulesSchemaDecoded = { + author: [], + severity_mapping: [], + risk_score_mapping: [], rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1401,6 +1422,9 @@ describe('update rules schema', () => { const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); const expected: UpdateRulesSchemaDecoded = { + author: [], + severity_mapping: [], + risk_score_mapping: [], rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1457,6 +1481,9 @@ describe('update rules schema', () => { const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); const expected: UpdateRulesSchemaDecoded = { + author: [], + severity_mapping: [], + risk_score_mapping: [], rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -1531,6 +1558,9 @@ describe('update rules schema', () => { const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual([]); const expected: UpdateRulesSchemaDecoded = { + author: [], + severity_mapping: [], + risk_score_mapping: [], rule_id: 'rule-1', description: 'some description', from: 'now-5m', diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.ts index 73078e617efc6..98082c2de838a 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/update_rules_schema.ts @@ -40,6 +40,13 @@ import { language, query, id, + building_block_type, + license, + rule_name_override, + timestamp_override, + Author, + RiskScoreMapping, + SeverityMapping, } from '../common/schemas'; /* eslint-enable @typescript-eslint/camelcase */ @@ -55,6 +62,8 @@ import { DefaultThrottleNull, DefaultListArray, ListArray, + DefaultRiskScoreMappingArray, + DefaultSeverityMappingArray, } from '../types'; /** @@ -79,6 +88,8 @@ export const updateRulesSchema = t.intersection([ id, // defaults to "undefined" if not set during decode actions: DefaultActionsArray, // defaults to empty actions array if not set during decode anomaly_threshold, // defaults to undefined if not set during decode + author: DefaultStringArray, // defaults to empty array of strings if not set during decode + building_block_type, // defaults to undefined if not set during decode enabled: DefaultBooleanTrue, // defaults to true if not set during decode false_positives: DefaultStringArray, // defaults to empty string array if not set during decode filters, // defaults to undefined if not set during decode @@ -88,6 +99,7 @@ export const updateRulesSchema = t.intersection([ interval: DefaultIntervalString, // defaults to "5m" if not set during decode query, // defaults to undefined if not set during decode language, // defaults to undefined if not set during decode + license, // defaults to "undefined" if not set during decode // TODO: output_index: This should be removed eventually output_index, // defaults to "undefined" if not set during decode saved_id, // defaults to "undefined" if not set during decode @@ -96,10 +108,14 @@ export const updateRulesSchema = t.intersection([ meta, // defaults to "undefined" if not set during decode machine_learning_job_id, // defaults to "undefined" if not set during decode max_signals: DefaultMaxSignalsNumber, // defaults to DEFAULT_MAX_SIGNALS (100) if not set during decode + risk_score_mapping: DefaultRiskScoreMappingArray, // defaults to empty risk score mapping array if not set during decode + rule_name_override, // defaults to "undefined" if not set during decode + severity_mapping: DefaultSeverityMappingArray, // defaults to empty actions array if not set during decode tags: DefaultStringArray, // defaults to empty string array if not set during decode to: DefaultToString, // defaults to "now" if not set during decode threat: DefaultThreatArray, // defaults to empty array if not set during decode throttle: DefaultThrottleNull, // defaults to "null" if not set during decode + timestamp_override, // defaults to "undefined" if not set during decode references: DefaultStringArray, // defaults to empty array of strings if not set during decode note, // defaults to "undefined" if not set during decode version, // defaults to "undefined" if not set during decode @@ -113,6 +129,7 @@ export type UpdateRulesSchema = t.TypeOf; // This type is used after a decode since some things are defaults after a decode. export type UpdateRulesSchemaDecoded = Omit< UpdateRulesSchema, + | 'author' | 'references' | 'actions' | 'enabled' @@ -120,6 +137,8 @@ export type UpdateRulesSchemaDecoded = Omit< | 'from' | 'interval' | 'max_signals' + | 'risk_score_mapping' + | 'severity_mapping' | 'tags' | 'to' | 'threat' @@ -127,6 +146,7 @@ export type UpdateRulesSchemaDecoded = Omit< | 'exceptions_list' | 'rule_id' > & { + author: Author; references: References; actions: Actions; enabled: Enabled; @@ -134,6 +154,8 @@ export type UpdateRulesSchemaDecoded = Omit< from: From; interval: Interval; max_signals: MaxSignals; + risk_score_mapping: RiskScoreMapping; + severity_mapping: SeverityMapping; tags: Tags; to: To; threat: Threat; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts index e63a7ad981e12..ed9fb8930ea1b 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts @@ -36,6 +36,7 @@ export const getPartialRulesSchemaMock = (): Partial => ({ }); export const getRulesSchemaMock = (anchorDate: string = ANCHOR_DATE): RulesSchema => ({ + author: [], id: '7a7065d7-6e8b-4aae-8d20-c93613dec9f9', created_at: new Date(anchorDate).toISOString(), updated_at: new Date(anchorDate).toISOString(), @@ -49,6 +50,7 @@ export const getRulesSchemaMock = (anchorDate: string = ANCHOR_DATE): RulesSchem query: 'user.name: root or user.name: admin', references: ['test 1', 'test 2'], severity: 'high', + severity_mapping: [], updated_by: 'elastic_kibana', tags: [], to: 'now', @@ -62,6 +64,7 @@ export const getRulesSchemaMock = (anchorDate: string = ANCHOR_DATE): RulesSchem output_index: '.siem-signals-hassanabad-frank-default', max_signals: 100, risk_score: 55, + risk_score_mapping: [], language: 'kuery', rule_id: 'query-rule-id', interval: '5m', diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts index 9803a80f57857..c0fec2b2eefc2 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts @@ -55,8 +55,17 @@ import { filters, meta, note, + building_block_type, + license, + rule_name_override, + timestamp_override, } from '../common/schemas'; import { DefaultListArray } from '../types/lists_default_array'; +import { + DefaultStringArray, + DefaultRiskScoreMappingArray, + DefaultSeverityMappingArray, +} from '../types'; /** * This is the required fields for the rules schema response. Put all required properties on @@ -64,6 +73,7 @@ import { DefaultListArray } from '../types/lists_default_array'; * output schema. */ export const requiredRulesSchema = t.type({ + author: DefaultStringArray, description, enabled, false_positives, @@ -75,9 +85,11 @@ export const requiredRulesSchema = t.type({ output_index, max_signals, risk_score, + risk_score_mapping: DefaultRiskScoreMappingArray, name, references, severity, + severity_mapping: DefaultSeverityMappingArray, updated_by, tags, to, @@ -120,9 +132,13 @@ export const dependentRulesSchema = t.partial({ */ export const partialRulesSchema = t.partial({ actions, + building_block_type, + license, throttle, + rule_name_override, status: job_status, status_date, + timestamp_override, last_success_at, last_success_message, last_failure_at, 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 new file mode 100644 index 0000000000000..ba74045b4e32c --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_risk_score_mapping_array.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; +// eslint-disable-next-line @typescript-eslint/camelcase +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 + */ +export const DefaultRiskScoreMappingArray = new t.Type( + 'DefaultRiskScoreMappingArray', + risk_score_mapping.is, + (input, context): Either => + input == null ? t.success([]) : risk_score_mapping.validate(input, context), + t.identity +); + +export type DefaultRiskScoreMappingArrayC = typeof DefaultRiskScoreMappingArray; 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 new file mode 100644 index 0000000000000..8e68b73148af1 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/default_severity_mapping_array.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; +// eslint-disable-next-line @typescript-eslint/camelcase +import { severity_mapping, SeverityMapping } from '../common/schemas'; + +/** + * Types the DefaultStringArray as: + * - If null or undefined, then a default severity_mapping array will be set + */ +export const DefaultSeverityMappingArray = new t.Type( + 'DefaultSeverityMappingArray', + severity_mapping.is, + (input, context): Either => + input == null ? t.success([]) : severity_mapping.validate(input, context), + t.identity +); + +export type DefaultSeverityMappingArrayC = typeof DefaultSeverityMappingArray; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/index.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/index.ts index 368dd4922eec4..aab9a550d25e7 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/index.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/index.ts @@ -15,6 +15,8 @@ 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'; diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/description_step/index.test.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/description_step/index.test.tsx index 2bd90f17daf0c..0a7e666d65aef 100644 --- a/x-pack/plugins/security_solution/public/alerts/components/rules/description_step/index.test.tsx +++ b/x-pack/plugins/security_solution/public/alerts/components/rules/description_step/index.test.tsx @@ -257,7 +257,7 @@ describe('description_step', () => { test('returns expected ListItems array when given valid inputs', () => { const result: ListItems[] = buildListItems(mockAboutStep, schema, mockFilterManager); - expect(result.length).toEqual(9); + expect(result.length).toEqual(11); }); }); diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/description_step/index.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/description_step/index.tsx index b9642b8761019..8f3a76c6aea57 100644 --- a/x-pack/plugins/security_solution/public/alerts/components/rules/description_step/index.tsx +++ b/x-pack/plugins/security_solution/public/alerts/components/rules/description_step/index.tsx @@ -18,7 +18,11 @@ import { } from '../../../../../../../../src/plugins/data/public'; import { DEFAULT_TIMELINE_TITLE } from '../../../../timelines/components/timeline/translations'; import { useKibana } from '../../../../common/lib/kibana'; -import { IMitreEnterpriseAttack } from '../../../pages/detection_engine/rules/types'; +import { + AboutStepRiskScore, + AboutStepSeverity, + IMitreEnterpriseAttack, +} from '../../../pages/detection_engine/rules/types'; import { FieldValueTimeline } from '../pick_timeline'; import { FormSchema } from '../../../../shared_imports'; import { ListItems } from './types'; @@ -184,9 +188,18 @@ export const getDescriptionItem = ( } else if (Array.isArray(get(field, data))) { const values: string[] = get(field, data); return buildStringArrayDescription(label, field, values); + // TODO: Add custom UI for Risk/Severity Mappings (and fix missing label) + } else if (field === 'riskScore') { + const val: AboutStepRiskScore = get(field, data); + return [ + { + title: label, + description: val.value, + }, + ]; } else if (field === 'severity') { - const val: string = get(field, data); - return buildSeverityDescription(label, val); + const val: AboutStepSeverity = get(field, data); + return buildSeverityDescription(label, val.value); } else if (field === 'timeline') { const timeline = get(field, data) as FieldValueTimeline; return [ diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/risk_score_mapping/index.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/risk_score_mapping/index.tsx new file mode 100644 index 0000000000000..bdf1ac600faef --- /dev/null +++ b/x-pack/plugins/security_solution/public/alerts/components/rules/risk_score_mapping/index.tsx @@ -0,0 +1,190 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiFormRow, + EuiFieldText, + EuiCheckbox, + EuiText, + EuiFlexGroup, + EuiFlexItem, + EuiFormLabel, + EuiIcon, + EuiSpacer, +} from '@elastic/eui'; +import React, { useCallback, useMemo, useState } from 'react'; +import styled from 'styled-components'; +import * as i18n from './translations'; +import { FieldHook } from '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; +import { CommonUseField } from '../../../../cases/components/create'; +import { AboutStepRiskScore } from '../../../pages/detection_engine/rules/types'; + +const NestedContent = styled.div` + margin-left: 24px; +`; + +const EuiFlexItemIconColumn = styled(EuiFlexItem)` + width: 20px; +`; + +const EuiFlexItemRiskScoreColumn = styled(EuiFlexItem)` + width: 160px; +`; + +interface RiskScoreFieldProps { + dataTestSubj: string; + field: FieldHook; + idAria: string; + indices: string[]; +} + +export const RiskScoreField = ({ dataTestSubj, field, idAria, indices }: RiskScoreFieldProps) => { + const [isRiskScoreMappingSelected, setIsRiskScoreMappingSelected] = useState(false); + + const updateRiskScoreMapping = useCallback( + (event) => { + const values = field.value as AboutStepRiskScore; + field.setValue({ + value: values.value, + mapping: [ + { + field: event.target.value, + operator: 'equals', + value: '', + }, + ], + }); + }, + [field] + ); + + const severityLabel = useMemo(() => { + return ( +
+ + {i18n.RISK_SCORE} + + + {i18n.RISK_SCORE_DESCRIPTION} +
+ ); + }, []); + + const severityMappingLabel = useMemo(() => { + return ( +
+ setIsRiskScoreMappingSelected(!isRiskScoreMappingSelected)} + > + + setIsRiskScoreMappingSelected(e.target.checked)} + /> + + {i18n.RISK_SCORE_MAPPING} + + + + {i18n.RISK_SCORE_MAPPING_DESCRIPTION} + +
+ ); + }, [isRiskScoreMappingSelected, setIsRiskScoreMappingSelected]); + + return ( + + + + + + + + {i18n.RISK_SCORE_MAPPING_DETAILS} + ) : ( + '' + ) + } + error={'errorMessage'} + isInvalid={false} + fullWidth + data-test-subj={dataTestSubj} + describedByIds={idAria ? [idAria] : undefined} + > + + + {isRiskScoreMappingSelected && ( + + + + + {i18n.SOURCE_FIELD} + + + + {i18n.RISK_SCORE} + + + + + + + + + + + + + + {i18n.RISK_SCORE_FIELD} + + + + + )} + + + + + ); +}; diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/risk_score_mapping/translations.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/risk_score_mapping/translations.tsx new file mode 100644 index 0000000000000..a75bf19b5b3c4 --- /dev/null +++ b/x-pack/plugins/security_solution/public/alerts/components/rules/risk_score_mapping/translations.tsx @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const RISK_SCORE = i18n.translate( + 'xpack.securitySolution.alerts.riskScoreMapping.riskScoreTitle', + { + defaultMessage: 'Default risk score', + } +); + +export const RISK_SCORE_FIELD = i18n.translate( + 'xpack.securitySolution.alerts.riskScoreMapping.riskScoreFieldTitle', + { + defaultMessage: 'signal.rule.risk_score', + } +); + +export const SOURCE_FIELD = i18n.translate( + 'xpack.securitySolution.alerts.riskScoreMapping.sourceFieldTitle', + { + defaultMessage: 'Source field', + } +); + +export const RISK_SCORE_MAPPING = i18n.translate( + 'xpack.securitySolution.alerts.riskScoreMapping.riskScoreMappingTitle', + { + defaultMessage: 'Risk score override', + } +); + +export const RISK_SCORE_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.alerts.riskScoreMapping.defaultDescriptionLabel', + { + defaultMessage: 'Select a risk score for all alerts generated by this rule.', + } +); + +export const RISK_SCORE_MAPPING_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.alerts.riskScoreMapping.mappingDescriptionLabel', + { + defaultMessage: 'Map a field from the source event (scaled 1-100) to risk score.', + } +); + +export const RISK_SCORE_MAPPING_DETAILS = i18n.translate( + 'xpack.securitySolution.alerts.riskScoreMapping.mappingDetailsLabel', + { + defaultMessage: + 'If value is out of bounds, or field is not present, the default risk score will be used.', + } +); diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/severity_mapping/index.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/severity_mapping/index.tsx new file mode 100644 index 0000000000000..47c45a6bdf88d --- /dev/null +++ b/x-pack/plugins/security_solution/public/alerts/components/rules/severity_mapping/index.tsx @@ -0,0 +1,214 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiFormRow, + EuiFieldText, + EuiCheckbox, + EuiText, + EuiFlexGroup, + EuiFlexItem, + EuiFormLabel, + EuiIcon, + EuiSpacer, +} from '@elastic/eui'; +import React, { useCallback, useMemo, useState } from 'react'; +import styled from 'styled-components'; +import * as i18n from './translations'; +import { FieldHook } from '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; +import { SeverityOptionItem } from '../step_about_rule/data'; +import { CommonUseField } from '../../../../cases/components/create'; +import { AboutStepSeverity } from '../../../pages/detection_engine/rules/types'; + +const NestedContent = styled.div` + margin-left: 24px; +`; + +const EuiFlexItemIconColumn = styled(EuiFlexItem)` + width: 20px; +`; + +const EuiFlexItemSeverityColumn = styled(EuiFlexItem)` + width: 80px; +`; + +interface SeverityFieldProps { + dataTestSubj: string; + field: FieldHook; + idAria: string; + indices: string[]; + options: SeverityOptionItem[]; +} + +export const SeverityField = ({ + dataTestSubj, + field, + idAria, + indices, // TODO: To be used with autocomplete fields once https://github.com/elastic/kibana/pull/67013 is merged + options, +}: SeverityFieldProps) => { + const [isSeverityMappingChecked, setIsSeverityMappingChecked] = useState(false); + + const updateSeverityMapping = useCallback( + (index: number, severity: string, mappingField: string, event) => { + const values = field.value as AboutStepSeverity; + field.setValue({ + value: values.value, + mapping: [ + ...values.mapping.slice(0, index), + { + ...values.mapping[index], + [mappingField]: event.target.value, + operator: 'equals', + severity, + }, + ...values.mapping.slice(index + 1), + ], + }); + }, + [field] + ); + + const severityLabel = useMemo(() => { + return ( +
+ + {i18n.SEVERITY} + + + {i18n.SEVERITY_DESCRIPTION} +
+ ); + }, []); + + const severityMappingLabel = useMemo(() => { + return ( +
+ setIsSeverityMappingChecked(!isSeverityMappingChecked)} + > + + setIsSeverityMappingChecked(e.target.checked)} + /> + + {i18n.SEVERITY_MAPPING} + + + + {i18n.SEVERITY_MAPPING_DESCRIPTION} + +
+ ); + }, [isSeverityMappingChecked, setIsSeverityMappingChecked]); + + return ( + + + + + + + + + {i18n.SEVERITY_MAPPING_DETAILS} + ) : ( + '' + ) + } + error={'errorMessage'} + isInvalid={false} + fullWidth + data-test-subj={dataTestSubj} + describedByIds={idAria ? [idAria] : undefined} + > + + + {isSeverityMappingChecked && ( + + + + + {i18n.SOURCE_FIELD} + + + {i18n.SOURCE_VALUE} + + + + {i18n.SEVERITY} + + + + + {options.map((option, index) => ( + + + + + + + + + + + + + + {option.inputDisplay} + + + + ))} + + )} + + + + + ); +}; diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/severity_mapping/translations.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/severity_mapping/translations.tsx new file mode 100644 index 0000000000000..9c9784bac6b63 --- /dev/null +++ b/x-pack/plugins/security_solution/public/alerts/components/rules/severity_mapping/translations.tsx @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const SEVERITY = i18n.translate( + 'xpack.securitySolution.alerts.severityMapping.severityTitle', + { + defaultMessage: 'Default severity', + } +); + +export const SOURCE_FIELD = i18n.translate( + 'xpack.securitySolution.alerts.severityMapping.sourceFieldTitle', + { + defaultMessage: 'Source field', + } +); + +export const SOURCE_VALUE = i18n.translate( + 'xpack.securitySolution.alerts.severityMapping.sourceValueTitle', + { + defaultMessage: 'Source value', + } +); + +export const SEVERITY_MAPPING = i18n.translate( + 'xpack.securitySolution.alerts.severityMapping.severityMappingTitle', + { + defaultMessage: 'Severity override', + } +); + +export const SEVERITY_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.alerts.severityMapping.defaultDescriptionLabel', + { + defaultMessage: 'Select a severity level for all alerts generated by this rule.', + } +); + +export const SEVERITY_MAPPING_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.alerts.severityMapping.mappingDescriptionLabel', + { + defaultMessage: 'Map a value from the source event to a specific severity.', + } +); + +export const SEVERITY_MAPPING_DETAILS = i18n.translate( + 'xpack.securitySolution.alerts.severityMapping.mappingDetailsLabel', + { + defaultMessage: + 'For multiple matches the highest severity match will apply. If no match is found, the default severity will be used.', + } +); diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/data.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/data.tsx index 269d2d4509508..1ef3edf8c720e 100644 --- a/x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/data.tsx +++ b/x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/data.tsx @@ -12,7 +12,7 @@ import * as I18n from './translations'; export type SeverityValue = 'low' | 'medium' | 'high' | 'critical'; -interface SeverityOptionItem { +export interface SeverityOptionItem { value: SeverityValue; inputDisplay: React.ReactElement; } diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/default_value.ts b/x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/default_value.ts index 977769158481e..060a2183eb06e 100644 --- a/x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/default_value.ts +++ b/x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/default_value.ts @@ -15,14 +15,19 @@ export const threatDefault = [ ]; export const stepAboutDefaultValue: AboutStepRule = { + author: [], name: '', description: '', + isBuildingBlock: false, isNew: true, - severity: 'low', - riskScore: 50, + severity: { value: 'low', mapping: [] }, + riskScore: { value: 50, mapping: [] }, references: [''], falsePositives: [''], + license: '', + ruleNameOverride: '', tags: [], + timestampOverride: '', threat: threatDefault, note: '', }; diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/index.test.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/index.test.tsx index 5a08b0a20d1fc..b21c54a0b6131 100644 --- a/x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/index.test.tsx +++ b/x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/index.test.tsx @@ -164,13 +164,18 @@ describe('StepAboutRuleComponent', () => { wrapper.find('button[data-test-subj="about-continue"]').first().simulate('click').update(); await wait(); const expected: Omit = { + author: [], + isBuildingBlock: false, + license: '', + ruleNameOverride: '', + timestampOverride: '', description: 'Test description text', falsePositives: [''], name: 'Test name text', note: '', references: [''], - riskScore: 50, - severity: 'low', + riskScore: { value: 50, mapping: [] }, + severity: { value: 'low', mapping: [] }, tags: [], threat: [ { @@ -217,13 +222,18 @@ describe('StepAboutRuleComponent', () => { wrapper.find('[data-test-subj="about-continue"]').first().simulate('click').update(); await wait(); const expected: Omit = { + author: [], + isBuildingBlock: false, + license: '', + ruleNameOverride: '', + timestampOverride: '', description: 'Test description text', falsePositives: [''], name: 'Test name text', note: '', references: [''], - riskScore: 80, - severity: 'low', + riskScore: { value: 80, mapping: [] }, + severity: { value: 'low', mapping: [] }, tags: [], threat: [ { diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/index.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/index.tsx index f23c51e019f24..7f7ee94ed85b7 100644 --- a/x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiAccordion, EuiFlexItem, EuiSpacer, EuiButtonEmpty } from '@elastic/eui'; +import { EuiAccordion, EuiFlexItem, EuiSpacer, EuiButtonEmpty, EuiFormRow } from '@elastic/eui'; import React, { FC, memo, useCallback, useEffect, useState } from 'react'; import styled from 'styled-components'; import deepEqual from 'fast-deep-equal'; @@ -13,6 +13,7 @@ import { RuleStepProps, RuleStep, AboutStepRule, + DefineStepRule, } from '../../../pages/detection_engine/rules/types'; import { AddItem } from '../add_item_form'; import { StepRuleDescription } from '../description_step'; @@ -35,11 +36,14 @@ import { StepContentWrapper } from '../step_content_wrapper'; import { NextStep } from '../next_step'; import { MarkdownEditorForm } from '../../../../common/components/markdown_editor/form'; import { setFieldValue } from '../../../pages/detection_engine/rules/helpers'; +import { SeverityField } from '../severity_mapping'; +import { RiskScoreField } from '../risk_score_mapping'; const CommonUseField = getUseField({ component: Field }); interface StepAboutRuleProps extends RuleStepProps { defaultValues?: AboutStepRule | null; + defineRuleData?: DefineStepRule | null; } const ThreeQuartersContainer = styled.div` @@ -77,6 +81,7 @@ const AdvancedSettingsAccordionButton = ( const StepAboutRuleComponent: FC = ({ addPadding = false, defaultValues, + defineRuleData, descriptionColumns = 'singleSplit', isReadOnlyView, isUpdateView = false, @@ -132,64 +137,54 @@ const StepAboutRuleComponent: FC = ({ <>
- - - - - - - - + + + + + - - + @@ -207,13 +202,13 @@ const StepAboutRuleComponent: FC = ({ }} /> - + - + = ({ dataTestSubj: 'detectionEngineStepAboutRuleMitreThreat', }} /> - - - + + + + + + + + + + + + + + + + - + {({ severity }) => { diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/schema.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/schema.tsx index 59ecebaeb9e4e..309557e5c9421 100644 --- a/x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/schema.tsx +++ b/x-pack/plugins/security_solution/public/alerts/components/rules/step_about_rule/schema.tsx @@ -22,6 +22,23 @@ import * as I18n from './translations'; const { emptyField } = fieldValidators; export const schema: FormSchema = { + author: { + type: FIELD_TYPES.COMBO_BOX, + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldAuthorLabel', + { + defaultMessage: 'Author', + } + ), + helpText: i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldAuthorHelpText', + { + defaultMessage: + 'Type one or more author for this rule. Press enter after each author to add a new one.', + } + ), + labelAppend: OptionalFieldLabel, + }, name: { type: FIELD_TYPES.TEXT, label: i18n.translate( @@ -64,36 +81,44 @@ export const schema: FormSchema = { }, ], }, - severity: { - type: FIELD_TYPES.SUPER_SELECT, + isBuildingBlock: { + type: FIELD_TYPES.CHECKBOX, label: i18n.translate( - 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldSeverityLabel', + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldBuildingBlockLabel', { - defaultMessage: 'Severity', + defaultMessage: 'Mark all generated alerts as "building block" alerts', } ), - validations: [ - { - validator: emptyField( - i18n.translate( - 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.severityFieldRequiredError', - { - defaultMessage: 'A severity is required.', - } - ) - ), - }, - ], + labelAppend: OptionalFieldLabel, + }, + severity: { + value: { + type: FIELD_TYPES.SUPER_SELECT, + validations: [ + { + validator: emptyField( + i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.severityFieldRequiredError', + { + defaultMessage: 'A severity is required.', + } + ) + ), + }, + ], + }, + mapping: { + type: FIELD_TYPES.TEXT, + }, }, riskScore: { - type: FIELD_TYPES.RANGE, - serializer: (input: string) => Number(input), - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldRiskScoreLabel', - { - defaultMessage: 'Risk score', - } - ), + value: { + type: FIELD_TYPES.RANGE, + serializer: (input: string) => Number(input), + }, + mapping: { + type: FIELD_TYPES.TEXT, + }, }, references: { label: i18n.translate( @@ -135,6 +160,39 @@ export const schema: FormSchema = { ), labelAppend: OptionalFieldLabel, }, + license: { + type: FIELD_TYPES.TEXT, + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldLicenseLabel', + { + defaultMessage: 'License', + } + ), + helpText: i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldLicenseHelpText', + { + defaultMessage: 'Add a license name', + } + ), + labelAppend: OptionalFieldLabel, + }, + ruleNameOverride: { + type: FIELD_TYPES.TEXT, + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldRuleNameOverrideLabel', + { + defaultMessage: 'Rule name override', + } + ), + helpText: i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldRuleNameOverrideHelpText', + { + defaultMessage: + 'Choose a field from the source event to populate the rule name in the alert list.', + } + ), + labelAppend: OptionalFieldLabel, + }, threat: { label: i18n.translate( 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldMitreThreatLabel', @@ -166,6 +224,23 @@ export const schema: FormSchema = { }, ], }, + timestampOverride: { + type: FIELD_TYPES.TEXT, + label: i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldTimestampOverrideLabel', + { + defaultMessage: 'Timestamp override', + } + ), + helpText: i18n.translate( + 'xpack.securitySolution.detectionEngine.createRule.stepAboutRule.fieldTimestampOverrideHelpText', + { + defaultMessage: + 'Choose timestamp field used when executing rule. Pick field with timestamp closest to ingest time (e.g. event.ingested).', + } + ), + labelAppend: OptionalFieldLabel, + }, tags: { type: FIELD_TYPES.COMBO_BOX, label: i18n.translate( diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/api.test.ts b/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/api.test.ts index abba7c02cf875..46829b9cb8f7b 100644 --- a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/api.test.ts +++ b/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/api.test.ts @@ -291,7 +291,7 @@ describe('Detections Rules API', () => { await duplicateRules({ rules: rulesMock.data }); expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_bulk_create', { body: - '[{"actions":[],"description":"Elastic Endpoint detected Credential Dumping. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.","enabled":false,"false_positives":[],"from":"now-660s","index":["endgame-*"],"interval":"10m","language":"kuery","output_index":".siem-signals-default","max_signals":100,"risk_score":73,"name":"Credential Dumping - Detected - Elastic Endpoint [Duplicate]","query":"event.kind:alert and event.module:endgame and event.action:cred_theft_event and endgame.metadata.type:detection","filters":[],"references":[],"severity":"high","tags":["Elastic","Endpoint"],"to":"now","type":"query","threat":[],"throttle":null,"version":1},{"actions":[],"description":"Elastic Endpoint detected an Adversary Behavior. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.","enabled":false,"false_positives":[],"from":"now-660s","index":["endgame-*"],"interval":"10m","language":"kuery","output_index":".siem-signals-default","max_signals":100,"risk_score":47,"name":"Adversary Behavior - Detected - Elastic Endpoint [Duplicate]","query":"event.kind:alert and event.module:endgame and event.action:rules_engine_event","filters":[],"references":[],"severity":"medium","tags":["Elastic","Endpoint"],"to":"now","type":"query","threat":[],"throttle":null,"version":1}]', + '[{"actions":[],"author":[],"description":"Elastic Endpoint detected Credential Dumping. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.","enabled":false,"false_positives":[],"from":"now-660s","index":["endgame-*"],"interval":"10m","language":"kuery","output_index":".siem-signals-default","max_signals":100,"risk_score":73,"risk_score_mapping":[],"name":"Credential Dumping - Detected - Elastic Endpoint [Duplicate]","query":"event.kind:alert and event.module:endgame and event.action:cred_theft_event and endgame.metadata.type:detection","filters":[],"references":[],"severity":"high","severity_mapping":[],"tags":["Elastic","Endpoint"],"to":"now","type":"query","threat":[],"throttle":null,"version":1},{"actions":[],"author":[],"description":"Elastic Endpoint detected an Adversary Behavior. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.","enabled":false,"false_positives":[],"from":"now-660s","index":["endgame-*"],"interval":"10m","language":"kuery","output_index":".siem-signals-default","max_signals":100,"risk_score":47,"risk_score_mapping":[],"name":"Adversary Behavior - Detected - Elastic Endpoint [Duplicate]","query":"event.kind:alert and event.module:endgame and event.action:rules_engine_event","filters":[],"references":[],"severity":"medium","severity_mapping":[],"tags":["Elastic","Endpoint"],"to":"now","type":"query","threat":[],"throttle":null,"version":1}]', method: 'POST', }); }); diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/mock.ts b/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/mock.ts index 59782e8a36338..fa11cfabcdf8b 100644 --- a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/mock.ts +++ b/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/mock.ts @@ -36,6 +36,7 @@ export const ruleMock: NewRule = { }; export const savedRuleMock: Rule = { + author: [], actions: [], created_at: 'mm/dd/yyyyTHH:MM:sssz', created_by: 'mockUser', @@ -58,11 +59,13 @@ export const savedRuleMock: Rule = { rule_id: 'bbd3106e-b4b5-4d7c-a1a2-47531d6a2baf', language: 'kuery', risk_score: 75, + risk_score_mapping: [], name: 'Test rule', max_signals: 100, query: "user.email: 'root@elastic.co'", references: [], severity: 'high', + severity_mapping: [], tags: ['APM'], to: 'now', type: 'query', @@ -79,6 +82,7 @@ export const rulesMock: FetchRulesResponse = { data: [ { actions: [], + author: [], created_at: '2020-02-14T19:49:28.178Z', updated_at: '2020-02-14T19:49:28.320Z', created_by: 'elastic', @@ -96,12 +100,14 @@ export const rulesMock: FetchRulesResponse = { output_index: '.siem-signals-default', max_signals: 100, risk_score: 73, + risk_score_mapping: [], name: 'Credential Dumping - Detected - Elastic Endpoint', query: 'event.kind:alert and event.module:endgame and event.action:cred_theft_event and endgame.metadata.type:detection', filters: [], references: [], severity: 'high', + severity_mapping: [], updated_by: 'elastic', tags: ['Elastic', 'Endpoint'], to: 'now', @@ -112,6 +118,7 @@ export const rulesMock: FetchRulesResponse = { }, { actions: [], + author: [], created_at: '2020-02-14T19:49:28.189Z', updated_at: '2020-02-14T19:49:28.326Z', created_by: 'elastic', @@ -129,11 +136,13 @@ export const rulesMock: FetchRulesResponse = { output_index: '.siem-signals-default', max_signals: 100, risk_score: 47, + risk_score_mapping: [], name: 'Adversary Behavior - Detected - Elastic Endpoint', query: 'event.kind:alert and event.module:endgame and event.action:rules_engine_event', filters: [], references: [], severity: 'medium', + severity_mapping: [], updated_by: 'elastic', tags: ['Elastic', 'Endpoint'], to: 'now', diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/types.ts index ab9b88fb81fa7..d991cc35b8dfe 100644 --- a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/types.ts @@ -7,6 +7,17 @@ import * as t from 'io-ts'; import { RuleTypeSchema } from '../../../../../common/detection_engine/types'; +/* eslint-disable @typescript-eslint/camelcase */ +import { + author, + building_block_type, + license, + risk_score_mapping, + rule_name_override, + severity_mapping, + timestamp_override, +} from '../../../../../common/detection_engine/schemas/common/schemas'; +/* eslint-enable @typescript-eslint/camelcase */ /** * Params is an "record", since it is a type of AlertActionParams which is action templates. @@ -76,6 +87,7 @@ const MetaRule = t.intersection([ export const RuleSchema = t.intersection([ t.type({ + author, created_at: t.string, created_by: t.string, description: t.string, @@ -89,8 +101,10 @@ export const RuleSchema = t.intersection([ max_signals: t.number, references: t.array(t.string), risk_score: t.number, + risk_score_mapping, rule_id: t.string, severity: t.string, + severity_mapping, tags: t.array(t.string), type: RuleTypeSchema, to: t.string, @@ -101,21 +115,25 @@ export const RuleSchema = t.intersection([ throttle: t.union([t.string, t.null]), }), t.partial({ + building_block_type, anomaly_threshold: t.number, filters: t.array(t.unknown), index: t.array(t.string), language: t.string, + license, last_failure_at: t.string, last_failure_message: t.string, meta: MetaRule, machine_learning_job_id: t.string, output_index: t.string, query: t.string, + rule_name_override, saved_id: t.string, status: t.string, status_date: t.string, timeline_id: t.string, timeline_title: t.string, + timestamp_override, note: t.string, version: t.number, }), diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_rule.test.tsx b/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_rule.test.tsx index 9bfbade060303..e3cc6878eabca 100644 --- a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_rule.test.tsx +++ b/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_rule.test.tsx @@ -32,6 +32,7 @@ describe('useRule', () => { false, { actions: [], + author: [], created_at: 'mm/dd/yyyyTHH:MM:sssz', created_by: 'mockUser', description: 'some desc', @@ -56,8 +57,10 @@ describe('useRule', () => { query: "user.email: 'root@elastic.co'", references: [], risk_score: 75, + risk_score_mapping: [], rule_id: 'bbd3106e-b4b5-4d7c-a1a2-47531d6a2baf', severity: 'high', + severity_mapping: [], tags: ['APM'], threat: [], throttle: null, diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_rule_status.test.tsx b/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_rule_status.test.tsx index f203eca42cde6..1f2c0c32d590f 100644 --- a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_rule_status.test.tsx +++ b/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_rule_status.test.tsx @@ -27,6 +27,7 @@ const testRule: Rule = { }, }, ], + author: [], created_at: 'mm/dd/yyyyTHH:MM:sssz', created_by: 'mockUser', description: 'some desc', @@ -51,8 +52,10 @@ const testRule: Rule = { query: "user.email: 'root@elastic.co'", references: [], risk_score: 75, + risk_score_mapping: [], rule_id: 'bbd3106e-b4b5-4d7c-a1a2-47531d6a2baf', severity: 'high', + severity_mapping: [], tags: ['APM'], threat: [], throttle: null, diff --git a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_rules.test.tsx b/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_rules.test.tsx index ad34c39272bbf..76f2a5b58754e 100644 --- a/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_rules.test.tsx +++ b/x-pack/plugins/security_solution/public/alerts/containers/detection_engine/rules/use_rules.test.tsx @@ -59,6 +59,7 @@ describe('useRules', () => { data: [ { actions: [], + author: [], created_at: '2020-02-14T19:49:28.178Z', created_by: 'elastic', description: @@ -79,8 +80,10 @@ describe('useRules', () => { 'event.kind:alert and event.module:endgame and event.action:cred_theft_event and endgame.metadata.type:detection', references: [], risk_score: 73, + risk_score_mapping: [], rule_id: '571afc56-5ed9-465d-a2a9-045f099f6e7e', severity: 'high', + severity_mapping: [], tags: ['Elastic', 'Endpoint'], threat: [], throttle: null, @@ -92,6 +95,7 @@ describe('useRules', () => { }, { actions: [], + author: [], created_at: '2020-02-14T19:49:28.189Z', created_by: 'elastic', description: @@ -112,8 +116,10 @@ describe('useRules', () => { 'event.kind:alert and event.module:endgame and event.action:rules_engine_event', references: [], risk_score: 47, + risk_score_mapping: [], rule_id: '77a3c3df-8ec4-4da4-b758-878f551dee69', severity: 'medium', + severity_mapping: [], tags: ['Elastic', 'Endpoint'], threat: [], throttle: null, diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/__mocks__/mock.ts b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/__mocks__/mock.ts index 1b43a513d0d29..f1416bfbc41b5 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/__mocks__/mock.ts +++ b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/__mocks__/mock.ts @@ -41,6 +41,7 @@ export const mockQueryBar: FieldValueQueryBar = { export const mockRule = (id: string): Rule => ({ actions: [], + author: [], created_at: '2020-01-10T21:11:45.839Z', updated_at: '2020-01-10T21:11:45.839Z', created_by: 'elastic', @@ -58,6 +59,7 @@ export const mockRule = (id: string): Rule => ({ output_index: '.siem-signals-default', max_signals: 100, risk_score: 21, + risk_score_mapping: [], name: 'Home Grown!', query: '', references: [], @@ -66,6 +68,7 @@ export const mockRule = (id: string): Rule => ({ timeline_title: 'Untitled timeline', meta: { from: '0m' }, severity: 'low', + severity_mapping: [], updated_by: 'elastic', tags: [], to: 'now', @@ -78,6 +81,7 @@ export const mockRule = (id: string): Rule => ({ export const mockRuleWithEverything = (id: string): Rule => ({ actions: [], + author: [], created_at: '2020-01-10T21:11:45.839Z', updated_at: '2020-01-10T21:11:45.839Z', created_by: 'elastic', @@ -113,9 +117,12 @@ export const mockRuleWithEverything = (id: string): Rule => ({ interval: '5m', rule_id: 'b5ba41ab-aaf3-4f43-971b-bdf9434ce0ea', language: 'kuery', + license: 'Elastic License', output_index: '.siem-signals-default', max_signals: 100, risk_score: 21, + risk_score_mapping: [], + rule_name_override: 'message', name: 'Query with rule-id', query: 'user.name: root or user.name: admin', references: ['www.test.co'], @@ -124,6 +131,7 @@ export const mockRuleWithEverything = (id: string): Rule => ({ timeline_title: 'Titled timeline', meta: { from: '0m' }, severity: 'low', + severity_mapping: [], updated_by: 'elastic', tags: ['tag1', 'tag2'], to: 'now', @@ -146,16 +154,23 @@ export const mockRuleWithEverything = (id: string): Rule => ({ }, ], throttle: 'no_actions', + timestamp_override: 'event.ingested', note: '# this is some markdown documentation', version: 1, }); +// TODO: update types mapping export const mockAboutStepRule = (isNew = false): AboutStepRule => ({ isNew, + author: ['Elastic'], + isBuildingBlock: false, + timestampOverride: '', + ruleNameOverride: '', + license: 'Elastic License', name: 'Query with rule-id', description: '24/7', - severity: 'low', - riskScore: 21, + severity: { value: 'low', mapping: [] }, + riskScore: { value: 21, mapping: [] }, references: ['www.test.co'], falsePositives: ['test'], tags: ['tag1', 'tag2'], diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/create/helpers.test.ts b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/create/helpers.test.ts index d9cbcfc8979a1..bbfbbaae058d4 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/create/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/create/helpers.test.ts @@ -339,13 +339,18 @@ describe('helpers', () => { test('returns formatted object as AboutStepRuleJson', () => { const result: AboutStepRuleJson = formatAboutStepData(mockData); const expected = { + author: ['Elastic'], description: '24/7', false_positives: ['test'], + license: 'Elastic License', name: 'Query with rule-id', note: '# this is some markdown documentation', references: ['www.test.co'], risk_score: 21, + risk_score_mapping: [], + rule_name_override: '', severity: 'low', + severity_mapping: [], tags: ['tag1', 'tag2'], threat: [ { @@ -364,6 +369,7 @@ describe('helpers', () => { ], }, ], + timestamp_override: '', }; expect(result).toEqual(expected); @@ -377,13 +383,18 @@ describe('helpers', () => { }; const result: AboutStepRuleJson = formatAboutStepData(mockStepData); const expected = { + author: ['Elastic'], description: '24/7', false_positives: ['test'], + license: 'Elastic License', name: 'Query with rule-id', note: '# this is some markdown documentation', references: ['www.test.co'], risk_score: 21, + risk_score_mapping: [], + rule_name_override: '', severity: 'low', + severity_mapping: [], tags: ['tag1', 'tag2'], threat: [ { @@ -402,6 +413,7 @@ describe('helpers', () => { ], }, ], + timestamp_override: '', }; expect(result).toEqual(expected); @@ -414,12 +426,17 @@ describe('helpers', () => { }; const result: AboutStepRuleJson = formatAboutStepData(mockStepData); const expected = { + author: ['Elastic'], description: '24/7', false_positives: ['test'], + license: 'Elastic License', name: 'Query with rule-id', references: ['www.test.co'], risk_score: 21, + risk_score_mapping: [], + rule_name_override: '', severity: 'low', + severity_mapping: [], tags: ['tag1', 'tag2'], threat: [ { @@ -438,6 +455,7 @@ describe('helpers', () => { ], }, ], + timestamp_override: '', }; expect(result).toEqual(expected); @@ -481,13 +499,18 @@ describe('helpers', () => { }; const result: AboutStepRuleJson = formatAboutStepData(mockStepData); const expected = { + author: ['Elastic'], + license: 'Elastic License', description: '24/7', false_positives: ['test'], name: 'Query with rule-id', note: '# this is some markdown documentation', references: ['www.test.co'], risk_score: 21, + risk_score_mapping: [], + rule_name_override: '', severity: 'low', + severity_mapping: [], tags: ['tag1', 'tag2'], threat: [ { @@ -496,6 +519,7 @@ describe('helpers', () => { technique: [{ id: '456', name: 'technique1', reference: 'technique reference' }], }, ], + timestamp_override: '', }; expect(result).toEqual(expected); diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/create/helpers.ts b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/create/helpers.ts index d5ce57ce5b3a9..b7cf94bb4f319 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/create/helpers.ts +++ b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/create/helpers.ts @@ -122,11 +122,30 @@ export const formatScheduleStepData = (scheduleData: ScheduleStepRule): Schedule }; export const formatAboutStepData = (aboutStepData: AboutStepRule): AboutStepRuleJson => { - const { falsePositives, references, riskScore, threat, isNew, note, ...rest } = aboutStepData; - return { + const { + author, + falsePositives, + references, + riskScore, + severity, + threat, + isBuildingBlock, + isNew, + note, + ruleNameOverride, + timestampOverride, + ...rest + } = aboutStepData; + const resp = { + author: author.filter((item) => !isEmpty(item)), + ...(isBuildingBlock ? { building_block_type: 'default' } : {}), false_positives: falsePositives.filter((item) => !isEmpty(item)), references: references.filter((item) => !isEmpty(item)), - risk_score: riskScore, + risk_score: riskScore.value, + risk_score_mapping: riskScore.mapping, + rule_name_override: ruleNameOverride, + severity: severity.value, + severity_mapping: severity.mapping, threat: threat .filter((singleThreat) => singleThreat.tactic.name !== 'none') .map((singleThreat) => ({ @@ -137,9 +156,11 @@ export const formatAboutStepData = (aboutStepData: AboutStepRule): AboutStepRule return { id, name, reference }; }), })), + timestamp_override: timestampOverride, ...(!isEmpty(note) ? { note } : {}), ...rest, }; + return resp; }; export const formatActionsStepData = (actionsStepData: ActionsStepRule): ActionsStepRuleJson => { diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/create/index.tsx b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/create/index.tsx index de3e23b11aaf8..4be408039d6f6 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/create/index.tsx +++ b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/create/index.tsx @@ -358,6 +358,9 @@ const CreateRulePageComponent: React.FC = () => { { }, }; const aboutRuleStepData = { + author: [], description: '24/7', falsePositives: ['test'], + isBuildingBlock: false, isNew: false, + license: 'Elastic License', name: 'Query with rule-id', note: '# this is some markdown documentation', references: ['www.test.co'], - riskScore: 21, - severity: 'low', + riskScore: { value: 21, mapping: [] }, + ruleNameOverride: 'message', + severity: { value: 'low', mapping: [] }, tags: ['tag1', 'tag2'], threat: [ { @@ -106,6 +110,7 @@ describe('rule helpers', () => { ], }, ], + timestampOverride: 'event.ingested', }; const scheduleRuleStepData = { from: '0s', interval: '5m', isNew: false }; const ruleActionsStepData = { diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/helpers.tsx b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/helpers.tsx index 2cd211a35e9ba..2a792f7d35eaa 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/helpers.tsx +++ b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/helpers.tsx @@ -116,6 +116,13 @@ export const getHumanizedDuration = (from: string, interval: string): string => export const getAboutStepsData = (rule: Rule, detailsView: boolean): AboutStepRule => { const { name, description, note } = determineDetailsValue(rule, detailsView); const { + author, + building_block_type: buildingBlockType, + license, + risk_score_mapping: riskScoreMapping, + rule_name_override: ruleNameOverride, + severity_mapping: severityMapping, + timestamp_override: timestampOverride, references, severity, false_positives: falsePositives, @@ -126,13 +133,24 @@ export const getAboutStepsData = (rule: Rule, detailsView: boolean): AboutStepRu return { isNew: false, + author, + isBuildingBlock: buildingBlockType !== undefined, + license: license ?? '', + ruleNameOverride: ruleNameOverride ?? '', + timestampOverride: timestampOverride ?? '', name, description, note: note!, references, - severity, + severity: { + value: severity, + mapping: severityMapping, + }, tags, - riskScore, + riskScore: { + value: riskScore, + mapping: riskScoreMapping, + }, falsePositives, threat: threat as IMitreEnterpriseAttack[], }; diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/types.ts index 5f81409010a28..f453b5a95994d 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/types.ts @@ -10,6 +10,15 @@ import { Filter } from '../../../../../../../../src/plugins/data/common'; import { FormData, FormHook } from '../../../../shared_imports'; import { FieldValueQueryBar } from '../../../components/rules/query_bar'; import { FieldValueTimeline } from '../../../components/rules/pick_timeline'; +import { + Author, + BuildingBlockType, + License, + RiskScoreMapping, + RuleNameOverride, + SeverityMapping, + TimestampOverride, +} from '../../../../../common/detection_engine/schemas/common/schemas'; export interface EuiBasicTableSortTypes { field: string; @@ -52,13 +61,18 @@ interface StepRuleData { isNew: boolean; } export interface AboutStepRule extends StepRuleData { + author: string[]; name: string; description: string; - severity: string; - riskScore: number; + isBuildingBlock: boolean; + severity: AboutStepSeverity; + riskScore: AboutStepRiskScore; references: string[]; falsePositives: string[]; + license: string; + ruleNameOverride: string; tags: string[]; + timestampOverride: string; threat: IMitreEnterpriseAttack[]; note: string; } @@ -68,6 +82,16 @@ export interface AboutStepRuleDetails { description: string; } +export interface AboutStepSeverity { + value: string; + mapping: SeverityMapping; +} + +export interface AboutStepRiskScore { + value: number; + mapping: RiskScoreMapping; +} + export interface DefineStepRule extends StepRuleData { anomalyThreshold: number; index: string[]; @@ -104,14 +128,21 @@ export interface DefineStepRuleJson { } export interface AboutStepRuleJson { + author: Author; + building_block_type?: BuildingBlockType; name: string; description: string; + license: License; severity: string; + severity_mapping: SeverityMapping; risk_score: number; + risk_score_mapping: RiskScoreMapping; references: string[]; false_positives: string[]; + rule_name_override: RuleNameOverride; tags: string[]; threat: IMitreEnterpriseAttack[]; + timestamp_override: TimestampOverride; note?: string; } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts index 581946f2300b4..9ca102b437511 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -342,6 +342,8 @@ export const getResult = (): RuleAlertType => ({ alertTypeId: 'siem.signals', consumer: 'siem', params: { + author: ['Elastic'], + buildingBlockType: undefined, anomalyThreshold: undefined, description: 'Detecting root and admin users', ruleId: 'rule-1', @@ -352,6 +354,7 @@ export const getResult = (): RuleAlertType => ({ savedId: undefined, query: 'user.name: root or user.name: admin', language: 'kuery', + license: 'Elastic License', machineLearningJobId: undefined, outputIndex: '.siem-signals', timelineId: 'some-timeline-id', @@ -367,8 +370,11 @@ export const getResult = (): RuleAlertType => ({ }, ], riskScore: 50, + riskScoreMapping: [], + ruleNameOverride: undefined, maxSignals: 100, severity: 'high', + severityMapping: [], to: 'now', type: 'query', threat: [ @@ -388,6 +394,7 @@ export const getResult = (): RuleAlertType => ({ ], }, ], + timestampOverride: undefined, references: ['http://www.example.com', 'https://ww.example.com'], note: '# Investigative notes', version: 1, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts index 7b7d3fbdea0bf..87903d1035903 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts @@ -36,6 +36,7 @@ export const getOutputRuleAlertForRest = (): Omit< RulesSchema, 'machine_learning_job_id' | 'anomaly_threshold' > => ({ + author: ['Elastic'], actions: [], created_by: 'elastic', created_at: '2019-12-13T16:40:33.400Z', @@ -49,14 +50,17 @@ export const getOutputRuleAlertForRest = (): Omit< index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], interval: '5m', risk_score: 50, + risk_score_mapping: [], rule_id: 'rule-1', language: 'kuery', + license: 'Elastic License', max_signals: 100, name: 'Detect Root/Admin Users', output_index: '.siem-signals', query: 'user.name: root or user.name: admin', references: ['http://www.example.com', 'https://ww.example.com'], severity: 'high', + severity_mapping: [], updated_by: 'elastic', tags: [], throttle: 'no_actions', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signals_mapping.json b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signals_mapping.json index dc20f0793a6f8..aa4166e93f4a1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signals_mapping.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signals_mapping.json @@ -46,6 +46,12 @@ "rule_id": { "type": "keyword" }, + "author": { + "type": "keyword" + }, + "building_block_type": { + "type": "keyword" + }, "false_positives": { "type": "keyword" }, @@ -64,6 +70,19 @@ "risk_score": { "type": "keyword" }, + "risk_score_mapping": { + "properties": { + "field": { + "type": "keyword" + }, + "operator": { + "type": "keyword" + }, + "value": { + "type": "keyword" + } + } + }, "output_index": { "type": "keyword" }, @@ -85,9 +104,15 @@ "language": { "type": "keyword" }, + "license": { + "type": "keyword" + }, "name": { "type": "keyword" }, + "rule_name_override": { + "type": "keyword" + }, "query": { "type": "keyword" }, @@ -97,6 +122,22 @@ "severity": { "type": "keyword" }, + "severity_mapping": { + "properties": { + "field": { + "type": "keyword" + }, + "operator": { + "type": "keyword" + }, + "value": { + "type": "keyword" + }, + "severity": { + "type": "keyword" + } + } + }, "tags": { "type": "keyword" }, @@ -136,6 +177,9 @@ "note": { "type": "text" }, + "timestamp_override": { + "type": "keyword" + }, "type": { "type": "keyword" }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts index 4b65ee5efdff0..fc2cc6551450c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts @@ -21,9 +21,12 @@ jest.mock('../../rules/get_prepackaged_rules', () => { getPrepackagedRules: (): AddPrepackagedRulesSchemaDecoded[] => { return [ { + author: ['Elastic'], tags: [], rule_id: 'rule-1', risk_score: 50, + risk_score_mapping: [], + severity_mapping: [], description: 'some description', from: 'now-5m', to: 'now', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts index 92a7ea17e7eaf..2942413057e37 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts @@ -64,12 +64,15 @@ export const createRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => const { actions: actionsRest, anomaly_threshold: anomalyThreshold, + author, + building_block_type: buildingBlockType, description, enabled, false_positives: falsePositives, from, query: queryOrUndefined, language: languageOrUndefined, + license, machine_learning_job_id: machineLearningJobId, output_index: outputIndex, saved_id: savedId, @@ -80,11 +83,15 @@ export const createRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => interval, max_signals: maxSignals, risk_score: riskScore, + risk_score_mapping: riskScoreMapping, + rule_name_override: ruleNameOverride, name, severity, + severity_mapping: severityMapping, tags, threat, throttle, + timestamp_override: timestampOverride, to, type, references, @@ -139,6 +146,8 @@ export const createRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => const createdRule = await createRules({ alertsClient, anomalyThreshold, + author, + buildingBlockType, description, enabled, falsePositives, @@ -146,6 +155,7 @@ export const createRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => immutable: false, query, language, + license, machineLearningJobId, outputIndex: finalIndex, savedId, @@ -157,13 +167,17 @@ export const createRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => index, interval, maxSignals, - riskScore, name, + riskScore, + riskScoreMapping, + ruleNameOverride, severity, + severityMapping, tags, to, type, threat, + timestampOverride, references, note, version, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts index 78d67e0e9366c..310a9da56282d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts @@ -47,12 +47,15 @@ export const createRulesRoute = (router: IRouter, ml: SetupPlugins['ml']): void const { actions: actionsRest, anomaly_threshold: anomalyThreshold, + author, + building_block_type: buildingBlockType, description, enabled, false_positives: falsePositives, from, query: queryOrUndefined, language: languageOrUndefined, + license, output_index: outputIndex, saved_id: savedId, timeline_id: timelineId, @@ -65,11 +68,15 @@ export const createRulesRoute = (router: IRouter, ml: SetupPlugins['ml']): void interval, max_signals: maxSignals, risk_score: riskScore, + risk_score_mapping: riskScoreMapping, + rule_name_override: ruleNameOverride, name, severity, + severity_mapping: severityMapping, tags, threat, throttle, + timestamp_override: timestampOverride, to, type, references, @@ -121,6 +128,8 @@ export const createRulesRoute = (router: IRouter, ml: SetupPlugins['ml']): void const createdRule = await createRules({ alertsClient, anomalyThreshold, + author, + buildingBlockType, description, enabled, falsePositives, @@ -128,6 +137,7 @@ export const createRulesRoute = (router: IRouter, ml: SetupPlugins['ml']): void immutable: false, query, language, + license, outputIndex: finalIndex, savedId, timelineId, @@ -139,13 +149,17 @@ export const createRulesRoute = (router: IRouter, ml: SetupPlugins['ml']): void index, interval, maxSignals, - riskScore, name, + riskScore, + riskScoreMapping, + ruleNameOverride, severity, + severityMapping, tags, to, type, threat, + timestampOverride, references, note, version: 1, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts index a277f97ccf9f0..43aa1ecd31922 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts @@ -134,6 +134,8 @@ export const importRulesRoute = (router: IRouter, config: ConfigType, ml: SetupP } const { anomaly_threshold: anomalyThreshold, + author, + building_block_type: buildingBlockType, description, enabled, false_positives: falsePositives, @@ -141,6 +143,7 @@ export const importRulesRoute = (router: IRouter, config: ConfigType, ml: SetupP immutable, query: queryOrUndefined, language: languageOrUndefined, + license, machine_learning_job_id: machineLearningJobId, output_index: outputIndex, saved_id: savedId, @@ -151,10 +154,14 @@ export const importRulesRoute = (router: IRouter, config: ConfigType, ml: SetupP interval, max_signals: maxSignals, risk_score: riskScore, + risk_score_mapping: riskScoreMapping, + rule_name_override: ruleNameOverride, name, severity, + severity_mapping: severityMapping, tags, threat, + timestamp_override: timestampOverride, to, type, references, @@ -184,6 +191,8 @@ export const importRulesRoute = (router: IRouter, config: ConfigType, ml: SetupP await createRules({ alertsClient, anomalyThreshold, + author, + buildingBlockType, description, enabled, falsePositives, @@ -191,6 +200,7 @@ export const importRulesRoute = (router: IRouter, config: ConfigType, ml: SetupP immutable, query, language, + license, machineLearningJobId, outputIndex: signalsIndex, savedId, @@ -202,13 +212,17 @@ export const importRulesRoute = (router: IRouter, config: ConfigType, ml: SetupP index, interval, maxSignals, - riskScore, name, + riskScore, + riskScoreMapping, + ruleNameOverride, severity, + severityMapping, tags, to, type, threat, + timestampOverride, references, note, version, @@ -219,6 +233,8 @@ export const importRulesRoute = (router: IRouter, config: ConfigType, ml: SetupP } else if (rule != null && request.query.overwrite) { await patchRules({ alertsClient, + author, + buildingBlockType, savedObjectsClient, description, enabled, @@ -226,6 +242,7 @@ export const importRulesRoute = (router: IRouter, config: ConfigType, ml: SetupP from, query, language, + license, outputIndex, savedId, timelineId, @@ -237,9 +254,13 @@ export const importRulesRoute = (router: IRouter, config: ConfigType, ml: SetupP interval, maxSignals, riskScore, + riskScoreMapping, + ruleNameOverride, name, severity, + severityMapping, tags, + timestampOverride, to, type, threat, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts index b2a9fdd103a68..c3d6f920e47a9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts @@ -55,12 +55,15 @@ export const patchRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => request.body.map(async (payloadRule) => { const { actions: actionsRest, + author, + building_block_type: buildingBlockType, description, enabled, false_positives: falsePositives, from, query, language, + license, output_index: outputIndex, saved_id: savedId, timeline_id: timelineId, @@ -73,12 +76,16 @@ export const patchRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => interval, max_signals: maxSignals, risk_score: riskScore, + risk_score_mapping: riskScoreMapping, + rule_name_override: ruleNameOverride, name, severity, + severity_mapping: severityMapping, tags, to, type, threat, + timestamp_override: timestampOverride, throttle, references, note, @@ -107,12 +114,15 @@ export const patchRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => const rule = await patchRules({ rule: existingRule, alertsClient, + author, + buildingBlockType, description, enabled, falsePositives, from, query, language, + license, outputIndex, savedId, savedObjectsClient, @@ -124,12 +134,16 @@ export const patchRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => interval, maxSignals, riskScore, + riskScoreMapping, + ruleNameOverride, name, severity, + severityMapping, tags, to, type, threat, + timestampOverride, references, note, version, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.ts index 385eec0fe1180..eb9624e6412e9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.ts @@ -46,12 +46,15 @@ export const patchRulesRoute = (router: IRouter, ml: SetupPlugins['ml']) => { } const { actions: actionsRest, + author, + building_block_type: buildingBlockType, description, enabled, false_positives: falsePositives, from, query, language, + license, output_index: outputIndex, saved_id: savedId, timeline_id: timelineId, @@ -64,12 +67,16 @@ export const patchRulesRoute = (router: IRouter, ml: SetupPlugins['ml']) => { interval, max_signals: maxSignals, risk_score: riskScore, + risk_score_mapping: riskScoreMapping, + rule_name_override: ruleNameOverride, name, severity, + severity_mapping: severityMapping, tags, to, type, threat, + timestamp_override: timestampOverride, throttle, references, note, @@ -105,12 +112,15 @@ export const patchRulesRoute = (router: IRouter, ml: SetupPlugins['ml']) => { const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); const rule = await patchRules({ alertsClient, + author, + buildingBlockType, description, enabled, falsePositives, from, query, language, + license, outputIndex, savedId, savedObjectsClient, @@ -123,12 +133,16 @@ export const patchRulesRoute = (router: IRouter, ml: SetupPlugins['ml']) => { interval, maxSignals, riskScore, + riskScoreMapping, + ruleNameOverride, name, severity, + severityMapping, tags, to, type, threat, + timestampOverride, references, note, version, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts index 1e6815a357154..c1ab1be2dbd0a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts @@ -57,12 +57,15 @@ export const updateRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => const { actions: actionsRest, anomaly_threshold: anomalyThreshold, + author, + building_block_type: buildingBlockType, description, enabled, false_positives: falsePositives, from, query: queryOrUndefined, language: languageOrUndefined, + license, machine_learning_job_id: machineLearningJobId, output_index: outputIndex, saved_id: savedId, @@ -76,13 +79,17 @@ export const updateRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => interval, max_signals: maxSignals, risk_score: riskScore, + risk_score_mapping: riskScoreMapping, + rule_name_override: ruleNameOverride, name, severity, + severity_mapping: severityMapping, tags, to, type, threat, throttle, + timestamp_override: timestampOverride, references, note, version, @@ -117,12 +124,15 @@ export const updateRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => const rule = await updateRules({ alertsClient, anomalyThreshold, + author, + buildingBlockType, description, enabled, falsePositives, from, query, language, + license, machineLearningJobId, outputIndex: finalIndex, savedId, @@ -137,12 +147,16 @@ export const updateRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => interval, maxSignals, riskScore, + riskScoreMapping, + ruleNameOverride, name, severity, + severityMapping, tags, to, type, threat, + timestampOverride, references, note, version, 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 f2b47f195ca5c..717f388cfc1e9 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 @@ -47,12 +47,15 @@ export const updateRulesRoute = (router: IRouter, ml: SetupPlugins['ml']) => { const { actions: actionsRest, anomaly_threshold: anomalyThreshold, + author, + building_block_type: buildingBlockType, description, enabled, false_positives: falsePositives, from, query: queryOrUndefined, language: languageOrUndefined, + license, machine_learning_job_id: machineLearningJobId, output_index: outputIndex, saved_id: savedId, @@ -66,13 +69,17 @@ export const updateRulesRoute = (router: IRouter, ml: SetupPlugins['ml']) => { interval, max_signals: maxSignals, risk_score: riskScore, + risk_score_mapping: riskScoreMapping, + rule_name_override: ruleNameOverride, name, severity, + severity_mapping: severityMapping, tags, to, type, threat, throttle, + timestamp_override: timestampOverride, references, note, version, @@ -107,12 +114,15 @@ export const updateRulesRoute = (router: IRouter, ml: SetupPlugins['ml']) => { const rule = await updateRules({ alertsClient, anomalyThreshold, + author, + buildingBlockType, description, enabled, falsePositives, from, query, language, + license, machineLearningJobId, outputIndex: finalIndex, savedId, @@ -127,12 +137,16 @@ export const updateRulesRoute = (router: IRouter, ml: SetupPlugins['ml']) => { interval, maxSignals, riskScore, + riskScoreMapping, + ruleNameOverride, name, severity, + severityMapping, tags, to, type, threat, + timestampOverride, references, note, version, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts index 9320eba26df0b..9e93dc051a041 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts @@ -105,7 +105,9 @@ export const transformAlertToRule = ( ruleStatus?: SavedObject ): Partial => { return pickBy((value: unknown) => value != null, { + author: alert.params.author ?? [], actions: ruleActions?.actions ?? [], + building_block_type: alert.params.buildingBlockType, created_at: alert.createdAt.toISOString(), updated_at: alert.updatedAt.toISOString(), created_by: alert.createdBy ?? 'elastic', @@ -121,10 +123,13 @@ export const transformAlertToRule = ( interval: alert.schedule.interval, rule_id: alert.params.ruleId, language: alert.params.language, + license: alert.params.license, output_index: alert.params.outputIndex, max_signals: alert.params.maxSignals, machine_learning_job_id: alert.params.machineLearningJobId, risk_score: alert.params.riskScore, + risk_score_mapping: alert.params.riskScoreMapping ?? [], + rule_name_override: alert.params.ruleNameOverride, name: alert.name, query: alert.params.query, references: alert.params.references, @@ -133,12 +138,14 @@ export const transformAlertToRule = ( timeline_title: alert.params.timelineTitle, meta: alert.params.meta, severity: alert.params.severity, + severity_mapping: alert.params.severityMapping ?? [], updated_by: alert.updatedBy ?? 'elastic', tags: transformTags(alert.tags), to: alert.params.to, type: alert.params.type, threat: alert.params.threat ?? [], throttle: ruleActions?.ruleThrottle || 'no_actions', + timestamp_override: alert.params.timestampOverride, note: alert.params.note, version: alert.params.version, status: ruleStatus?.attributes.status ?? undefined, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.test.ts index 0065696712628..4dafafe3153ef 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.test.ts @@ -18,6 +18,7 @@ import { getListArrayMock } from '../../../../../common/detection_engine/schemas export const ruleOutput: RulesSchema = { actions: [], + author: ['Elastic'], created_at: '2019-12-13T16:40:33.400Z', updated_at: '2019-12-13T16:40:33.400Z', created_by: 'elastic', @@ -30,13 +31,16 @@ export const ruleOutput: RulesSchema = { interval: '5m', rule_id: 'rule-1', language: 'kuery', + license: 'Elastic License', output_index: '.siem-signals', max_signals: 100, risk_score: 50, + risk_score_mapping: [], name: 'Detect Root/Admin Users', query: 'user.name: root or user.name: admin', references: ['http://www.example.com', 'https://ww.example.com'], severity: 'high', + severity_mapping: [], updated_by: 'elastic', tags: [], to: 'now', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.mock.ts index d00bffb96ad05..a7e24a1ac1609 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.mock.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.mock.ts @@ -8,6 +8,8 @@ import { CreateRulesOptions } from './types'; import { alertsClientMock } from '../../../../../alerts/server/mocks'; export const getCreateRulesOptionsMock = (): CreateRulesOptions => ({ + author: ['Elastic'], + buildingBlockType: undefined, alertsClient: alertsClientMock.create(), anomalyThreshold: undefined, description: 'some description', @@ -16,6 +18,7 @@ export const getCreateRulesOptionsMock = (): CreateRulesOptions => ({ from: 'now-6m', query: 'user.name: root or user.name: admin', language: 'kuery', + license: 'Elastic License', savedId: 'savedId-123', timelineId: 'timelineid-123', timelineTitle: 'timeline-title-123', @@ -28,11 +31,15 @@ export const getCreateRulesOptionsMock = (): CreateRulesOptions => ({ interval: '5m', maxSignals: 100, riskScore: 80, + riskScoreMapping: [], + ruleNameOverride: undefined, outputIndex: 'output-1', name: 'Query with a rule id', severity: 'high', + severityMapping: [], tags: [], threat: [], + timestampOverride: undefined, to: 'now', type: 'query', references: ['http://www.example.com'], @@ -43,6 +50,8 @@ export const getCreateRulesOptionsMock = (): CreateRulesOptions => ({ }); export const getCreateMlRulesOptionsMock = (): CreateRulesOptions => ({ + author: ['Elastic'], + buildingBlockType: undefined, alertsClient: alertsClientMock.create(), anomalyThreshold: 55, description: 'some description', @@ -51,6 +60,7 @@ export const getCreateMlRulesOptionsMock = (): CreateRulesOptions => ({ from: 'now-6m', query: undefined, language: undefined, + license: 'Elastic License', savedId: 'savedId-123', timelineId: 'timelineid-123', timelineTitle: 'timeline-title-123', @@ -63,11 +73,15 @@ export const getCreateMlRulesOptionsMock = (): CreateRulesOptions => ({ interval: '5m', maxSignals: 100, riskScore: 80, + riskScoreMapping: [], + ruleNameOverride: undefined, outputIndex: 'output-1', name: 'Machine Learning Job', severity: 'high', + severityMapping: [], tags: [], threat: [], + timestampOverride: undefined, to: 'now', type: 'machine_learning', references: ['http://www.example.com'], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts index 83e9b0de16f06..b4e246718efd7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts @@ -14,12 +14,15 @@ import { hasListsFeature } from '../feature_flags'; export const createRules = async ({ alertsClient, anomalyThreshold, + author, + buildingBlockType, description, enabled, falsePositives, from, query, language, + license, savedId, timelineId, timelineTitle, @@ -32,11 +35,15 @@ export const createRules = async ({ interval, maxSignals, riskScore, + riskScoreMapping, + ruleNameOverride, outputIndex, name, severity, + severityMapping, tags, threat, + timestampOverride, to, type, references, @@ -55,6 +62,8 @@ export const createRules = async ({ consumer: APP_ID, params: { anomalyThreshold, + author, + buildingBlockType, description, ruleId, index, @@ -63,6 +72,7 @@ export const createRules = async ({ immutable, query, language, + license, outputIndex, savedId, timelineId, @@ -72,8 +82,12 @@ export const createRules = async ({ filters, maxSignals, riskScore, + riskScoreMapping, + ruleNameOverride, severity, + severityMapping, threat, + timestampOverride, to, type, references, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.test.ts index c4d7df61061bd..f2061ce1d36de 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.test.ts @@ -49,16 +49,19 @@ describe('create_rules_stream_from_ndjson', () => { ]); expect(result).toEqual([ { + author: [], actions: [], rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, + risk_score_mapping: [], description: 'some description', from: 'now-5m', to: 'now', index: ['index-1'], name: 'some-name', severity: 'low', + severity_mapping: [], interval: '5m', type: 'query', enabled: true, @@ -73,16 +76,19 @@ describe('create_rules_stream_from_ndjson', () => { version: 1, }, { + author: [], actions: [], rule_id: 'rule-2', output_index: '.siem-signals', risk_score: 50, + risk_score_mapping: [], description: 'some description', from: 'now-5m', to: 'now', index: ['index-1'], name: 'some-name', severity: 'low', + severity_mapping: [], interval: '5m', type: 'query', enabled: true, @@ -135,16 +141,19 @@ describe('create_rules_stream_from_ndjson', () => { ]); expect(result).toEqual([ { + author: [], actions: [], rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, + risk_score_mapping: [], description: 'some description', from: 'now-5m', to: 'now', index: ['index-1'], name: 'some-name', severity: 'low', + severity_mapping: [], interval: '5m', type: 'query', enabled: true, @@ -159,16 +168,19 @@ describe('create_rules_stream_from_ndjson', () => { version: 1, }, { + author: [], actions: [], rule_id: 'rule-2', output_index: '.siem-signals', risk_score: 50, + risk_score_mapping: [], description: 'some description', from: 'now-5m', to: 'now', index: ['index-1'], name: 'some-name', severity: 'low', + severity_mapping: [], interval: '5m', type: 'query', enabled: true, @@ -204,16 +216,19 @@ describe('create_rules_stream_from_ndjson', () => { ]); expect(result).toEqual([ { + author: [], actions: [], rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, + risk_score_mapping: [], description: 'some description', from: 'now-5m', to: 'now', index: ['index-1'], name: 'some-name', severity: 'low', + severity_mapping: [], interval: '5m', type: 'query', enabled: true, @@ -228,16 +243,19 @@ describe('create_rules_stream_from_ndjson', () => { version: 1, }, { + author: [], actions: [], rule_id: 'rule-2', output_index: '.siem-signals', risk_score: 50, + risk_score_mapping: [], description: 'some description', from: 'now-5m', to: 'now', index: ['index-1'], name: 'some-name', severity: 'low', + severity_mapping: [], interval: '5m', type: 'query', enabled: true, @@ -273,16 +291,19 @@ describe('create_rules_stream_from_ndjson', () => { ]); const resultOrError = result as Error[]; expect(resultOrError[0]).toEqual({ + author: [], actions: [], rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, + risk_score_mapping: [], description: 'some description', from: 'now-5m', to: 'now', index: ['index-1'], name: 'some-name', severity: 'low', + severity_mapping: [], interval: '5m', type: 'query', enabled: true, @@ -298,16 +319,19 @@ describe('create_rules_stream_from_ndjson', () => { }); expect(resultOrError[1].message).toEqual('Unexpected token , in JSON at position 1'); expect(resultOrError[2]).toEqual({ + author: [], actions: [], rule_id: 'rule-2', output_index: '.siem-signals', risk_score: 50, + risk_score_mapping: [], description: 'some description', from: 'now-5m', to: 'now', index: ['index-1'], name: 'some-name', severity: 'low', + severity_mapping: [], interval: '5m', type: 'query', enabled: true, @@ -342,16 +366,19 @@ describe('create_rules_stream_from_ndjson', () => { ]); const resultOrError = result as BadRequestError[]; expect(resultOrError[0]).toEqual({ + author: [], actions: [], rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, + risk_score_mapping: [], description: 'some description', from: 'now-5m', to: 'now', index: ['index-1'], name: 'some-name', severity: 'low', + severity_mapping: [], interval: '5m', type: 'query', enabled: true, @@ -369,16 +396,19 @@ describe('create_rules_stream_from_ndjson', () => { 'Invalid value "undefined" supplied to "description",Invalid value "undefined" supplied to "risk_score",Invalid value "undefined" supplied to "name",Invalid value "undefined" supplied to "severity",Invalid value "undefined" supplied to "type",Invalid value "undefined" supplied to "rule_id"' ); expect(resultOrError[2]).toEqual({ + author: [], actions: [], rule_id: 'rule-2', output_index: '.siem-signals', risk_score: 50, + risk_score_mapping: [], description: 'some description', from: 'now-5m', to: 'now', index: ['index-1'], name: 'some-name', severity: 'low', + severity_mapping: [], interval: '5m', type: 'query', enabled: true, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_all.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_all.test.ts index 7d4bbfdced432..c8ea000dd0dcd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_all.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_all.test.ts @@ -30,6 +30,7 @@ describe('getExportAll', () => { const exports = await getExportAll(alertsClient); expect(exports).toEqual({ rulesNdjson: `${JSON.stringify({ + author: ['Elastic'], actions: [], created_at: '2019-12-13T16:40:33.400Z', updated_at: '2019-12-13T16:40:33.400Z', @@ -45,9 +46,11 @@ describe('getExportAll', () => { interval: '5m', rule_id: 'rule-1', language: 'kuery', + license: 'Elastic License', output_index: '.siem-signals', max_signals: 100, risk_score: 50, + risk_score_mapping: [], name: 'Detect Root/Admin Users', query: 'user.name: root or user.name: admin', references: ['http://www.example.com', 'https://ww.example.com'], @@ -55,6 +58,7 @@ describe('getExportAll', () => { timeline_title: 'some-timeline-title', meta: { someMeta: 'someField' }, severity: 'high', + severity_mapping: [], updated_by: 'elastic', tags: [], to: 'now', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts index 043e563a4c8b5..d5dffff00b896 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts @@ -38,6 +38,7 @@ describe('get_export_by_object_ids', () => { const exports = await getExportByObjectIds(alertsClient, objects); expect(exports).toEqual({ rulesNdjson: `${JSON.stringify({ + author: ['Elastic'], actions: [], created_at: '2019-12-13T16:40:33.400Z', updated_at: '2019-12-13T16:40:33.400Z', @@ -53,9 +54,11 @@ describe('get_export_by_object_ids', () => { interval: '5m', rule_id: 'rule-1', language: 'kuery', + license: 'Elastic License', output_index: '.siem-signals', max_signals: 100, risk_score: 50, + risk_score_mapping: [], name: 'Detect Root/Admin Users', query: 'user.name: root or user.name: admin', references: ['http://www.example.com', 'https://ww.example.com'], @@ -63,6 +66,7 @@ describe('get_export_by_object_ids', () => { timeline_title: 'some-timeline-title', meta: { someMeta: 'someField' }, severity: 'high', + severity_mapping: [], updated_by: 'elastic', tags: [], to: 'now', @@ -139,6 +143,7 @@ describe('get_export_by_object_ids', () => { rules: [ { actions: [], + author: ['Elastic'], created_at: '2019-12-13T16:40:33.400Z', updated_at: '2019-12-13T16:40:33.400Z', created_by: 'elastic', @@ -153,9 +158,11 @@ describe('get_export_by_object_ids', () => { interval: '5m', rule_id: 'rule-1', language: 'kuery', + license: 'Elastic License', output_index: '.siem-signals', max_signals: 100, risk_score: 50, + risk_score_mapping: [], name: 'Detect Root/Admin Users', query: 'user.name: root or user.name: admin', references: ['http://www.example.com', 'https://ww.example.com'], @@ -163,6 +170,7 @@ describe('get_export_by_object_ids', () => { timeline_title: 'some-timeline-title', meta: { someMeta: 'someField' }, severity: 'high', + severity_mapping: [], updated_by: 'elastic', tags: [], to: 'now', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/install_prepacked_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/install_prepacked_rules.ts index a51acf99b570c..8a86a0f103371 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/install_prepacked_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/install_prepacked_rules.ts @@ -18,12 +18,15 @@ export const installPrepackagedRules = ( rules.reduce>>((acc, rule) => { const { anomaly_threshold: anomalyThreshold, + author, + building_block_type: buildingBlockType, description, enabled, false_positives: falsePositives, from, query, language, + license, machine_learning_job_id: machineLearningJobId, saved_id: savedId, timeline_id: timelineId, @@ -35,12 +38,16 @@ export const installPrepackagedRules = ( interval, max_signals: maxSignals, risk_score: riskScore, + risk_score_mapping: riskScoreMapping, + rule_name_override: ruleNameOverride, name, severity, + severity_mapping: severityMapping, tags, to, type, threat, + timestamp_override: timestampOverride, references, note, version, @@ -54,6 +61,8 @@ export const installPrepackagedRules = ( createRules({ alertsClient, anomalyThreshold, + author, + buildingBlockType, description, enabled, falsePositives, @@ -61,6 +70,7 @@ export const installPrepackagedRules = ( immutable: true, // At the moment we force all prepackaged rules to be immutable query, language, + license, machineLearningJobId, outputIndex, savedId, @@ -73,12 +83,16 @@ export const installPrepackagedRules = ( interval, maxSignals, riskScore, + riskScoreMapping, + ruleNameOverride, name, severity, + severityMapping, tags, to, type, threat, + timestampOverride, references, note, version, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.mock.ts index e711d8d2ac287..f3102a5ad2cf3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.mock.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.mock.ts @@ -113,6 +113,8 @@ const rule: SanitizedAlert = { }; export const getPatchRulesOptionsMock = (): PatchRulesOptions => ({ + author: ['Elastic'], + buildingBlockType: undefined, alertsClient: alertsClientMock.create(), savedObjectsClient: savedObjectsClientMock.create(), anomalyThreshold: undefined, @@ -122,6 +124,7 @@ export const getPatchRulesOptionsMock = (): PatchRulesOptions => ({ from: 'now-6m', query: 'user.name: root or user.name: admin', language: 'kuery', + license: 'Elastic License', savedId: 'savedId-123', timelineId: 'timelineid-123', timelineTitle: 'timeline-title-123', @@ -132,11 +135,15 @@ export const getPatchRulesOptionsMock = (): PatchRulesOptions => ({ interval: '5m', maxSignals: 100, riskScore: 80, + riskScoreMapping: [], + ruleNameOverride: undefined, outputIndex: 'output-1', name: 'Query with a rule id', severity: 'high', + severityMapping: [], tags: [], threat: [], + timestampOverride: undefined, to: 'now', type: 'query', references: ['http://www.example.com'], @@ -148,6 +155,8 @@ export const getPatchRulesOptionsMock = (): PatchRulesOptions => ({ }); export const getPatchMlRulesOptionsMock = (): PatchRulesOptions => ({ + author: ['Elastic'], + buildingBlockType: undefined, alertsClient: alertsClientMock.create(), savedObjectsClient: savedObjectsClientMock.create(), anomalyThreshold: 55, @@ -157,6 +166,7 @@ export const getPatchMlRulesOptionsMock = (): PatchRulesOptions => ({ from: 'now-6m', query: undefined, language: undefined, + license: 'Elastic License', savedId: 'savedId-123', timelineId: 'timelineid-123', timelineTitle: 'timeline-title-123', @@ -167,11 +177,15 @@ export const getPatchMlRulesOptionsMock = (): PatchRulesOptions => ({ interval: '5m', maxSignals: 100, riskScore: 80, + riskScoreMapping: [], + ruleNameOverride: undefined, outputIndex: 'output-1', name: 'Machine Learning Job', severity: 'high', + severityMapping: [], tags: [], threat: [], + timestampOverride: undefined, to: 'now', type: 'machine_learning', references: ['http://www.example.com'], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts index 0c103b7176db4..577d8d426b63d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts @@ -14,12 +14,15 @@ import { ruleStatusSavedObjectsClientFactory } from '../signals/rule_status_save export const patchRules = async ({ alertsClient, + author, + buildingBlockType, savedObjectsClient, description, falsePositives, enabled, query, language, + license, outputIndex, savedId, timelineId, @@ -31,11 +34,15 @@ export const patchRules = async ({ interval, maxSignals, riskScore, + riskScoreMapping, + ruleNameOverride, rule, name, severity, + severityMapping, tags, threat, + timestampOverride, to, type, references, @@ -51,10 +58,13 @@ export const patchRules = async ({ } const calculatedVersion = calculateVersion(rule.params.immutable, rule.params.version, { + author, + buildingBlockType, description, falsePositives, query, language, + license, outputIndex, savedId, timelineId, @@ -66,10 +76,14 @@ export const patchRules = async ({ interval, maxSignals, riskScore, + riskScoreMapping, + ruleNameOverride, name, severity, + severityMapping, tags, threat, + timestampOverride, to, type, references, @@ -85,11 +99,14 @@ export const patchRules = async ({ ...rule.params, }, { + author, + buildingBlockType, description, falsePositives, from, query, language, + license, outputIndex, savedId, timelineId, @@ -99,8 +116,12 @@ export const patchRules = async ({ index, maxSignals, riskScore, + riskScoreMapping, + ruleNameOverride, severity, + severityMapping, threat, + timestampOverride, to, type, references, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts index fc95f0cfeb78e..7b793ffbdb362 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts @@ -73,6 +73,16 @@ import { LastSuccessMessage, LastFailureAt, LastFailureMessage, + Author, + AuthorOrUndefined, + LicenseOrUndefined, + RiskScoreMapping, + RiskScoreMappingOrUndefined, + SeverityMapping, + SeverityMappingOrUndefined, + TimestampOverrideOrUndefined, + BuildingBlockTypeOrUndefined, + RuleNameOverrideOrUndefined, } from '../../../../common/detection_engine/schemas/common/schemas'; import { AlertsClient, PartialAlert } from '../../../../../alerts/server'; import { Alert, SanitizedAlert } from '../../../../../alerts/common'; @@ -165,6 +175,8 @@ export const isRuleStatusFindTypes = ( export interface CreateRulesOptions { alertsClient: AlertsClient; anomalyThreshold: AnomalyThresholdOrUndefined; + author: Author; + buildingBlockType: BuildingBlockTypeOrUndefined; description: Description; enabled: Enabled; falsePositives: FalsePositives; @@ -181,13 +193,18 @@ export interface CreateRulesOptions { immutable: Immutable; index: IndexOrUndefined; interval: Interval; + license: LicenseOrUndefined; maxSignals: MaxSignals; riskScore: RiskScore; + riskScoreMapping: RiskScoreMapping; + ruleNameOverride: RuleNameOverrideOrUndefined; outputIndex: OutputIndex; name: Name; severity: Severity; + severityMapping: SeverityMapping; tags: Tags; threat: Threat; + timestampOverride: TimestampOverrideOrUndefined; to: To; type: Type; references: References; @@ -202,6 +219,8 @@ export interface UpdateRulesOptions { savedObjectsClient: SavedObjectsClientContract; alertsClient: AlertsClient; anomalyThreshold: AnomalyThresholdOrUndefined; + author: Author; + buildingBlockType: BuildingBlockTypeOrUndefined; description: Description; enabled: Enabled; falsePositives: FalsePositives; @@ -217,13 +236,18 @@ export interface UpdateRulesOptions { ruleId: RuleIdOrUndefined; index: IndexOrUndefined; interval: Interval; + license: LicenseOrUndefined; maxSignals: MaxSignals; riskScore: RiskScore; + riskScoreMapping: RiskScoreMapping; + ruleNameOverride: RuleNameOverrideOrUndefined; outputIndex: OutputIndex; name: Name; severity: Severity; + severityMapping: SeverityMapping; tags: Tags; threat: Threat; + timestampOverride: TimestampOverrideOrUndefined; to: To; type: Type; references: References; @@ -237,6 +261,8 @@ export interface PatchRulesOptions { savedObjectsClient: SavedObjectsClientContract; alertsClient: AlertsClient; anomalyThreshold: AnomalyThresholdOrUndefined; + author: AuthorOrUndefined; + buildingBlockType: BuildingBlockTypeOrUndefined; description: DescriptionOrUndefined; enabled: EnabledOrUndefined; falsePositives: FalsePositivesOrUndefined; @@ -251,13 +277,18 @@ export interface PatchRulesOptions { filters: PartialFilter[]; index: IndexOrUndefined; interval: IntervalOrUndefined; + license: LicenseOrUndefined; maxSignals: MaxSignalsOrUndefined; riskScore: RiskScoreOrUndefined; + riskScoreMapping: RiskScoreMappingOrUndefined; + ruleNameOverride: RuleNameOverrideOrUndefined; outputIndex: OutputIndexOrUndefined; name: NameOrUndefined; severity: SeverityOrUndefined; + severityMapping: SeverityMappingOrUndefined; tags: TagsOrUndefined; threat: ThreatOrUndefined; + timestampOverride: TimestampOverrideOrUndefined; to: ToOrUndefined; type: TypeOrUndefined; references: ReferencesOrUndefined; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.ts index c4792eaa97ee1..6466cc596d891 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.ts @@ -20,11 +20,14 @@ export const updatePrepackagedRules = async ( await Promise.all( rules.map(async (rule) => { const { + author, + building_block_type: buildingBlockType, description, false_positives: falsePositives, from, query, language, + license, saved_id: savedId, meta, filters: filtersObject, @@ -33,12 +36,16 @@ export const updatePrepackagedRules = async ( interval, max_signals: maxSignals, risk_score: riskScore, + risk_score_mapping: riskScoreMapping, + rule_name_override: ruleNameOverride, name, severity, + severity_mapping: severityMapping, tags, to, type, threat, + timestamp_override: timestampOverride, references, version, note, @@ -58,11 +65,14 @@ export const updatePrepackagedRules = async ( // or enable rules on the user when they were not expecting it if a rule updates return patchRules({ alertsClient, + author, + buildingBlockType, description, falsePositives, from, query, language, + license, outputIndex, rule: existingRule, savedId, @@ -73,9 +83,13 @@ export const updatePrepackagedRules = async ( interval, maxSignals, riskScore, + riskScoreMapping, + ruleNameOverride, name, severity, + severityMapping, tags, + timestampOverride, to, type, threat, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.mock.ts index 7812c66a74d1f..fdc0a61274e75 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.mock.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.mock.ts @@ -9,6 +9,8 @@ import { alertsClientMock } from '../../../../../alerts/server/mocks'; import { savedObjectsClientMock } from '../../../../../../../src/core/server/mocks'; export const getUpdateRulesOptionsMock = (): UpdateRulesOptions => ({ + author: ['Elastic'], + buildingBlockType: undefined, id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', alertsClient: alertsClientMock.create(), savedObjectsClient: savedObjectsClientMock.create(), @@ -19,6 +21,7 @@ export const getUpdateRulesOptionsMock = (): UpdateRulesOptions => ({ from: 'now-6m', query: 'user.name: root or user.name: admin', language: 'kuery', + license: 'Elastic License', savedId: 'savedId-123', timelineId: 'timelineid-123', timelineTitle: 'timeline-title-123', @@ -30,11 +33,15 @@ export const getUpdateRulesOptionsMock = (): UpdateRulesOptions => ({ interval: '5m', maxSignals: 100, riskScore: 80, + riskScoreMapping: [], + ruleNameOverride: undefined, outputIndex: 'output-1', name: 'Query with a rule id', severity: 'high', + severityMapping: [], tags: [], threat: [], + timestampOverride: undefined, to: 'now', type: 'query', references: ['http://www.example.com'], @@ -45,6 +52,8 @@ export const getUpdateRulesOptionsMock = (): UpdateRulesOptions => ({ }); export const getUpdateMlRulesOptionsMock = (): UpdateRulesOptions => ({ + author: ['Elastic'], + buildingBlockType: undefined, id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', alertsClient: alertsClientMock.create(), savedObjectsClient: savedObjectsClientMock.create(), @@ -55,6 +64,7 @@ export const getUpdateMlRulesOptionsMock = (): UpdateRulesOptions => ({ from: 'now-6m', query: undefined, language: undefined, + license: 'Elastic License', savedId: 'savedId-123', timelineId: 'timelineid-123', timelineTitle: 'timeline-title-123', @@ -66,11 +76,15 @@ export const getUpdateMlRulesOptionsMock = (): UpdateRulesOptions => ({ interval: '5m', maxSignals: 100, riskScore: 80, + riskScoreMapping: [], + ruleNameOverride: undefined, outputIndex: 'output-1', name: 'Machine Learning Job', severity: 'high', + severityMapping: [], tags: [], threat: [], + timestampOverride: undefined, to: 'now', type: 'machine_learning', references: ['http://www.example.com'], 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 b3f327857dbb3..5cc68db25afc8 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 @@ -15,12 +15,15 @@ import { ruleStatusSavedObjectsClientFactory } from '../signals/rule_status_save export const updateRules = async ({ alertsClient, + author, + buildingBlockType, savedObjectsClient, description, falsePositives, enabled, query, language, + license, outputIndex, savedId, timelineId, @@ -34,10 +37,14 @@ export const updateRules = async ({ interval, maxSignals, riskScore, + riskScoreMapping, + ruleNameOverride, name, severity, + severityMapping, tags, threat, + timestampOverride, to, type, references, @@ -54,10 +61,13 @@ export const updateRules = async ({ } const calculatedVersion = calculateVersion(rule.params.immutable, rule.params.version, { + author, + buildingBlockType, description, falsePositives, query, language, + license, outputIndex, savedId, timelineId, @@ -69,10 +79,14 @@ export const updateRules = async ({ interval, maxSignals, riskScore, + riskScoreMapping, + ruleNameOverride, name, severity, + severityMapping, tags, threat, + timestampOverride, to, type, references, @@ -95,6 +109,8 @@ export const updateRules = async ({ actions: actions.map(transformRuleToAlertAction), throttle: null, params: { + author, + buildingBlockType, description, ruleId: rule.params.ruleId, falsePositives, @@ -102,6 +118,7 @@ export const updateRules = async ({ immutable: rule.params.immutable, query, language, + license, outputIndex, savedId, timelineId, @@ -111,8 +128,12 @@ export const updateRules = async ({ index, maxSignals, riskScore, + riskScoreMapping, + ruleNameOverride, severity, + severityMapping, threat, + timestampOverride, to, type, references, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.test.ts index 0f65b2a78ec4c..aa0512678b073 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.test.ts @@ -28,10 +28,13 @@ describe('utils', () => { test('returning the same version number if given an immutable but no updated version number', () => { expect( calculateVersion(true, 1, { + author: [], + buildingBlockType: undefined, description: 'some description change', falsePositives: undefined, query: undefined, language: undefined, + license: undefined, outputIndex: undefined, savedId: undefined, timelineId: undefined, @@ -43,11 +46,15 @@ describe('utils', () => { interval: undefined, maxSignals: undefined, riskScore: undefined, + riskScoreMapping: undefined, + ruleNameOverride: undefined, name: undefined, severity: undefined, + severityMapping: undefined, tags: undefined, threat: undefined, to: undefined, + timestampOverride: undefined, type: undefined, references: undefined, version: undefined, @@ -62,10 +69,13 @@ describe('utils', () => { test('returning an updated version number if given an immutable and an updated version number', () => { expect( calculateVersion(true, 2, { + author: [], + buildingBlockType: undefined, description: 'some description change', falsePositives: undefined, query: undefined, language: undefined, + license: undefined, outputIndex: undefined, savedId: undefined, timelineId: undefined, @@ -77,11 +87,15 @@ describe('utils', () => { interval: undefined, maxSignals: undefined, riskScore: undefined, + riskScoreMapping: undefined, + ruleNameOverride: undefined, name: undefined, severity: undefined, + severityMapping: undefined, tags: undefined, threat: undefined, to: undefined, + timestampOverride: undefined, type: undefined, references: undefined, version: undefined, @@ -96,10 +110,13 @@ describe('utils', () => { test('returning an updated version number if not given an immutable but but an updated description', () => { expect( calculateVersion(false, 1, { + author: [], + buildingBlockType: undefined, description: 'some description change', falsePositives: undefined, query: undefined, language: undefined, + license: undefined, outputIndex: undefined, savedId: undefined, timelineId: undefined, @@ -111,11 +128,15 @@ describe('utils', () => { interval: undefined, maxSignals: undefined, riskScore: undefined, + riskScoreMapping: undefined, + ruleNameOverride: undefined, name: undefined, severity: undefined, + severityMapping: undefined, tags: undefined, threat: undefined, to: undefined, + timestampOverride: undefined, type: undefined, references: undefined, version: undefined, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts index 5c620a5df61f8..861d02a8203e6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts @@ -31,6 +31,13 @@ import { ThreatOrUndefined, TypeOrUndefined, ReferencesOrUndefined, + AuthorOrUndefined, + BuildingBlockTypeOrUndefined, + LicenseOrUndefined, + RiskScoreMappingOrUndefined, + RuleNameOverrideOrUndefined, + SeverityMappingOrUndefined, + TimestampOverrideOrUndefined, } from '../../../../common/detection_engine/schemas/common/schemas'; import { PartialFilter } from '../types'; import { ListArrayOrUndefined } from '../../../../common/detection_engine/schemas/types'; @@ -49,11 +56,14 @@ export const calculateInterval = ( }; export interface UpdateProperties { + author: AuthorOrUndefined; + buildingBlockType: BuildingBlockTypeOrUndefined; description: DescriptionOrUndefined; falsePositives: FalsePositivesOrUndefined; from: FromOrUndefined; query: QueryOrUndefined; language: LanguageOrUndefined; + license: LicenseOrUndefined; savedId: SavedIdOrUndefined; timelineId: TimelineIdOrUndefined; timelineTitle: TimelineTitleOrUndefined; @@ -64,11 +74,15 @@ export interface UpdateProperties { interval: IntervalOrUndefined; maxSignals: MaxSignalsOrUndefined; riskScore: RiskScoreOrUndefined; + riskScoreMapping: RiskScoreMappingOrUndefined; + ruleNameOverride: RuleNameOverrideOrUndefined; outputIndex: OutputIndexOrUndefined; name: NameOrUndefined; severity: SeverityOrUndefined; + severityMapping: SeverityMappingOrUndefined; tags: TagsOrUndefined; threat: ThreatOrUndefined; + timestampOverride: TimestampOverrideOrUndefined; to: ToOrUndefined; type: TypeOrUndefined; references: ReferencesOrUndefined; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/post_rule.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/post_rule.sh index 432045634ba7b..ac551781d2042 100755 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/post_rule.sh +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/post_rule.sh @@ -24,7 +24,7 @@ do { -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ -X POST ${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules \ -d @${RULE} \ - | jq .; + | jq -S .; } & done diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/query_with_mappings.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/query_with_mappings.json new file mode 100644 index 0000000000000..f0d7cb4ec914b --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/queries/query_with_mappings.json @@ -0,0 +1,44 @@ +{ + "description": "Makes external events actionable within Elastic Security! 🎬", + "enabled": false, + "index": [ + "apm-*-transaction*", + "auditbeat-*", + "endgame-*", + "filebeat-*", + "packetbeat-*", + "winlogbeat-*" + ], + "language": "kuery", + "risk_score": 50, + "severity": "high", + "name": "External alerts", + "query": "event.type: \"alert\"", + "type": "query", + "author": ["Elastic"], + "building_block_type": "default", + "license": "Elastic License", + "risk_score_mapping": [ + { + "field": "event.risk_score", + "operator": "equals", + "value": "0" + } + ], + "rule_name_override": "event.message", + "severity_mapping":[ + { + "field": "event.severity", + "operator": "equals", + "value": "low", + "severity": "low" + }, + { + "field": "event.severity", + "operator": "equals", + "value": "medium", + "severity": "medium" + } + ], + "timestamp_override": "event.ingested" +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts index 50f6e7d9e9c10..7492422968062 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts @@ -20,6 +20,8 @@ export const sampleRuleAlertParams = ( maxSignals?: number | undefined, riskScore?: number | undefined ): RuleTypeParams => ({ + author: ['Elastic'], + buildingBlockType: 'default', ruleId: 'rule-1', description: 'Detecting root and admin users', falsePositives: [], @@ -29,11 +31,15 @@ export const sampleRuleAlertParams = ( from: 'now-6m', to: 'now', severity: 'high', + severityMapping: [], query: 'user.name: root or user.name: admin', language: 'kuery', + license: 'Elastic License', outputIndex: '.siem-signals', references: ['http://google.com'], riskScore: riskScore ? riskScore : 50, + riskScoreMapping: [], + ruleNameOverride: undefined, maxSignals: maxSignals ? maxSignals : 10000, note: '', anomalyThreshold: undefined, @@ -42,6 +48,7 @@ export const sampleRuleAlertParams = ( savedId: undefined, timelineId: undefined, timelineTitle: undefined, + timestampOverride: undefined, meta: undefined, threat: undefined, version: 1, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.test.ts index ad43932818836..e840ae96cf3c1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.test.ts @@ -64,11 +64,14 @@ describe('buildBulkBody', () => { status: 'open', rule: { actions: [], + author: ['Elastic'], + building_block_type: 'default', id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', rule_id: 'rule-1', false_positives: [], max_signals: 10000, risk_score: 50, + risk_score_mapping: [], output_index: '.siem-signals', description: 'Detecting root and admin users', from: 'now-6m', @@ -76,10 +79,12 @@ describe('buildBulkBody', () => { index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], interval: '5m', language: 'kuery', + license: 'Elastic License', name: 'rule-name', query: 'user.name: root or user.name: admin', references: ['http://google.com'], severity: 'high', + severity_mapping: [], tags: ['some fake tag 1', 'some fake tag 2'], threat: [], throttle: 'no_actions', @@ -160,11 +165,14 @@ describe('buildBulkBody', () => { status: 'open', rule: { actions: [], + author: ['Elastic'], + building_block_type: 'default', id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', rule_id: 'rule-1', false_positives: [], max_signals: 10000, risk_score: 50, + risk_score_mapping: [], output_index: '.siem-signals', description: 'Detecting root and admin users', from: 'now-6m', @@ -172,10 +180,12 @@ describe('buildBulkBody', () => { index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], interval: '5m', language: 'kuery', + license: 'Elastic License', name: 'rule-name', query: 'user.name: root or user.name: admin', references: ['http://google.com'], severity: 'high', + severity_mapping: [], tags: ['some fake tag 1', 'some fake tag 2'], type: 'query', to: 'now', @@ -254,11 +264,14 @@ describe('buildBulkBody', () => { status: 'open', rule: { actions: [], + author: ['Elastic'], + building_block_type: 'default', id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', rule_id: 'rule-1', false_positives: [], max_signals: 10000, risk_score: 50, + risk_score_mapping: [], output_index: '.siem-signals', description: 'Detecting root and admin users', from: 'now-6m', @@ -266,10 +279,12 @@ describe('buildBulkBody', () => { index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], interval: '5m', language: 'kuery', + license: 'Elastic License', name: 'rule-name', query: 'user.name: root or user.name: admin', references: ['http://google.com'], severity: 'high', + severity_mapping: [], threat: [], tags: ['some fake tag 1', 'some fake tag 2'], type: 'query', @@ -341,11 +356,14 @@ describe('buildBulkBody', () => { status: 'open', rule: { actions: [], + author: ['Elastic'], + building_block_type: 'default', id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', rule_id: 'rule-1', false_positives: [], max_signals: 10000, risk_score: 50, + risk_score_mapping: [], output_index: '.siem-signals', description: 'Detecting root and admin users', from: 'now-6m', @@ -353,10 +371,12 @@ describe('buildBulkBody', () => { index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], interval: '5m', language: 'kuery', + license: 'Elastic License', name: 'rule-name', query: 'user.name: root or user.name: admin', references: ['http://google.com'], severity: 'high', + severity_mapping: [], tags: ['some fake tag 1', 'some fake tag 2'], threat: [], type: 'query', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.test.ts index 9aef5a370b86a..ed632ee2576dc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.test.ts @@ -43,6 +43,8 @@ describe('buildRule', () => { }); const expected: Partial = { actions: [], + author: ['Elastic'], + building_block_type: 'default', created_by: 'elastic', description: 'Detecting root and admin users', enabled: false, @@ -53,14 +55,17 @@ describe('buildRule', () => { index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], interval: 'some interval', language: 'kuery', + license: 'Elastic License', max_signals: 10000, name: 'some-name', output_index: '.siem-signals', query: 'user.name: root or user.name: admin', references: ['http://google.com'], risk_score: 50, + risk_score_mapping: [], rule_id: 'rule-1', severity: 'high', + severity_mapping: [], tags: ['some fake tag 1', 'some fake tag 2'], threat: [], to: 'now', @@ -106,6 +111,8 @@ describe('buildRule', () => { }); const expected: Partial = { actions: [], + author: ['Elastic'], + building_block_type: 'default', created_by: 'elastic', description: 'Detecting root and admin users', enabled: true, @@ -116,14 +123,17 @@ describe('buildRule', () => { index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], interval: 'some interval', language: 'kuery', + license: 'Elastic License', max_signals: 10000, name: 'some-name', output_index: '.siem-signals', query: 'user.name: root or user.name: admin', references: ['http://google.com'], risk_score: 50, + risk_score_mapping: [], rule_id: 'rule-1', severity: 'high', + severity_mapping: [], tags: ['some fake tag 1', 'some fake tag 2'], threat: [], to: 'now', @@ -158,6 +168,8 @@ describe('buildRule', () => { }); const expected: Partial = { actions: [], + author: ['Elastic'], + building_block_type: 'default', created_by: 'elastic', description: 'Detecting root and admin users', enabled: true, @@ -168,6 +180,7 @@ describe('buildRule', () => { index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], interval: 'some interval', language: 'kuery', + license: 'Elastic License', max_signals: 10000, name: 'some-name', note: '', @@ -175,8 +188,10 @@ describe('buildRule', () => { query: 'user.name: root or user.name: admin', references: ['http://google.com'], risk_score: 50, + risk_score_mapping: [], rule_id: 'rule-1', severity: 'high', + severity_mapping: [], tags: ['some fake tag 1', 'some fake tag 2'], threat: [], to: 'now', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.ts index bde9c970b0c8c..fc8b26450c852 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.ts @@ -42,13 +42,16 @@ export const buildRule = ({ id, rule_id: ruleParams.ruleId ?? '(unknown rule_id)', actions, + author: ruleParams.author ?? [], + building_block_type: ruleParams.buildingBlockType, false_positives: ruleParams.falsePositives, saved_id: ruleParams.savedId, timeline_id: ruleParams.timelineId, timeline_title: ruleParams.timelineTitle, meta: ruleParams.meta, max_signals: ruleParams.maxSignals, - risk_score: ruleParams.riskScore, + risk_score: ruleParams.riskScore, // TODO: Risk Score Override via risk_score_mapping + risk_score_mapping: ruleParams.riskScoreMapping ?? [], output_index: ruleParams.outputIndex, description: ruleParams.description, note: ruleParams.note, @@ -57,10 +60,13 @@ export const buildRule = ({ index: ruleParams.index, interval, language: ruleParams.language, - name, + license: ruleParams.license, + name, // TODO: Rule Name Override via rule_name_override query: ruleParams.query, references: ruleParams.references, - severity: ruleParams.severity, + rule_name_override: ruleParams.ruleNameOverride, + severity: ruleParams.severity, // TODO: Severity Override via severity_mapping + severity_mapping: ruleParams.severityMapping ?? [], tags, type: ruleParams.type, to: ruleParams.to, @@ -69,6 +75,7 @@ export const buildRule = ({ created_by: createdBy, updated_by: updatedBy, threat: ruleParams.threat ?? [], + timestamp_override: ruleParams.timestampOverride, // TODO: Timestamp Override via timestamp_override throttle, version: ruleParams.version, created_at: createdAt, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.mock.ts index d60509b28f7da..0c56ed300cb48 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.mock.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.mock.ts @@ -19,6 +19,8 @@ export const getSignalParamsSchemaMock = (): Partial => ({ }); export const getSignalParamsSchemaDecodedMock = (): SignalParamsSchema => ({ + author: [], + buildingBlockType: null, description: 'Detecting root and admin users', falsePositives: [], filters: null, @@ -26,6 +28,7 @@ export const getSignalParamsSchemaDecodedMock = (): SignalParamsSchema => ({ immutable: false, index: null, language: 'kuery', + license: null, maxSignals: 100, meta: null, note: null, @@ -33,12 +36,16 @@ export const getSignalParamsSchemaDecodedMock = (): SignalParamsSchema => ({ query: 'user.name: root or user.name: admin', references: [], riskScore: 55, + riskScoreMapping: null, + ruleNameOverride: null, ruleId: 'rule-1', savedId: null, severity: 'high', + severityMapping: null, threat: null, timelineId: null, timelineTitle: null, + timestampOverride: null, to: 'now', type: 'query', version: 1, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.ts index 5f95f635a6bd8..2583cf2c8da91 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_params_schema.ts @@ -10,6 +10,8 @@ import { DEFAULT_MAX_SIGNALS } from '../../../../common/constants'; const signalSchema = schema.object({ anomalyThreshold: schema.maybe(schema.number()), + author: schema.arrayOf(schema.string(), { defaultValue: [] }), + buildingBlockType: schema.nullable(schema.string()), description: schema.string(), note: schema.nullable(schema.string()), falsePositives: schema.arrayOf(schema.string(), { defaultValue: [] }), @@ -18,6 +20,7 @@ const signalSchema = schema.object({ immutable: schema.boolean({ defaultValue: false }), index: schema.nullable(schema.arrayOf(schema.string())), language: schema.nullable(schema.string()), + license: schema.nullable(schema.string()), outputIndex: schema.nullable(schema.string()), savedId: schema.nullable(schema.string()), timelineId: schema.nullable(schema.string()), @@ -28,8 +31,13 @@ const signalSchema = schema.object({ filters: schema.nullable(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), maxSignals: schema.number({ defaultValue: DEFAULT_MAX_SIGNALS }), riskScore: schema.number(), + // TODO: Specify types explicitly since they're known? + riskScoreMapping: schema.nullable(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), + ruleNameOverride: schema.nullable(schema.string()), severity: schema.string(), + severityMapping: schema.nullable(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), threat: schema.nullable(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), + timestampOverride: schema.nullable(schema.string()), to: schema.string(), type: schema.string(), references: schema.arrayOf(schema.string(), { defaultValue: [] }), diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/types.ts index 0fb743c9c3ed6..365222d62d322 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/types.ts @@ -28,6 +28,13 @@ import { Version, MetaOrUndefined, RuleId, + AuthorOrUndefined, + BuildingBlockTypeOrUndefined, + LicenseOrUndefined, + RiskScoreMappingOrUndefined, + RuleNameOverrideOrUndefined, + SeverityMappingOrUndefined, + TimestampOverrideOrUndefined, } from '../../../common/detection_engine/schemas/common/schemas'; import { LegacyCallAPIOptions } from '../../../../../../src/core/server'; import { Filter } from '../../../../../../src/plugins/data/server'; @@ -38,6 +45,8 @@ export type PartialFilter = Partial; export interface RuleTypeParams { anomalyThreshold: AnomalyThresholdOrUndefined; + author: AuthorOrUndefined; + buildingBlockType: BuildingBlockTypeOrUndefined; description: Description; note: NoteOrUndefined; falsePositives: FalsePositives; @@ -46,6 +55,7 @@ export interface RuleTypeParams { immutable: Immutable; index: IndexOrUndefined; language: LanguageOrUndefined; + license: LicenseOrUndefined; outputIndex: OutputIndex; savedId: SavedIdOrUndefined; timelineId: TimelineIdOrUndefined; @@ -56,8 +66,12 @@ export interface RuleTypeParams { filters: PartialFilter[] | undefined; maxSignals: MaxSignals; riskScore: RiskScore; + riskScoreMapping: RiskScoreMappingOrUndefined; + ruleNameOverride: RuleNameOverrideOrUndefined; severity: Severity; + severityMapping: SeverityMappingOrUndefined; threat: ThreatOrUndefined; + timestampOverride: TimestampOverrideOrUndefined; to: To; type: RuleType; references: References; diff --git a/x-pack/test/detection_engine_api_integration/utils.ts b/x-pack/test/detection_engine_api_integration/utils.ts index 816df9c133ea1..6ad9cf4cd5baf 100644 --- a/x-pack/test/detection_engine_api_integration/utils.ts +++ b/x-pack/test/detection_engine_api_integration/utils.ts @@ -179,6 +179,7 @@ export const binaryToString = (res: any, callback: any): void => { */ export const getSimpleRuleOutput = (ruleId = 'rule-1'): Partial => ({ actions: [], + author: [], created_by: 'elastic', description: 'Simple Rule Query', enabled: true, @@ -192,10 +193,12 @@ export const getSimpleRuleOutput = (ruleId = 'rule-1'): Partial => output_index: '.siem-signals-default', max_signals: 100, risk_score: 1, + risk_score_mapping: [], name: 'Simple Rule Query', query: 'user.name: root or user.name: admin', references: [], severity: 'high', + severity_mapping: [], updated_by: 'elastic', tags: [], to: 'now', @@ -307,6 +310,7 @@ export const ruleToNdjson = (rule: Partial): Buffer => { */ export const getComplexRule = (ruleId = 'rule-1'): Partial => ({ actions: [], + author: [], name: 'Complex Rule Query', description: 'Complex Rule Query', false_positives: [ @@ -314,6 +318,7 @@ export const getComplexRule = (ruleId = 'rule-1'): Partial => ({ 'some text string about why another condition could be a false positive', ], risk_score: 1, + risk_score_mapping: [], rule_id: ruleId, filters: [ { @@ -340,6 +345,7 @@ export const getComplexRule = (ruleId = 'rule-1'): Partial => ({ to: 'now', from: 'now-6m', severity: 'high', + severity_mapping: [], language: 'kuery', type: 'query', threat: [ @@ -391,6 +397,7 @@ export const getComplexRule = (ruleId = 'rule-1'): Partial => ({ */ export const getComplexRuleOutput = (ruleId = 'rule-1'): Partial => ({ actions: [], + author: [], created_by: 'elastic', name: 'Complex Rule Query', description: 'Complex Rule Query', @@ -399,6 +406,7 @@ export const getComplexRuleOutput = (ruleId = 'rule-1'): Partial => 'some text string about why another condition could be a false positive', ], risk_score: 1, + risk_score_mapping: [], rule_id: ruleId, filters: [ { @@ -426,6 +434,7 @@ export const getComplexRuleOutput = (ruleId = 'rule-1'): Partial => to: 'now', from: 'now-6m', severity: 'high', + severity_mapping: [], language: 'kuery', type: 'query', threat: [ From 0f7afd4402e50f2a42f8951c94ea2fae7c422906 Mon Sep 17 00:00:00 2001 From: Madison Caldwell Date: Thu, 2 Jul 2020 01:00:27 -0400 Subject: [PATCH 20/34] [SIEM][Security Solution][Endpoint] Endpoint Artifact Manifest Management + Artifact Download and Distribution (#67707) * stub out task for the exceptions list packager * Hits list code and pages * refactor * Begin adding saved object and type definitions * Transforms to endpoint exceptions * Get internal SO client * update messaging * cleanup * Integrating with task manager * Integrated with task manager properly * Begin adding schemas * Add multiple OS and schema version support * filter by OS * Fixing sort * Move to security_solutions * siem -> securitySolution * Progress on downloads, cleanup * Add config, update artifact creation, add TODOs * Fixing buffer serialization problem * Adding cleanup to task * Handle HEAD req * proper header * More robust task management * single -> agnostic * Fix OS filtering * Scaffolding digital signatures / tests * Adds rotue for creating endpoint user * Cleanup * persisting user * Adding route to fetch created user * Addings tests for translating exceptions * Adding test for download API * Download tweaks + artifact generation fixes * reorganize * fix imports * Fixing test * Changes id of SO * integration tests setup * Add first integration tests * Cache layer * more schema validation * Set up for manifest update * minor change * remove setup code * add manifest schema * refactoring * manifest rewrite (partial) * finish scaffolding new manifest logic * syntax errors * more refactoring * Move to endpoint directory * minor cleanup * clean up old artifacts * Use diff appropriately * Fix download * schedule task on interval * Split up into client/manager * more mocks * config interval * Fixing download tests and adding cache tests * lint * mo money, mo progress * Converting to io-ts * More tests and mocks * even more tests and mocks * Merging both refactors * Adding more tests for the convertion layer * fix conflicts * Adding lzma types * Bug fixes * lint * resolve some type errors * Adding back in cache * Fixing download test * Changing cache to be sized * Fix manifest manager initialization * Hook up datasource service * Fix download tests * Incremental progress * Adds integration with ingest manager for auth * Update test fixture * Add manifest dispatch * Refactoring to use the same SO Client from ingest * bug fixes * build renovate config * Fix endpoint_app_context_services tests * Only index the fields that are necessary for searching * Integ test progress * mock and test city * Add task tests * Tests for artifact_client and manifest_client * Add manifest_manager tests * minor refactor * Finish manifest_manager tests * Type errors * Update integ test * Type errors, final cleanup * Fix integration test and add test for invalid api key * minor fixup * Remove compression * Update task interval * Removing .text suffix from translated list * Fixes hashes for unit tests * clean up yarn.lock * Remove lzma-native from package.json * missed updating one of the tests Co-authored-by: Alex Kahan --- x-pack/plugins/lists/server/mocks.ts | 2 +- .../common/endpoint/generate_data.ts | 7 + .../common/endpoint/schema/common.ts | 22 ++ .../common/endpoint/schema/manifest.ts | 27 ++ .../common/endpoint/types.ts | 4 + x-pack/plugins/security_solution/kibana.json | 1 + .../policy/store/policy_details/index.test.ts | 7 + .../endpoint_app_context_services.test.ts | 14 +- .../endpoint/endpoint_app_context_services.ts | 33 +- .../server/endpoint/ingest_integration.ts | 82 +++-- .../endpoint/lib/artifacts/cache.test.ts | 43 +++ .../server/endpoint/lib/artifacts/cache.ts | 37 +++ .../server/endpoint/lib/artifacts/common.ts | 17 + .../server/endpoint/lib/artifacts/index.ts | 12 + .../endpoint/lib/artifacts/lists.test.ts | 196 ++++++++++++ .../server/endpoint/lib/artifacts/lists.ts | 157 +++++++++ .../endpoint/lib/artifacts/manifest.test.ts | 150 +++++++++ .../server/endpoint/lib/artifacts/manifest.ts | 130 ++++++++ .../lib/artifacts/manifest_entry.test.ts | 64 ++++ .../endpoint/lib/artifacts/manifest_entry.ts | 48 +++ .../lib/artifacts/saved_object_mappings.ts | 67 ++++ .../endpoint/lib/artifacts/task.mock.ts | 11 + .../endpoint/lib/artifacts/task.test.ts | 73 +++++ .../server/endpoint/lib/artifacts/task.ts | 107 +++++++ .../server/endpoint/mocks.ts | 48 ++- .../artifacts/download_exception_list.test.ts | 301 ++++++++++++++++++ .../artifacts/download_exception_list.ts | 107 +++++++ .../server/endpoint/routes/artifacts/index.ts | 7 + .../endpoint/schemas/artifacts/common.ts | 19 ++ .../endpoint/schemas/artifacts/index.ts | 11 + .../endpoint/schemas/artifacts/lists.mock.ts | 32 ++ .../endpoint/schemas/artifacts/lists.ts | 65 ++++ .../request/download_artifact_schema.ts | 19 ++ .../schemas/artifacts/request/index.ts | 7 + .../response/download_artifact_schema.ts | 25 ++ .../schemas/artifacts/response/index.ts | 7 + .../schemas/artifacts/saved_objects.mock.ts | 40 +++ .../schemas/artifacts/saved_objects.ts | 31 ++ .../server/endpoint/schemas/index.ts | 7 + .../artifacts/artifact_client.mock.ts | 18 ++ .../artifacts/artifact_client.test.ts | 49 +++ .../services/artifacts/artifact_client.ts | 42 +++ .../endpoint/services/artifacts/index.ts | 8 + .../artifacts/manifest_client.mock.ts | 18 ++ .../artifacts/manifest_client.test.ts | 69 ++++ .../services/artifacts/manifest_client.ts | 85 +++++ .../artifacts/manifest_manager/index.ts | 7 + .../manifest_manager/manifest_manager.mock.ts | 111 +++++++ .../manifest_manager/manifest_manager.test.ts | 82 +++++ .../manifest_manager/manifest_manager.ts | 270 ++++++++++++++++ .../server/endpoint/services/index.ts | 7 + .../server/endpoint/types.ts | 2 +- .../security_solution/server/plugin.ts | 52 ++- .../security_solution/server/saved_objects.ts | 14 +- .../apis/endpoint/artifacts/index.ts | 93 ++++++ .../endpoint/artifacts/api_feature/data.json | 179 +++++++++++ 56 files changed, 3101 insertions(+), 42 deletions(-) create mode 100644 x-pack/plugins/security_solution/common/endpoint/schema/common.ts create mode 100644 x-pack/plugins/security_solution/common/endpoint/schema/manifest.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/lib/artifacts/cache.test.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/lib/artifacts/cache.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/lib/artifacts/index.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.test.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest_entry.test.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest_entry.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/lib/artifacts/saved_object_mappings.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.mock.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.test.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.test.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/routes/artifacts/index.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/common.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/index.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/lists.mock.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/lists.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/request/download_artifact_schema.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/request/index.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/response/download_artifact_schema.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/response/index.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.mock.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/schemas/index.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.mock.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.test.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/services/artifacts/index.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_client.mock.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_client.test.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_client.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/index.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/services/index.ts create mode 100644 x-pack/test/api_integration/apis/endpoint/artifacts/index.ts create mode 100644 x-pack/test/functional/es_archives/endpoint/artifacts/api_feature/data.json diff --git a/x-pack/plugins/lists/server/mocks.ts b/x-pack/plugins/lists/server/mocks.ts index aad4a25a900a1..ba565216fe431 100644 --- a/x-pack/plugins/lists/server/mocks.ts +++ b/x-pack/plugins/lists/server/mocks.ts @@ -18,6 +18,6 @@ const createSetupMock = (): jest.Mocked => { export const listMock = { createSetup: createSetupMock, - getExceptionList: getExceptionListClientMock, + getExceptionListClient: getExceptionListClientMock, getListClient: getListClientMock, }; diff --git a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts index a6fe12a9b029f..6720f3523d5c7 100644 --- a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts @@ -1034,6 +1034,13 @@ export class EndpointDocGenerator { enabled: true, streams: [], config: { + artifact_manifest: { + value: { + manifest_version: 'v0', + schema_version: '1.0.0', + artifacts: {}, + }, + }, policy: { value: policyFactory(), }, diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/common.ts b/x-pack/plugins/security_solution/common/endpoint/schema/common.ts new file mode 100644 index 0000000000000..7f8c938d54feb --- /dev/null +++ b/x-pack/plugins/security_solution/common/endpoint/schema/common.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as t from 'io-ts'; + +export const identifier = t.string; + +export const manifestVersion = t.string; + +export const manifestSchemaVersion = t.keyof({ + '1.0.0': null, +}); +export type ManifestSchemaVersion = t.TypeOf; + +export const sha256 = t.string; + +export const size = t.number; + +export const url = t.string; diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/manifest.ts b/x-pack/plugins/security_solution/common/endpoint/schema/manifest.ts new file mode 100644 index 0000000000000..470e9b13ef78a --- /dev/null +++ b/x-pack/plugins/security_solution/common/endpoint/schema/manifest.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as t from 'io-ts'; +import { identifier, manifestSchemaVersion, manifestVersion, sha256, size, url } from './common'; + +export const manifestEntrySchema = t.exact( + t.type({ + url, + sha256, + size, + }) +); + +export const manifestSchema = t.exact( + t.type({ + manifest_version: manifestVersion, + schema_version: manifestSchemaVersion, + artifacts: t.record(identifier, manifestEntrySchema), + }) +); + +export type ManifestEntrySchema = t.TypeOf; +export type ManifestSchema = t.TypeOf; diff --git a/x-pack/plugins/security_solution/common/endpoint/types.ts b/x-pack/plugins/security_solution/common/endpoint/types.ts index f76da977eef85..4efe89b2429ad 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types.ts @@ -5,6 +5,7 @@ */ import { PackageConfig, NewPackageConfig } from '../../../ingest_manager/common'; +import { ManifestSchema } from './schema/manifest'; /** * Object that allows you to maintain stateful information in the location object across navigation events @@ -691,6 +692,9 @@ export type NewPolicyData = NewPackageConfig & { enabled: boolean; streams: []; config: { + artifact_manifest: { + value: ManifestSchema; + }; policy: { value: PolicyConfig; }; diff --git a/x-pack/plugins/security_solution/kibana.json b/x-pack/plugins/security_solution/kibana.json index 8ce8820a8e57d..f6f2d5171312c 100644 --- a/x-pack/plugins/security_solution/kibana.json +++ b/x-pack/plugins/security_solution/kibana.json @@ -12,6 +12,7 @@ "features", "home", "ingestManager", + "taskManager", "inspector", "licensing", "maps", diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts index 469b71854dfcc..0bd623b27f4fb 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts @@ -41,6 +41,13 @@ describe('policy details: ', () => { enabled: true, streams: [], config: { + artifact_manifest: { + value: { + manifest_version: 'v0', + schema_version: '1.0.0', + artifacts: {}, + }, + }, policy: { value: policyConfigFactory(), }, diff --git a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.test.ts b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.test.ts index 8cf2ada9907d3..2daf259941cbf 100644 --- a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.test.ts @@ -3,11 +3,23 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + +import { httpServerMock } from '../../../../../src/core/server/mocks'; import { EndpointAppContextService } from './endpoint_app_context_services'; describe('test endpoint app context services', () => { - it('should throw error if start is not called', async () => { + it('should throw error on getAgentService if start is not called', async () => { const endpointAppContextService = new EndpointAppContextService(); expect(() => endpointAppContextService.getAgentService()).toThrow(Error); }); + it('should return undefined on getManifestManager if start is not called', async () => { + const endpointAppContextService = new EndpointAppContextService(); + expect(endpointAppContextService.getManifestManager()).toEqual(undefined); + }); + it('should throw error on getScopedSavedObjectsClient if start is not called', async () => { + const endpointAppContextService = new EndpointAppContextService(); + expect(() => + endpointAppContextService.getScopedSavedObjectsClient(httpServerMock.createKibanaRequest()) + ).toThrow(Error); + }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts index 1fce5d355f5a7..97a82049634c4 100644 --- a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts +++ b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts @@ -3,14 +3,22 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { + SavedObjectsServiceStart, + KibanaRequest, + SavedObjectsClientContract, +} from 'src/core/server'; import { AgentService, IngestManagerStartContract } from '../../../ingest_manager/server'; -import { handlePackageConfigCreate } from './ingest_integration'; +import { getPackageConfigCreateCallback } from './ingest_integration'; +import { ManifestManager } from './services/artifacts'; export type EndpointAppContextServiceStartContract = Pick< IngestManagerStartContract, 'agentService' > & { + manifestManager?: ManifestManager | undefined; registerIngestCallback: IngestManagerStartContract['registerExternalCallback']; + savedObjectsStart: SavedObjectsServiceStart; }; /** @@ -19,10 +27,20 @@ export type EndpointAppContextServiceStartContract = Pick< */ export class EndpointAppContextService { private agentService: AgentService | undefined; + private manifestManager: ManifestManager | undefined; + private savedObjectsStart: SavedObjectsServiceStart | undefined; public start(dependencies: EndpointAppContextServiceStartContract) { this.agentService = dependencies.agentService; - dependencies.registerIngestCallback('packageConfigCreate', handlePackageConfigCreate); + this.manifestManager = dependencies.manifestManager; + this.savedObjectsStart = dependencies.savedObjectsStart; + + if (this.manifestManager !== undefined) { + dependencies.registerIngestCallback( + 'packageConfigCreate', + getPackageConfigCreateCallback(this.manifestManager) + ); + } } public stop() {} @@ -33,4 +51,15 @@ export class EndpointAppContextService { } return this.agentService; } + + public getManifestManager(): ManifestManager | undefined { + return this.manifestManager; + } + + public getScopedSavedObjectsClient(req: KibanaRequest): SavedObjectsClientContract { + if (!this.savedObjectsStart) { + throw new Error(`must call start on ${EndpointAppContextService.name} to call getter`); + } + return this.savedObjectsStart.getScopedClient(req, { excludedWrappers: ['security'] }); + } } diff --git a/x-pack/plugins/security_solution/server/endpoint/ingest_integration.ts b/x-pack/plugins/security_solution/server/endpoint/ingest_integration.ts index bb0b950dad019..67a331f4ba677 100644 --- a/x-pack/plugins/security_solution/server/endpoint/ingest_integration.ts +++ b/x-pack/plugins/security_solution/server/endpoint/ingest_integration.ts @@ -4,46 +4,62 @@ * you may not use this file except in compliance with the Elastic License. */ +import { NewPackageConfig } from '../../../ingest_manager/common/types/models'; import { factory as policyConfigFactory } from '../../common/endpoint/models/policy_config'; import { NewPolicyData } from '../../common/endpoint/types'; -import { NewPackageConfig } from '../../../ingest_manager/common/types/models'; +import { ManifestManager } from './services/artifacts'; /** - * Callback to handle creation of package configs in Ingest Manager - * @param newPackageConfig + * Callback to handle creation of PackageConfigs in Ingest Manager */ -export const handlePackageConfigCreate = async ( - newPackageConfig: NewPackageConfig -): Promise => { - // We only care about Endpoint package configs - if (newPackageConfig.package?.name !== 'endpoint') { - return newPackageConfig; - } +export const getPackageConfigCreateCallback = ( + manifestManager: ManifestManager +): ((newPackageConfig: NewPackageConfig) => Promise) => { + const handlePackageConfigCreate = async ( + newPackageConfig: NewPackageConfig + ): Promise => { + // We only care about Endpoint package configs + if (newPackageConfig.package?.name !== 'endpoint') { + return newPackageConfig; + } - // We cast the type here so that any changes to the Endpoint specific data - // follow the types/schema expected - let updatedPackageConfig = newPackageConfig as NewPolicyData; + // We cast the type here so that any changes to the Endpoint specific data + // follow the types/schema expected + let updatedPackageConfig = newPackageConfig as NewPolicyData; - // Until we get the Default Policy Configuration in the Endpoint package, - // we will add it here manually at creation time. - // @ts-ignore - if (newPackageConfig.inputs.length === 0) { - updatedPackageConfig = { - ...newPackageConfig, - inputs: [ - { - type: 'endpoint', - enabled: true, - streams: [], - config: { - policy: { - value: policyConfigFactory(), + const wrappedManifest = await manifestManager.refresh({ initialize: true }); + if (wrappedManifest !== null) { + // Until we get the Default Policy Configuration in the Endpoint package, + // we will add it here manually at creation time. + // @ts-ignore + if (newPackageConfig.inputs.length === 0) { + updatedPackageConfig = { + ...newPackageConfig, + inputs: [ + { + type: 'endpoint', + enabled: true, + streams: [], + config: { + artifact_manifest: { + value: wrappedManifest.manifest.toEndpointFormat(), + }, + policy: { + value: policyConfigFactory(), + }, + }, }, - }, - }, - ], - }; - } + ], + }; + } + } + + try { + return updatedPackageConfig; + } finally { + await manifestManager.commit(wrappedManifest); + } + }; - return updatedPackageConfig; + return handlePackageConfigCreate; }; diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/cache.test.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/cache.test.ts new file mode 100644 index 0000000000000..5a0fb91345552 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/cache.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ExceptionsCache } from './cache'; + +describe('ExceptionsCache tests', () => { + let cache: ExceptionsCache; + + beforeEach(() => { + jest.clearAllMocks(); + cache = new ExceptionsCache(3); + }); + + test('it should cache', async () => { + cache.set('test', 'body'); + const cacheResp = cache.get('test'); + expect(cacheResp).toEqual('body'); + }); + + test('it should handle cache miss', async () => { + cache.set('test', 'body'); + const cacheResp = cache.get('not test'); + expect(cacheResp).toEqual(undefined); + }); + + test('it should handle cache eviction', async () => { + cache.set('1', 'a'); + cache.set('2', 'b'); + cache.set('3', 'c'); + const cacheResp = cache.get('1'); + expect(cacheResp).toEqual('a'); + + cache.set('4', 'd'); + const secondResp = cache.get('1'); + expect(secondResp).toEqual(undefined); + expect(cache.get('2')).toEqual('b'); + expect(cache.get('3')).toEqual('c'); + expect(cache.get('4')).toEqual('d'); + }); +}); diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/cache.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/cache.ts new file mode 100644 index 0000000000000..b7a4c2feb6bf8 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/cache.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; + * you may not use this file except in compliance with the Elastic License. + */ + +const DEFAULT_MAX_SIZE = 10; + +/** + * FIFO cache implementation for artifact downloads. + */ +export class ExceptionsCache { + private cache: Map; + private queue: string[]; + private maxSize: number; + + constructor(maxSize: number) { + this.cache = new Map(); + this.queue = []; + this.maxSize = maxSize || DEFAULT_MAX_SIZE; + } + + set(id: string, body: string) { + if (this.queue.length + 1 > this.maxSize) { + const entry = this.queue.shift(); + if (entry !== undefined) { + this.cache.delete(entry); + } + } + this.queue.push(id); + this.cache.set(id, body); + } + + get(id: string): string | undefined { + return this.cache.get(id); + } +} diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.ts new file mode 100644 index 0000000000000..4c3153ca0ef11 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.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; + * you may not use this file except in compliance with the Elastic License. + */ + +export const ArtifactConstants = { + GLOBAL_ALLOWLIST_NAME: 'endpoint-exceptionlist', + SAVED_OBJECT_TYPE: 'endpoint:exceptions-artifact', + SUPPORTED_OPERATING_SYSTEMS: ['linux', 'macos', 'windows'], + SCHEMA_VERSION: '1.0.0', +}; + +export const ManifestConstants = { + SAVED_OBJECT_TYPE: 'endpoint:exceptions-manifest', + SCHEMA_VERSION: '1.0.0', +}; diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/index.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/index.ts new file mode 100644 index 0000000000000..ee7d44459aa38 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/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; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './cache'; +export * from './common'; +export * from './lists'; +export * from './manifest'; +export * from './manifest_entry'; +export * from './task'; diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts new file mode 100644 index 0000000000000..738890fb4038f --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts @@ -0,0 +1,196 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ExceptionListClient } from '../../../../../lists/server'; +import { listMock } from '../../../../../lists/server/mocks'; +import { getFoundExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/found_exception_list_item_schema.mock'; +import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; +import { EntriesArray, EntryList } from '../../../../../lists/common/schemas/types/entries'; +import { getFullEndpointExceptionList } from './lists'; + +describe('buildEventTypeSignal', () => { + let mockExceptionClient: ExceptionListClient; + + beforeEach(() => { + jest.clearAllMocks(); + mockExceptionClient = listMock.getExceptionListClient(); + }); + + test('it should convert the exception lists response to the proper endpoint format', async () => { + const expectedEndpointExceptions = { + exceptions_list: [ + { + entries: [ + { + field: 'nested.field', + operator: 'included', + type: 'exact_cased', + value: 'some value', + }, + ], + field: 'some.parentField', + type: 'nested', + }, + { + field: 'some.not.nested.field', + operator: 'included', + type: 'exact_cased', + value: 'some value', + }, + ], + }; + + const first = getFoundExceptionListItemSchemaMock(); + mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); + const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', '1.0.0'); + expect(resp).toEqual(expectedEndpointExceptions); + }); + + test('it should convert simple fields', async () => { + const testEntries: EntriesArray = [ + { field: 'server.domain', operator: 'included', type: 'match', value: 'DOMAIN' }, + { field: 'server.ip', operator: 'included', type: 'match', value: '192.168.1.1' }, + { field: 'host.hostname', operator: 'included', type: 'match', value: 'estc' }, + ]; + + const expectedEndpointExceptions = { + exceptions_list: [ + { + field: 'server.domain', + operator: 'included', + type: 'exact_cased', + value: 'DOMAIN', + }, + { + field: 'server.ip', + operator: 'included', + type: 'exact_cased', + value: '192.168.1.1', + }, + { + field: 'host.hostname', + operator: 'included', + type: 'exact_cased', + value: 'estc', + }, + ], + }; + + const first = getFoundExceptionListItemSchemaMock(); + first.data[0].entries = testEntries; + mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); + + const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', '1.0.0'); + expect(resp).toEqual(expectedEndpointExceptions); + }); + + test('it should convert fields case sensitive', async () => { + const testEntries: EntriesArray = [ + { field: 'server.domain.text', operator: 'included', type: 'match', value: 'DOMAIN' }, + { field: 'server.ip', operator: 'included', type: 'match', value: '192.168.1.1' }, + { + field: 'host.hostname.text', + operator: 'included', + type: 'match_any', + value: ['estc', 'kibana'], + }, + ]; + + const expectedEndpointExceptions = { + exceptions_list: [ + { + field: 'server.domain', + operator: 'included', + type: 'exact_caseless', + value: 'DOMAIN', + }, + { + field: 'server.ip', + operator: 'included', + type: 'exact_cased', + value: '192.168.1.1', + }, + { + field: 'host.hostname', + operator: 'included', + type: 'exact_caseless_any', + value: ['estc', 'kibana'], + }, + ], + }; + + const first = getFoundExceptionListItemSchemaMock(); + first.data[0].entries = testEntries; + mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); + + const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', '1.0.0'); + expect(resp).toEqual(expectedEndpointExceptions); + }); + + test('it should ignore unsupported entries', async () => { + // Lists and exists are not supported by the Endpoint + const testEntries: EntriesArray = [ + { field: 'server.domain', operator: 'included', type: 'match', value: 'DOMAIN' }, + { + field: 'server.domain', + operator: 'included', + type: 'list', + list: { + id: 'lists_not_supported', + type: 'keyword', + }, + } as EntryList, + { field: 'server.ip', operator: 'included', type: 'exists' }, + ]; + + const expectedEndpointExceptions = { + exceptions_list: [ + { + field: 'server.domain', + operator: 'included', + type: 'exact_cased', + value: 'DOMAIN', + }, + ], + }; + + const first = getFoundExceptionListItemSchemaMock(); + first.data[0].entries = testEntries; + mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); + + const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', '1.0.0'); + expect(resp).toEqual(expectedEndpointExceptions); + }); + + test('it should convert the exception lists response to the proper endpoint format while paging', async () => { + // The first call returns one exception + const first = getFoundExceptionListItemSchemaMock(); + + // The second call returns two exceptions + const second = getFoundExceptionListItemSchemaMock(); + second.data.push(getExceptionListItemSchemaMock()); + + // The third call returns no exceptions, paging stops + const third = getFoundExceptionListItemSchemaMock(); + third.data = []; + mockExceptionClient.findExceptionListItem = jest + .fn() + .mockReturnValueOnce(first) + .mockReturnValueOnce(second) + .mockReturnValueOnce(third); + const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', '1.0.0'); + expect(resp.exceptions_list.length).toEqual(6); + }); + + test('it should handle no exceptions', async () => { + const exceptionsResponse = getFoundExceptionListItemSchemaMock(); + exceptionsResponse.data = []; + exceptionsResponse.total = 0; + mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(exceptionsResponse); + const resp = await getFullEndpointExceptionList(mockExceptionClient, 'linux', '1.0.0'); + expect(resp.exceptions_list.length).toEqual(0); + }); +}); diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts new file mode 100644 index 0000000000000..7fd057afdbd55 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts @@ -0,0 +1,157 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createHash } from 'crypto'; +import { validate } from '../../../../common/validate'; + +import { + Entry, + EntryNested, + EntryMatch, + EntryMatchAny, +} from '../../../../../lists/common/schemas/types/entries'; +import { FoundExceptionListItemSchema } from '../../../../../lists/common/schemas/response/found_exception_list_item_schema'; +import { ExceptionListClient } from '../../../../../lists/server'; +import { + InternalArtifactSchema, + TranslatedEntry, + TranslatedEntryMatch, + TranslatedEntryMatchAny, + TranslatedEntryNested, + WrappedTranslatedExceptionList, + wrappedExceptionList, +} from '../../schemas'; +import { ArtifactConstants } from './common'; + +export async function buildArtifact( + exceptions: WrappedTranslatedExceptionList, + os: string, + schemaVersion: string +): Promise { + const exceptionsBuffer = Buffer.from(JSON.stringify(exceptions)); + const sha256 = createHash('sha256').update(exceptionsBuffer.toString()).digest('hex'); + + return { + identifier: `${ArtifactConstants.GLOBAL_ALLOWLIST_NAME}-${os}-${schemaVersion}`, + sha256, + encoding: 'application/json', + created: Date.now(), + body: exceptionsBuffer.toString('base64'), + size: exceptionsBuffer.byteLength, + }; +} + +export async function getFullEndpointExceptionList( + eClient: ExceptionListClient, + os: string, + schemaVersion: string +): Promise { + const exceptions: WrappedTranslatedExceptionList = { exceptions_list: [] }; + let numResponses = 0; + let page = 1; + + do { + const response = await eClient.findExceptionListItem({ + listId: 'endpoint_list', + namespaceType: 'agnostic', + filter: `exception-list-agnostic.attributes._tags:\"os:${os}\"`, + perPage: 100, + page, + sortField: 'created_at', + sortOrder: 'desc', + }); + + if (response?.data !== undefined) { + numResponses = response.data.length; + + exceptions.exceptions_list = exceptions.exceptions_list.concat( + translateToEndpointExceptions(response, schemaVersion) + ); + + page++; + } else { + break; + } + } while (numResponses > 0); + + const [validated, errors] = validate(exceptions, wrappedExceptionList); + if (errors != null) { + throw new Error(errors); + } + return validated as WrappedTranslatedExceptionList; +} + +/** + * Translates Exception list items to Exceptions the endpoint can understand + * @param exc + */ +export function translateToEndpointExceptions( + exc: FoundExceptionListItemSchema, + schemaVersion: string +): TranslatedEntry[] { + const translatedList: TranslatedEntry[] = []; + + if (schemaVersion === '1.0.0') { + exc.data.forEach((list) => { + list.entries.forEach((entry) => { + const tEntry = translateEntry(schemaVersion, entry); + if (tEntry !== undefined) { + translatedList.push(tEntry); + } + }); + }); + } else { + throw new Error('unsupported schemaVersion'); + } + return translatedList; +} + +function translateEntry( + schemaVersion: string, + entry: Entry | EntryNested +): TranslatedEntry | undefined { + let translatedEntry; + switch (entry.type) { + case 'nested': { + const e = (entry as unknown) as EntryNested; + const nestedEntries: TranslatedEntry[] = []; + for (const nestedEntry of e.entries) { + const translation = translateEntry(schemaVersion, nestedEntry); + if (translation !== undefined) { + nestedEntries.push(translation); + } + } + translatedEntry = { + entries: nestedEntries, + field: e.field, + type: 'nested', + } as TranslatedEntryNested; + break; + } + case 'match': { + const e = (entry as unknown) as EntryMatch; + translatedEntry = { + field: e.field.endsWith('.text') ? e.field.substring(0, e.field.length - 5) : e.field, + operator: e.operator, + type: e.field.endsWith('.text') ? 'exact_caseless' : 'exact_cased', + value: e.value, + } as TranslatedEntryMatch; + break; + } + case 'match_any': + { + const e = (entry as unknown) as EntryMatchAny; + translatedEntry = { + field: e.field.endsWith('.text') ? e.field.substring(0, e.field.length - 5) : e.field, + operator: e.operator, + type: e.field.endsWith('.text') ? 'exact_caseless_any' : 'exact_cased_any', + value: e.value, + } as TranslatedEntryMatchAny; + } + break; + } + return translatedEntry || undefined; +} diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.test.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.test.ts new file mode 100644 index 0000000000000..0434e3d8ffcb2 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.test.ts @@ -0,0 +1,150 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ManifestSchemaVersion } from '../../../../common/endpoint/schema/common'; +import { InternalArtifactSchema } from '../../schemas'; +import { + getInternalArtifactMock, + getInternalArtifactMockWithDiffs, +} from '../../schemas/artifacts/saved_objects.mock'; +import { Manifest } from './manifest'; + +describe('manifest', () => { + describe('Manifest object sanity checks', () => { + const artifacts: InternalArtifactSchema[] = []; + const now = new Date(); + let manifest1: Manifest; + let manifest2: Manifest; + + beforeAll(async () => { + const artifactLinux = await getInternalArtifactMock('linux', '1.0.0'); + const artifactMacos = await getInternalArtifactMock('macos', '1.0.0'); + const artifactWindows = await getInternalArtifactMock('windows', '1.0.0'); + artifacts.push(artifactLinux); + artifacts.push(artifactMacos); + artifacts.push(artifactWindows); + + manifest1 = new Manifest(now, '1.0.0', 'v0'); + manifest1.addEntry(artifactLinux); + manifest1.addEntry(artifactMacos); + manifest1.addEntry(artifactWindows); + manifest1.setVersion('abcd'); + + const newArtifactLinux = await getInternalArtifactMockWithDiffs('linux', '1.0.0'); + manifest2 = new Manifest(new Date(), '1.0.0', 'v0'); + manifest2.addEntry(newArtifactLinux); + manifest2.addEntry(artifactMacos); + manifest2.addEntry(artifactWindows); + }); + + test('Can create manifest with valid schema version', () => { + const manifest = new Manifest(new Date(), '1.0.0', 'v0'); + expect(manifest).toBeInstanceOf(Manifest); + }); + + test('Cannot create manifest with invalid schema version', () => { + expect(() => { + new Manifest(new Date(), 'abcd' as ManifestSchemaVersion, 'v0'); + }).toThrow(); + }); + + test('Manifest transforms correctly to expected endpoint format', async () => { + expect(manifest1.toEndpointFormat()).toStrictEqual({ + artifacts: { + 'endpoint-exceptionlist-linux-1.0.0': { + sha256: '70d2e0ee5db0073b242df9af32e64447b932b73c3e66de3a922c61a4077b1a9c', + size: 268, + url: + '/api/endpoint/artifacts/download/endpoint-exceptionlist-linux-1.0.0/70d2e0ee5db0073b242df9af32e64447b932b73c3e66de3a922c61a4077b1a9c', + }, + 'endpoint-exceptionlist-macos-1.0.0': { + sha256: '70d2e0ee5db0073b242df9af32e64447b932b73c3e66de3a922c61a4077b1a9c', + size: 268, + url: + '/api/endpoint/artifacts/download/endpoint-exceptionlist-macos-1.0.0/70d2e0ee5db0073b242df9af32e64447b932b73c3e66de3a922c61a4077b1a9c', + }, + 'endpoint-exceptionlist-windows-1.0.0': { + sha256: '70d2e0ee5db0073b242df9af32e64447b932b73c3e66de3a922c61a4077b1a9c', + size: 268, + url: + '/api/endpoint/artifacts/download/endpoint-exceptionlist-windows-1.0.0/70d2e0ee5db0073b242df9af32e64447b932b73c3e66de3a922c61a4077b1a9c', + }, + }, + manifest_version: 'abcd', + schema_version: '1.0.0', + }); + }); + + test('Manifest transforms correctly to expected saved object format', async () => { + expect(manifest1.toSavedObject()).toStrictEqual({ + created: now.getTime(), + ids: [ + 'endpoint-exceptionlist-linux-1.0.0-70d2e0ee5db0073b242df9af32e64447b932b73c3e66de3a922c61a4077b1a9c', + 'endpoint-exceptionlist-macos-1.0.0-70d2e0ee5db0073b242df9af32e64447b932b73c3e66de3a922c61a4077b1a9c', + 'endpoint-exceptionlist-windows-1.0.0-70d2e0ee5db0073b242df9af32e64447b932b73c3e66de3a922c61a4077b1a9c', + ], + }); + }); + + test('Manifest returns diffs since supplied manifest', async () => { + const diffs = manifest2.diff(manifest1); + expect(diffs).toEqual([ + { + id: + 'endpoint-exceptionlist-linux-1.0.0-70d2e0ee5db0073b242df9af32e64447b932b73c3e66de3a922c61a4077b1a9c', + type: 'delete', + }, + { + id: + 'endpoint-exceptionlist-linux-1.0.0-69328f83418f4957470640ed6cc605be6abb5fe80e0e388fd74f9764ad7ed5d1', + type: 'add', + }, + ]); + }); + + test('Manifest returns data for given artifact', async () => { + const artifact = artifacts[0]; + const returned = manifest1.getArtifact(`${artifact.identifier}-${artifact.sha256}`); + expect(returned).toEqual(artifact); + }); + + test('Manifest returns entries map', async () => { + const entries = manifest1.getEntries(); + const keys = Object.keys(entries); + expect(keys).toEqual([ + 'endpoint-exceptionlist-linux-1.0.0-70d2e0ee5db0073b242df9af32e64447b932b73c3e66de3a922c61a4077b1a9c', + 'endpoint-exceptionlist-macos-1.0.0-70d2e0ee5db0073b242df9af32e64447b932b73c3e66de3a922c61a4077b1a9c', + 'endpoint-exceptionlist-windows-1.0.0-70d2e0ee5db0073b242df9af32e64447b932b73c3e66de3a922c61a4077b1a9c', + ]); + }); + + test('Manifest returns true if contains artifact', async () => { + const found = manifest1.contains( + 'endpoint-exceptionlist-macos-1.0.0-70d2e0ee5db0073b242df9af32e64447b932b73c3e66de3a922c61a4077b1a9c' + ); + expect(found).toEqual(true); + }); + + test('Manifest can be created from list of artifacts', async () => { + const manifest = Manifest.fromArtifacts(artifacts, '1.0.0', 'v0'); + expect( + manifest.contains( + 'endpoint-exceptionlist-linux-1.0.0-70d2e0ee5db0073b242df9af32e64447b932b73c3e66de3a922c61a4077b1a9c' + ) + ).toEqual(true); + expect( + manifest.contains( + 'endpoint-exceptionlist-macos-1.0.0-70d2e0ee5db0073b242df9af32e64447b932b73c3e66de3a922c61a4077b1a9c' + ) + ).toEqual(true); + expect( + manifest.contains( + 'endpoint-exceptionlist-windows-1.0.0-70d2e0ee5db0073b242df9af32e64447b932b73c3e66de3a922c61a4077b1a9c' + ) + ).toEqual(true); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.ts new file mode 100644 index 0000000000000..c343568226e22 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest.ts @@ -0,0 +1,130 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { validate } from '../../../../common/validate'; +import { InternalArtifactSchema, InternalManifestSchema } from '../../schemas/artifacts'; +import { + manifestSchemaVersion, + ManifestSchemaVersion, +} from '../../../../common/endpoint/schema/common'; +import { ManifestSchema, manifestSchema } from '../../../../common/endpoint/schema/manifest'; +import { ManifestEntry } from './manifest_entry'; + +export interface ManifestDiff { + type: string; + id: string; +} + +export class Manifest { + private created: Date; + private entries: Record; + private schemaVersion: ManifestSchemaVersion; + + // For concurrency control + private version: string; + + constructor(created: Date, schemaVersion: string, version: string) { + this.created = created; + this.entries = {}; + this.version = version; + + const [validated, errors] = validate( + (schemaVersion as unknown) as object, + manifestSchemaVersion + ); + + if (errors != null || validated === null) { + throw new Error(`Invalid manifest version: ${schemaVersion}`); + } + + this.schemaVersion = validated; + } + + public static fromArtifacts( + artifacts: InternalArtifactSchema[], + schemaVersion: string, + version: string + ): Manifest { + const manifest = new Manifest(new Date(), schemaVersion, version); + artifacts.forEach((artifact) => { + manifest.addEntry(artifact); + }); + return manifest; + } + + public getSchemaVersion(): ManifestSchemaVersion { + return this.schemaVersion; + } + + public getVersion(): string { + return this.version; + } + + public setVersion(version: string) { + this.version = version; + } + + public addEntry(artifact: InternalArtifactSchema) { + const entry = new ManifestEntry(artifact); + this.entries[entry.getDocId()] = entry; + } + + public contains(artifactId: string): boolean { + return artifactId in this.entries; + } + + public getEntries(): Record { + return this.entries; + } + + public getArtifact(artifactId: string): InternalArtifactSchema { + return this.entries[artifactId].getArtifact(); + } + + public diff(manifest: Manifest): ManifestDiff[] { + const diffs: ManifestDiff[] = []; + + for (const id in manifest.getEntries()) { + if (!this.contains(id)) { + diffs.push({ type: 'delete', id }); + } + } + + for (const id in this.entries) { + if (!manifest.contains(id)) { + diffs.push({ type: 'add', id }); + } + } + + return diffs; + } + + public toEndpointFormat(): ManifestSchema { + const manifestObj: ManifestSchema = { + manifest_version: this.version ?? 'v0', + schema_version: this.schemaVersion, + artifacts: {}, + }; + + for (const entry of Object.values(this.entries)) { + manifestObj.artifacts[entry.getIdentifier()] = entry.getRecord(); + } + + const [validated, errors] = validate(manifestObj, manifestSchema); + if (errors != null) { + throw new Error(errors); + } + + return validated as ManifestSchema; + } + + public toSavedObject(): InternalManifestSchema { + return { + created: this.created.getTime(), + ids: Object.keys(this.entries), + }; + } +} diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest_entry.test.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest_entry.test.ts new file mode 100644 index 0000000000000..34bd2b0f388e1 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest_entry.test.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InternalArtifactSchema } from '../../schemas'; +import { getInternalArtifactMock } from '../../schemas/artifacts/saved_objects.mock'; +import { ManifestEntry } from './manifest_entry'; + +describe('manifest_entry', () => { + describe('ManifestEntry object sanity checks', () => { + let artifact: InternalArtifactSchema; + let manifestEntry: ManifestEntry; + + beforeAll(async () => { + artifact = await getInternalArtifactMock('windows', '1.0.0'); + manifestEntry = new ManifestEntry(artifact); + }); + + test('Can create manifest entry', () => { + expect(manifestEntry).toBeInstanceOf(ManifestEntry); + }); + + test('Correct doc_id is returned', () => { + expect(manifestEntry.getDocId()).toEqual( + 'endpoint-exceptionlist-windows-1.0.0-70d2e0ee5db0073b242df9af32e64447b932b73c3e66de3a922c61a4077b1a9c' + ); + }); + + test('Correct identifier is returned', () => { + expect(manifestEntry.getIdentifier()).toEqual('endpoint-exceptionlist-windows-1.0.0'); + }); + + test('Correct sha256 is returned', () => { + expect(manifestEntry.getSha256()).toEqual( + '70d2e0ee5db0073b242df9af32e64447b932b73c3e66de3a922c61a4077b1a9c' + ); + }); + + test('Correct size is returned', () => { + expect(manifestEntry.getSize()).toEqual(268); + }); + + test('Correct url is returned', () => { + expect(manifestEntry.getUrl()).toEqual( + '/api/endpoint/artifacts/download/endpoint-exceptionlist-windows-1.0.0/70d2e0ee5db0073b242df9af32e64447b932b73c3e66de3a922c61a4077b1a9c' + ); + }); + + test('Correct artifact is returned', () => { + expect(manifestEntry.getArtifact()).toEqual(artifact); + }); + + test('Correct record is returned', () => { + expect(manifestEntry.getRecord()).toEqual({ + sha256: '70d2e0ee5db0073b242df9af32e64447b932b73c3e66de3a922c61a4077b1a9c', + size: 268, + url: + '/api/endpoint/artifacts/download/endpoint-exceptionlist-windows-1.0.0/70d2e0ee5db0073b242df9af32e64447b932b73c3e66de3a922c61a4077b1a9c', + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest_entry.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest_entry.ts new file mode 100644 index 0000000000000..00fd446bf14b5 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/manifest_entry.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InternalArtifactSchema } from '../../schemas/artifacts'; +import { ManifestEntrySchema } from '../../../../common/endpoint/schema/manifest'; + +export class ManifestEntry { + private artifact: InternalArtifactSchema; + + constructor(artifact: InternalArtifactSchema) { + this.artifact = artifact; + } + + public getDocId(): string { + return `${this.getIdentifier()}-${this.getSha256()}`; + } + + public getIdentifier(): string { + return this.artifact.identifier; + } + + public getSha256(): string { + return this.artifact.sha256; + } + + public getSize(): number { + return this.artifact.size; + } + + public getUrl(): string { + return `/api/endpoint/artifacts/download/${this.getIdentifier()}/${this.getSha256()}`; + } + + public getArtifact(): InternalArtifactSchema { + return this.artifact; + } + + public getRecord(): ManifestEntrySchema { + return { + sha256: this.getSha256(), + size: this.getSize(), + url: this.getUrl(), + }; + } +} diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/saved_object_mappings.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/saved_object_mappings.ts new file mode 100644 index 0000000000000..d38026fbcbbd9 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/saved_object_mappings.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SavedObjectsType } from '../../../../../../../src/core/server'; + +import { ArtifactConstants, ManifestConstants } from './common'; + +export const exceptionsArtifactSavedObjectType = ArtifactConstants.SAVED_OBJECT_TYPE; +export const manifestSavedObjectType = ManifestConstants.SAVED_OBJECT_TYPE; + +export const exceptionsArtifactSavedObjectMappings: SavedObjectsType['mappings'] = { + properties: { + identifier: { + type: 'keyword', + }, + sha256: { + type: 'keyword', + }, + encoding: { + type: 'keyword', + index: false, + }, + created: { + type: 'date', + index: false, + }, + body: { + type: 'binary', + index: false, + }, + size: { + type: 'long', + index: false, + }, + }, +}; + +export const manifestSavedObjectMappings: SavedObjectsType['mappings'] = { + properties: { + created: { + type: 'date', + index: false, + }, + // array of doc ids + ids: { + type: 'keyword', + index: false, + }, + }, +}; + +export const exceptionsArtifactType: SavedObjectsType = { + name: exceptionsArtifactSavedObjectType, + hidden: false, // TODO: should these be hidden? + namespaceType: 'agnostic', + mappings: exceptionsArtifactSavedObjectMappings, +}; + +export const manifestType: SavedObjectsType = { + name: manifestSavedObjectType, + hidden: false, // TODO: should these be hidden? + namespaceType: 'agnostic', + mappings: manifestSavedObjectMappings, +}; diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.mock.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.mock.ts new file mode 100644 index 0000000000000..4391d89f3b2b2 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.mock.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ManifestTask } from './task'; + +export class MockManifestTask extends ManifestTask { + public runTask = jest.fn(); +} diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.test.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.test.ts new file mode 100644 index 0000000000000..daa8a7dd83ee0 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.test.ts @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { taskManagerMock } from '../../../../../task_manager/server/mocks'; +import { TaskStatus } from '../../../../../task_manager/server'; + +import { createMockEndpointAppContext } from '../../mocks'; + +import { ManifestTaskConstants, ManifestTask } from './task'; +import { MockManifestTask } from './task.mock'; + +describe('task', () => { + describe('Periodic task sanity checks', () => { + test('can create task', () => { + const manifestTask = new ManifestTask({ + endpointAppContext: createMockEndpointAppContext(), + taskManager: taskManagerMock.createSetup(), + }); + expect(manifestTask).toBeInstanceOf(ManifestTask); + }); + + test('task should be registered', () => { + const mockTaskManager = taskManagerMock.createSetup(); + new ManifestTask({ + endpointAppContext: createMockEndpointAppContext(), + taskManager: mockTaskManager, + }); + expect(mockTaskManager.registerTaskDefinitions).toHaveBeenCalled(); + }); + + test('task should be scheduled', async () => { + const mockTaskManagerSetup = taskManagerMock.createSetup(); + const manifestTask = new ManifestTask({ + endpointAppContext: createMockEndpointAppContext(), + taskManager: mockTaskManagerSetup, + }); + const mockTaskManagerStart = taskManagerMock.createStart(); + manifestTask.start({ taskManager: mockTaskManagerStart }); + expect(mockTaskManagerStart.ensureScheduled).toHaveBeenCalled(); + }); + + test('task should run', async () => { + const mockContext = createMockEndpointAppContext(); + const mockTaskManager = taskManagerMock.createSetup(); + const mockManifestTask = new MockManifestTask({ + endpointAppContext: mockContext, + taskManager: mockTaskManager, + }); + const mockTaskInstance = { + id: ManifestTaskConstants.TYPE, + runAt: new Date(), + attempts: 0, + ownerId: '', + status: TaskStatus.Running, + startedAt: new Date(), + scheduledAt: new Date(), + retryAt: new Date(), + params: {}, + state: {}, + taskType: ManifestTaskConstants.TYPE, + }; + const createTaskRunner = + mockTaskManager.registerTaskDefinitions.mock.calls[0][0][ManifestTaskConstants.TYPE] + .createTaskRunner; + const taskRunner = createTaskRunner({ taskInstance: mockTaskInstance }); + await taskRunner.run(); + expect(mockManifestTask.runTask).toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.ts new file mode 100644 index 0000000000000..08d02e70dac16 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/task.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Logger } from 'src/core/server'; +import { + ConcreteTaskInstance, + TaskManagerSetupContract, + TaskManagerStartContract, +} from '../../../../../task_manager/server'; +import { EndpointAppContext } from '../../types'; + +export const ManifestTaskConstants = { + TIMEOUT: '1m', + TYPE: 'securitySolution:endpoint:exceptions-packager', + VERSION: '1.0.0', +}; + +export interface ManifestTaskSetupContract { + endpointAppContext: EndpointAppContext; + taskManager: TaskManagerSetupContract; +} + +export interface ManifestTaskStartContract { + taskManager: TaskManagerStartContract; +} + +export class ManifestTask { + private endpointAppContext: EndpointAppContext; + private logger: Logger; + + constructor(setupContract: ManifestTaskSetupContract) { + this.endpointAppContext = setupContract.endpointAppContext; + this.logger = this.endpointAppContext.logFactory.get(this.getTaskId()); + + setupContract.taskManager.registerTaskDefinitions({ + [ManifestTaskConstants.TYPE]: { + title: 'Security Solution Endpoint Exceptions Handler', + type: ManifestTaskConstants.TYPE, + timeout: ManifestTaskConstants.TIMEOUT, + createTaskRunner: ({ taskInstance }: { taskInstance: ConcreteTaskInstance }) => { + return { + run: async () => { + await this.runTask(taskInstance.id); + }, + cancel: async () => {}, + }; + }, + }, + }); + } + + public start = async (startContract: ManifestTaskStartContract) => { + try { + await startContract.taskManager.ensureScheduled({ + id: this.getTaskId(), + taskType: ManifestTaskConstants.TYPE, + scope: ['securitySolution'], + schedule: { + interval: '60s', + }, + state: {}, + params: { version: ManifestTaskConstants.VERSION }, + }); + } catch (e) { + this.logger.debug(`Error scheduling task, received ${e.message}`); + } + }; + + private getTaskId = (): string => { + return `${ManifestTaskConstants.TYPE}:${ManifestTaskConstants.VERSION}`; + }; + + public runTask = async (taskId: string) => { + // Check that this task is current + if (taskId !== this.getTaskId()) { + // old task, return + this.logger.debug(`Outdated task running: ${taskId}`); + return; + } + + const manifestManager = this.endpointAppContext.service.getManifestManager(); + + if (manifestManager === undefined) { + this.logger.debug('Manifest Manager not available.'); + return; + } + + manifestManager + .refresh() + .then((wrappedManifest) => { + if (wrappedManifest) { + return manifestManager.dispatch(wrappedManifest); + } + }) + .then((wrappedManifest) => { + if (wrappedManifest) { + return manifestManager.commit(wrappedManifest); + } + }) + .catch((err) => { + this.logger.error(err); + }); + }; +} diff --git a/x-pack/plugins/security_solution/server/endpoint/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/mocks.ts index ffd919db87fc9..55d7baec36dc6 100644 --- a/x-pack/plugins/security_solution/server/endpoint/mocks.ts +++ b/x-pack/plugins/security_solution/server/endpoint/mocks.ts @@ -5,23 +5,67 @@ */ import { ILegacyScopedClusterClient, SavedObjectsClientContract } from 'kibana/server'; +import { loggingSystemMock, savedObjectsServiceMock } from 'src/core/server/mocks'; import { xpackMocks } from '../../../../mocks'; import { AgentService, IngestManagerStartContract, ExternalCallback, } from '../../../ingest_manager/server'; -import { EndpointAppContextServiceStartContract } from './endpoint_app_context_services'; import { createPackageConfigServiceMock } from '../../../ingest_manager/server/mocks'; +import { ConfigType } from '../config'; +import { createMockConfig } from '../lib/detection_engine/routes/__mocks__'; +import { + EndpointAppContextService, + EndpointAppContextServiceStartContract, +} from './endpoint_app_context_services'; +import { + ManifestManagerMock, + getManifestManagerMock, +} from './services/artifacts/manifest_manager/manifest_manager.mock'; +import { EndpointAppContext } from './types'; + +/** + * Creates a mocked EndpointAppContext. + */ +export const createMockEndpointAppContext = ( + mockManifestManager?: ManifestManagerMock +): EndpointAppContext => { + return { + logFactory: loggingSystemMock.create(), + // @ts-ignore + config: createMockConfig() as ConfigType, + service: createMockEndpointAppContextService(mockManifestManager), + }; +}; + +/** + * Creates a mocked EndpointAppContextService + */ +export const createMockEndpointAppContextService = ( + mockManifestManager?: ManifestManagerMock +): jest.Mocked => { + return { + start: jest.fn(), + stop: jest.fn(), + getAgentService: jest.fn(), + // @ts-ignore + getManifestManager: mockManifestManager ?? jest.fn(), + getScopedSavedObjectsClient: jest.fn(), + }; +}; /** - * Crates a mocked input contract for the `EndpointAppContextService#start()` method + * Creates a mocked input contract for the `EndpointAppContextService#start()` method */ export const createMockEndpointAppContextServiceStartContract = (): jest.Mocked< EndpointAppContextServiceStartContract > => { return { agentService: createMockAgentService(), + savedObjectsStart: savedObjectsServiceMock.createStartContract(), + // @ts-ignore + manifestManager: getManifestManagerMock(), registerIngestCallback: jest.fn< ReturnType, Parameters diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.test.ts new file mode 100644 index 0000000000000..540976134d8ae --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.test.ts @@ -0,0 +1,301 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { + ILegacyClusterClient, + IRouter, + SavedObjectsClientContract, + ILegacyScopedClusterClient, + RouteConfig, + RequestHandler, + KibanaResponseFactory, + RequestHandlerContext, + SavedObject, +} from 'kibana/server'; +import { + elasticsearchServiceMock, + savedObjectsClientMock, + httpServiceMock, + httpServerMock, + loggingSystemMock, +} from 'src/core/server/mocks'; +import { ExceptionsCache } from '../../lib/artifacts/cache'; +import { ArtifactConstants } from '../../lib/artifacts'; +import { registerDownloadExceptionListRoute } from './download_exception_list'; +import { EndpointAppContextService } from '../../endpoint_app_context_services'; +import { createMockEndpointAppContextServiceStartContract } from '../../mocks'; +import { createMockConfig } from '../../../lib/detection_engine/routes/__mocks__'; +import { WrappedTranslatedExceptionList } from '../../schemas/artifacts/lists'; + +const mockArtifactName = `${ArtifactConstants.GLOBAL_ALLOWLIST_NAME}-windows-1.0.0`; +const expectedEndpointExceptions: WrappedTranslatedExceptionList = { + exceptions_list: [ + { + entries: [ + { + field: 'some.not.nested.field', + operator: 'included', + type: 'exact_cased', + value: 'some value', + }, + ], + field: 'some.field', + type: 'nested', + }, + { + field: 'some.not.nested.field', + operator: 'included', + type: 'exact_cased', + value: 'some value', + }, + ], +}; +const mockIngestSOResponse = { + page: 1, + per_page: 100, + total: 1, + saved_objects: [ + { + id: 'agent1', + type: 'agent', + references: [], + score: 0, + attributes: { + active: true, + access_api_key_id: 'pedTuHIBTEDt93wW0Fhr', + }, + }, + ], +}; +const AuthHeader = 'ApiKey cGVkVHVISUJURUR0OTN3VzBGaHI6TnU1U0JtbHJSeC12Rm9qQWpoSHlUZw=='; + +describe('test alerts route', () => { + let routerMock: jest.Mocked; + let mockClusterClient: jest.Mocked; + let mockScopedClient: jest.Mocked; + let mockSavedObjectClient: jest.Mocked; + let mockResponse: jest.Mocked; + // @ts-ignore + let routeConfig: RouteConfig; + let routeHandler: RequestHandler; + let endpointAppContextService: EndpointAppContextService; + let cache: ExceptionsCache; + let ingestSavedObjectClient: jest.Mocked; + + beforeEach(() => { + mockClusterClient = elasticsearchServiceMock.createClusterClient(); + mockScopedClient = elasticsearchServiceMock.createScopedClusterClient(); + mockSavedObjectClient = savedObjectsClientMock.create(); + mockResponse = httpServerMock.createResponseFactory(); + mockClusterClient.asScoped.mockReturnValue(mockScopedClient); + routerMock = httpServiceMock.createRouter(); + endpointAppContextService = new EndpointAppContextService(); + cache = new ExceptionsCache(5); + const startContract = createMockEndpointAppContextServiceStartContract(); + + // The authentication with the Fleet Plugin needs a separate scoped SO Client + ingestSavedObjectClient = savedObjectsClientMock.create(); + ingestSavedObjectClient.find.mockReturnValue(Promise.resolve(mockIngestSOResponse)); + // @ts-ignore + startContract.savedObjectsStart.getScopedClient.mockReturnValue(ingestSavedObjectClient); + endpointAppContextService.start(startContract); + + registerDownloadExceptionListRoute( + routerMock, + { + logFactory: loggingSystemMock.create(), + service: endpointAppContextService, + config: () => Promise.resolve(createMockConfig()), + }, + cache + ); + }); + + it('should serve the artifact to download', async () => { + const mockRequest = httpServerMock.createKibanaRequest({ + path: `/api/endpoint/artifacts/download/${mockArtifactName}/123456`, + method: 'get', + params: { sha256: '123456' }, + headers: { + authorization: AuthHeader, + }, + }); + + // Mock the SavedObjectsClient get response for fetching the artifact + const mockArtifact = { + id: '2468', + type: 'test', + references: [], + attributes: { + identifier: mockArtifactName, + schemaVersion: '1.0.0', + sha256: '123456', + encoding: 'application/json', + created: Date.now(), + body: Buffer.from(JSON.stringify(expectedEndpointExceptions)).toString('base64'), + size: 100, + }, + }; + const soFindResp: SavedObject = { + ...mockArtifact, + }; + ingestSavedObjectClient.get.mockImplementationOnce(() => Promise.resolve(soFindResp)); + + [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => + path.startsWith('/api/endpoint/artifacts/download') + )!; + + await routeHandler( + ({ + core: { + savedObjects: { + client: mockSavedObjectClient, + }, + }, + } as unknown) as RequestHandlerContext, + mockRequest, + mockResponse + ); + + const expectedHeaders = { + 'content-encoding': 'application/json', + 'content-disposition': `attachment; filename=${mockArtifactName}.json`, + }; + + expect(mockResponse.ok).toBeCalled(); + expect(mockResponse.ok.mock.calls[0][0]?.headers).toEqual(expectedHeaders); + const artifact = mockResponse.ok.mock.calls[0][0]?.body; + expect(artifact).toEqual(Buffer.from(mockArtifact.attributes.body, 'base64').toString()); + }); + + it('should handle fetching a non-existent artifact', async () => { + const mockRequest = httpServerMock.createKibanaRequest({ + path: `/api/endpoint/artifacts/download/${mockArtifactName}/123456`, + method: 'get', + params: { sha256: '789' }, + headers: { + authorization: AuthHeader, + }, + }); + + ingestSavedObjectClient.get.mockImplementationOnce(() => + // eslint-disable-next-line prefer-promise-reject-errors + Promise.reject({ output: { statusCode: 404 } }) + ); + + [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => + path.startsWith('/api/endpoint/artifacts/download') + )!; + + await routeHandler( + ({ + core: { + savedObjects: { + client: mockSavedObjectClient, + }, + }, + } as unknown) as RequestHandlerContext, + mockRequest, + mockResponse + ); + expect(mockResponse.notFound).toBeCalled(); + }); + + it('should utilize the cache', async () => { + const mockSha = '123456789'; + const mockRequest = httpServerMock.createKibanaRequest({ + path: `/api/endpoint/artifacts/download/${mockArtifactName}/${mockSha}`, + method: 'get', + params: { sha256: mockSha, identifier: mockArtifactName }, + headers: { + authorization: AuthHeader, + }, + }); + + // Add to the download cache + const mockArtifact = expectedEndpointExceptions; + const cacheKey = `${mockArtifactName}-${mockSha}`; + cache.set(cacheKey, JSON.stringify(mockArtifact)); + + [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => + path.startsWith('/api/endpoint/artifacts/download') + )!; + + await routeHandler( + ({ + core: { + savedObjects: { + client: mockSavedObjectClient, + }, + }, + } as unknown) as RequestHandlerContext, + mockRequest, + mockResponse + ); + expect(mockResponse.ok).toBeCalled(); + // The saved objects client should be bypassed as the cache will contain the download + expect(ingestSavedObjectClient.get.mock.calls.length).toEqual(0); + }); + + it('should respond with a 401 if a valid API Token is not supplied', async () => { + const mockSha = '123456789'; + const mockRequest = httpServerMock.createKibanaRequest({ + path: `/api/endpoint/artifacts/download/${mockArtifactName}/${mockSha}`, + method: 'get', + params: { sha256: mockSha, identifier: mockArtifactName }, + }); + + [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => + path.startsWith('/api/endpoint/artifacts/download') + )!; + + await routeHandler( + ({ + core: { + savedObjects: { + client: mockSavedObjectClient, + }, + }, + } as unknown) as RequestHandlerContext, + mockRequest, + mockResponse + ); + expect(mockResponse.unauthorized).toBeCalled(); + }); + + it('should respond with a 404 if an agent cannot be linked to the API token', async () => { + const mockSha = '123456789'; + const mockRequest = httpServerMock.createKibanaRequest({ + path: `/api/endpoint/artifacts/download/${mockArtifactName}/${mockSha}`, + method: 'get', + params: { sha256: mockSha, identifier: mockArtifactName }, + headers: { + authorization: AuthHeader, + }, + }); + + // Mock the SavedObjectsClient find response for verifying the API token with no results + mockIngestSOResponse.saved_objects = []; + mockIngestSOResponse.total = 0; + ingestSavedObjectClient.find.mockReturnValue(Promise.resolve(mockIngestSOResponse)); + + [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => + path.startsWith('/api/endpoint/artifacts/download') + )!; + + await routeHandler( + ({ + core: { + savedObjects: { + client: mockSavedObjectClient, + }, + }, + } as unknown) as RequestHandlerContext, + mockRequest, + mockResponse + ); + expect(mockResponse.notFound).toBeCalled(); + }); +}); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.ts b/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.ts new file mode 100644 index 0000000000000..337393e768a8f --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/download_exception_list.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + IRouter, + SavedObjectsClientContract, + HttpResponseOptions, + IKibanaResponse, + SavedObject, +} from 'src/core/server'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { authenticateAgentWithAccessToken } from '../../../../../ingest_manager/server/services/agents/authenticate'; +import { validate } from '../../../../common/validate'; +import { buildRouteValidation } from '../../../utils/build_validation/route_validation'; +import { ArtifactConstants, ExceptionsCache } from '../../lib/artifacts'; +import { + DownloadArtifactRequestParamsSchema, + downloadArtifactRequestParamsSchema, + downloadArtifactResponseSchema, + InternalArtifactSchema, +} from '../../schemas/artifacts'; +import { EndpointAppContext } from '../../types'; + +const allowlistBaseRoute: string = '/api/endpoint/artifacts'; + +/** + * Registers the exception list route to enable sensors to download an allowlist artifact + */ +export function registerDownloadExceptionListRoute( + router: IRouter, + endpointContext: EndpointAppContext, + cache: ExceptionsCache +) { + router.get( + { + path: `${allowlistBaseRoute}/download/{identifier}/{sha256}`, + validate: { + params: buildRouteValidation< + typeof downloadArtifactRequestParamsSchema, + DownloadArtifactRequestParamsSchema + >(downloadArtifactRequestParamsSchema), + }, + options: { tags: [] }, + }, + // @ts-ignore + async (context, req, res) => { + let scopedSOClient: SavedObjectsClientContract; + const logger = endpointContext.logFactory.get('download_exception_list'); + + // The ApiKey must be associated with an enrolled Fleet agent + try { + scopedSOClient = endpointContext.service.getScopedSavedObjectsClient(req); + await authenticateAgentWithAccessToken(scopedSOClient, req); + } catch (err) { + if (err.output.statusCode === 401) { + return res.unauthorized(); + } else { + return res.notFound(); + } + } + + const buildAndValidateResponse = (artName: string, body: string): IKibanaResponse => { + const artifact: HttpResponseOptions = { + body, + headers: { + 'content-encoding': 'application/json', + 'content-disposition': `attachment; filename=${artName}.json`, + }, + }; + + const [validated, errors] = validate(artifact, downloadArtifactResponseSchema); + if (errors !== null || validated === null) { + return res.internalError({ body: errors! }); + } else { + return res.ok((validated as unknown) as HttpResponseOptions); + } + }; + + const id = `${req.params.identifier}-${req.params.sha256}`; + const cacheResp = cache.get(id); + + if (cacheResp) { + logger.debug(`Cache HIT artifact ${id}`); + return buildAndValidateResponse(req.params.identifier, cacheResp); + } else { + logger.debug(`Cache MISS artifact ${id}`); + return scopedSOClient + .get(ArtifactConstants.SAVED_OBJECT_TYPE, id) + .then((artifact: SavedObject) => { + const body = Buffer.from(artifact.attributes.body, 'base64').toString(); + cache.set(id, body); + return buildAndValidateResponse(artifact.attributes.identifier, body); + }) + .catch((err) => { + if (err?.output?.statusCode === 404) { + return res.notFound({ body: `No artifact found for ${id}` }); + } else { + return res.internalError({ body: err }); + } + }); + } + } + ); +} diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/index.ts b/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/index.ts new file mode 100644 index 0000000000000..945646c73c46c --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/routes/artifacts/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './download_exception_list'; diff --git a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/common.ts b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/common.ts new file mode 100644 index 0000000000000..3c066e150288a --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/common.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as t from 'io-ts'; + +export const body = t.string; + +export const created = t.number; // TODO: Make this into an ISO Date string check + +export const encoding = t.keyof({ + 'application/json': null, +}); + +export const schemaVersion = t.keyof({ + '1.0.0': null, +}); diff --git a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/index.ts b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/index.ts new file mode 100644 index 0000000000000..908fbb698adef --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './common'; +export * from './lists'; +export * from './request'; +export * from './response'; +export * from './saved_objects'; diff --git a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/lists.mock.ts b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/lists.mock.ts new file mode 100644 index 0000000000000..7354b5fd0ec4d --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/lists.mock.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { WrappedTranslatedExceptionList } from './lists'; + +export const getTranslatedExceptionListMock = (): WrappedTranslatedExceptionList => { + return { + exceptions_list: [ + { + entries: [ + { + field: 'some.not.nested.field', + operator: 'included', + type: 'exact_cased', + value: 'some value', + }, + ], + field: 'some.field', + type: 'nested', + }, + { + field: 'some.not.nested.field', + operator: 'included', + type: 'exact_cased', + value: 'some value', + }, + ], + }; +}; diff --git a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/lists.ts b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/lists.ts new file mode 100644 index 0000000000000..21d1105a313e7 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/lists.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as t from 'io-ts'; +import { operator } from '../../../../../lists/common/schemas'; + +export const translatedEntryMatchAny = t.exact( + t.type({ + field: t.string, + operator, + type: t.keyof({ + exact_cased_any: null, + exact_caseless_any: null, + }), + value: t.array(t.string), + }) +); +export type TranslatedEntryMatchAny = t.TypeOf; + +export const translatedEntryMatch = t.exact( + t.type({ + field: t.string, + operator, + type: t.keyof({ + exact_cased: null, + exact_caseless: null, + }), + value: t.string, + }) +); +export type TranslatedEntryMatch = t.TypeOf; + +export const translatedEntryNested = t.exact( + t.type({ + field: t.string, + type: t.keyof({ nested: null }), + entries: t.array(t.union([translatedEntryMatch, translatedEntryMatchAny])), + }) +); +export type TranslatedEntryNested = t.TypeOf; + +export const translatedEntry = t.union([ + translatedEntryNested, + translatedEntryMatch, + translatedEntryMatchAny, +]); +export type TranslatedEntry = t.TypeOf; + +export const translatedExceptionList = t.exact( + t.type({ + type: t.string, + entries: t.array(translatedEntry), + }) +); +export type TranslatedExceptionList = t.TypeOf; + +export const wrappedExceptionList = t.exact( + t.type({ + exceptions_list: t.array(translatedEntry), + }) +); +export type WrappedTranslatedExceptionList = t.TypeOf; diff --git a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/request/download_artifact_schema.ts b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/request/download_artifact_schema.ts new file mode 100644 index 0000000000000..7a194fdc7b5f4 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/request/download_artifact_schema.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as t from 'io-ts'; +import { identifier, sha256 } from '../../../../../common/endpoint/schema/common'; + +export const downloadArtifactRequestParamsSchema = t.exact( + t.type({ + identifier, + sha256, + }) +); + +export type DownloadArtifactRequestParamsSchema = t.TypeOf< + typeof downloadArtifactRequestParamsSchema +>; diff --git a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/request/index.ts b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/request/index.ts new file mode 100644 index 0000000000000..13e4165eb5f16 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/request/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './download_artifact_schema'; diff --git a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/response/download_artifact_schema.ts b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/response/download_artifact_schema.ts new file mode 100644 index 0000000000000..537f7707889e4 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/response/download_artifact_schema.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as t from 'io-ts'; +import { encoding } from '../common'; + +const body = t.string; +const headers = t.exact( + t.type({ + 'content-encoding': encoding, + 'content-disposition': t.string, + }) +); + +export const downloadArtifactResponseSchema = t.exact( + t.type({ + body, + headers, + }) +); + +export type DownloadArtifactResponseSchema = t.TypeOf; diff --git a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/response/index.ts b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/response/index.ts new file mode 100644 index 0000000000000..13e4165eb5f16 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/response/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './download_artifact_schema'; diff --git a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.mock.ts b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.mock.ts new file mode 100644 index 0000000000000..1a9cc55ca5725 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.mock.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ArtifactConstants, buildArtifact } from '../../lib/artifacts'; +import { getTranslatedExceptionListMock } from './lists.mock'; +import { InternalArtifactSchema, InternalManifestSchema } from './saved_objects'; + +export const getInternalArtifactMock = async ( + os: string, + schemaVersion: string +): Promise => { + return buildArtifact(getTranslatedExceptionListMock(), os, schemaVersion); +}; + +export const getInternalArtifactMockWithDiffs = async ( + os: string, + schemaVersion: string +): Promise => { + const mock = getTranslatedExceptionListMock(); + mock.exceptions_list.pop(); + return buildArtifact(mock, os, schemaVersion); +}; + +export const getInternalArtifactsMock = async ( + os: string, + schemaVersion: string +): Promise => { + // @ts-ignore + return ArtifactConstants.SUPPORTED_OPERATING_SYSTEMS.map(async () => { + await buildArtifact(getTranslatedExceptionListMock(), os, schemaVersion); + }); +}; + +export const getInternalManifestMock = (): InternalManifestSchema => ({ + created: Date.now(), + ids: [], +}); diff --git a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.ts b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.ts new file mode 100644 index 0000000000000..2e71ef98387f1 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/saved_objects.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as t from 'io-ts'; +import { identifier, sha256, size } from '../../../../common/endpoint/schema/common'; +import { body, created, encoding } from './common'; + +export const internalArtifactSchema = t.exact( + t.type({ + identifier, + sha256, + encoding, + created, + body, + size, + }) +); + +export type InternalArtifactSchema = t.TypeOf; + +export const internalManifestSchema = t.exact( + t.type({ + created, + ids: t.array(identifier), + }) +); + +export type InternalManifestSchema = t.TypeOf; diff --git a/x-pack/plugins/security_solution/server/endpoint/schemas/index.ts b/x-pack/plugins/security_solution/server/endpoint/schemas/index.ts new file mode 100644 index 0000000000000..a3b6e68e4ada2 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/schemas/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './artifacts'; diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.mock.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.mock.ts new file mode 100644 index 0000000000000..6392c59b2377c --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { savedObjectsClientMock } from 'src/core/server/mocks'; +import { SavedObjectsClientContract } from 'src/core/server'; +import { ArtifactClient } from './artifact_client'; + +export const getArtifactClientMock = ( + savedObjectsClient?: SavedObjectsClientContract +): ArtifactClient => { + if (savedObjectsClient !== undefined) { + return new ArtifactClient(savedObjectsClient); + } + return new ArtifactClient(savedObjectsClientMock.create()); +}; diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.test.ts new file mode 100644 index 0000000000000..08e29b5c6b82b --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { savedObjectsClientMock } from 'src/core/server/mocks'; +import { ArtifactConstants } from '../../lib/artifacts'; +import { getInternalArtifactMock } from '../../schemas/artifacts/saved_objects.mock'; +import { getArtifactClientMock } from './artifact_client.mock'; +import { ArtifactClient } from './artifact_client'; + +describe('artifact_client', () => { + describe('ArtifactClient sanity checks', () => { + test('can create ArtifactClient', () => { + const artifactClient = new ArtifactClient(savedObjectsClientMock.create()); + expect(artifactClient).toBeInstanceOf(ArtifactClient); + }); + + test('can get artifact', async () => { + const savedObjectsClient = savedObjectsClientMock.create(); + const artifactClient = getArtifactClientMock(savedObjectsClient); + await artifactClient.getArtifact('abcd'); + expect(savedObjectsClient.get).toHaveBeenCalled(); + }); + + test('can create artifact', async () => { + const savedObjectsClient = savedObjectsClientMock.create(); + const artifactClient = getArtifactClientMock(savedObjectsClient); + const artifact = await getInternalArtifactMock('linux', '1.0.0'); + await artifactClient.createArtifact(artifact); + expect(savedObjectsClient.create).toHaveBeenCalledWith( + ArtifactConstants.SAVED_OBJECT_TYPE, + artifact, + { id: artifactClient.getArtifactId(artifact) } + ); + }); + + test('can delete artifact', async () => { + const savedObjectsClient = savedObjectsClientMock.create(); + const artifactClient = getArtifactClientMock(savedObjectsClient); + await artifactClient.deleteArtifact('abcd'); + expect(savedObjectsClient.delete).toHaveBeenCalledWith( + ArtifactConstants.SAVED_OBJECT_TYPE, + 'abcd' + ); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.ts new file mode 100644 index 0000000000000..4a3dcaae1bd3d --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/artifact_client.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SavedObject, SavedObjectsClientContract } from 'src/core/server'; +import { ArtifactConstants } from '../../lib/artifacts'; +import { InternalArtifactSchema } from '../../schemas/artifacts'; + +export class ArtifactClient { + private savedObjectsClient: SavedObjectsClientContract; + + constructor(savedObjectsClient: SavedObjectsClientContract) { + this.savedObjectsClient = savedObjectsClient; + } + + public getArtifactId(artifact: InternalArtifactSchema) { + return `${artifact.identifier}-${artifact.sha256}`; + } + + public async getArtifact(id: string): Promise> { + return this.savedObjectsClient.get( + ArtifactConstants.SAVED_OBJECT_TYPE, + id + ); + } + + public async createArtifact( + artifact: InternalArtifactSchema + ): Promise> { + return this.savedObjectsClient.create( + ArtifactConstants.SAVED_OBJECT_TYPE, + artifact, + { id: this.getArtifactId(artifact) } + ); + } + + public async deleteArtifact(id: string) { + return this.savedObjectsClient.delete(ArtifactConstants.SAVED_OBJECT_TYPE, id); + } +} diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/index.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/index.ts new file mode 100644 index 0000000000000..44a4d7e77dbcb --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/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; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './artifact_client'; +export * from './manifest_manager'; diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_client.mock.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_client.mock.ts new file mode 100644 index 0000000000000..bfeacbcedf2cb --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_client.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SavedObjectsClientContract } from 'src/core/server'; +import { savedObjectsClientMock } from 'src/core/server/mocks'; +import { ManifestClient } from './manifest_client'; + +export const getManifestClientMock = ( + savedObjectsClient?: SavedObjectsClientContract +): ManifestClient => { + if (savedObjectsClient !== undefined) { + return new ManifestClient(savedObjectsClient, '1.0.0'); + } + return new ManifestClient(savedObjectsClientMock.create(), '1.0.0'); +}; diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_client.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_client.test.ts new file mode 100644 index 0000000000000..5780c6279ee6a --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_client.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { savedObjectsClientMock } from 'src/core/server/mocks'; +import { ManifestSchemaVersion } from '../../../../common/endpoint/schema/common'; +import { ManifestConstants } from '../../lib/artifacts'; +import { getInternalManifestMock } from '../../schemas/artifacts/saved_objects.mock'; +import { getManifestClientMock } from './manifest_client.mock'; +import { ManifestClient } from './manifest_client'; + +describe('manifest_client', () => { + describe('ManifestClient sanity checks', () => { + test('can create ManifestClient', () => { + const manifestClient = new ManifestClient(savedObjectsClientMock.create(), '1.0.0'); + expect(manifestClient).toBeInstanceOf(ManifestClient); + }); + + test('cannot create ManifestClient with invalid schema version', () => { + expect(() => { + new ManifestClient(savedObjectsClientMock.create(), 'invalid' as ManifestSchemaVersion); + }).toThrow(); + }); + + test('can get manifest', async () => { + const savedObjectsClient = savedObjectsClientMock.create(); + const manifestClient = getManifestClientMock(savedObjectsClient); + await manifestClient.getManifest(); + expect(savedObjectsClient.get).toHaveBeenCalled(); + }); + + test('can create manifest', async () => { + const savedObjectsClient = savedObjectsClientMock.create(); + const manifestClient = getManifestClientMock(savedObjectsClient); + const manifest = getInternalManifestMock(); + await manifestClient.createManifest(manifest); + expect(savedObjectsClient.create).toHaveBeenCalledWith( + ManifestConstants.SAVED_OBJECT_TYPE, + manifest, + { id: manifestClient.getManifestId() } + ); + }); + + test('can update manifest', async () => { + const savedObjectsClient = savedObjectsClientMock.create(); + const manifestClient = getManifestClientMock(savedObjectsClient); + const manifest = getInternalManifestMock(); + await manifestClient.updateManifest(manifest, { version: 'abcd' }); + expect(savedObjectsClient.update).toHaveBeenCalledWith( + ManifestConstants.SAVED_OBJECT_TYPE, + manifestClient.getManifestId(), + manifest, + { version: 'abcd' } + ); + }); + + test('can delete manifest', async () => { + const savedObjectsClient = savedObjectsClientMock.create(); + const manifestClient = getManifestClientMock(savedObjectsClient); + await manifestClient.deleteManifest(); + expect(savedObjectsClient.delete).toHaveBeenCalledWith( + ManifestConstants.SAVED_OBJECT_TYPE, + manifestClient.getManifestId() + ); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_client.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_client.ts new file mode 100644 index 0000000000000..45182841e56fc --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_client.ts @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + SavedObject, + SavedObjectsClientContract, + SavedObjectsUpdateResponse, +} from 'src/core/server'; +import { + manifestSchemaVersion, + ManifestSchemaVersion, +} from '../../../../common/endpoint/schema/common'; +import { validate } from '../../../../common/validate'; +import { ManifestConstants } from '../../lib/artifacts'; +import { InternalManifestSchema } from '../../schemas/artifacts'; + +interface UpdateManifestOpts { + version: string; +} + +export class ManifestClient { + private schemaVersion: ManifestSchemaVersion; + private savedObjectsClient: SavedObjectsClientContract; + + constructor( + savedObjectsClient: SavedObjectsClientContract, + schemaVersion: ManifestSchemaVersion + ) { + this.savedObjectsClient = savedObjectsClient; + + const [validated, errors] = validate( + (schemaVersion as unknown) as object, + manifestSchemaVersion + ); + + if (errors != null || validated === null) { + throw new Error(`Invalid manifest version: ${schemaVersion}`); + } + + this.schemaVersion = validated; + } + + public getManifestId(): string { + return `endpoint-manifest-${this.schemaVersion}`; + } + + public async getManifest(): Promise> { + return this.savedObjectsClient.get( + ManifestConstants.SAVED_OBJECT_TYPE, + this.getManifestId() + ); + } + + public async createManifest( + manifest: InternalManifestSchema + ): Promise> { + return this.savedObjectsClient.create( + ManifestConstants.SAVED_OBJECT_TYPE, + manifest, + { id: this.getManifestId() } + ); + } + + public async updateManifest( + manifest: InternalManifestSchema, + opts?: UpdateManifestOpts + ): Promise> { + return this.savedObjectsClient.update( + ManifestConstants.SAVED_OBJECT_TYPE, + this.getManifestId(), + manifest, + opts + ); + } + + public async deleteManifest() { + return this.savedObjectsClient.delete( + ManifestConstants.SAVED_OBJECT_TYPE, + this.getManifestId() + ); + } +} diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/index.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/index.ts new file mode 100644 index 0000000000000..03d5d27b3ff78 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './manifest_manager'; diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.ts new file mode 100644 index 0000000000000..cd70b11aef305 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.mock.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; + * you may not use this file except in compliance with the Elastic License. + */ + +// eslint-disable-next-line max-classes-per-file +import { savedObjectsClientMock, loggingSystemMock } from 'src/core/server/mocks'; +import { Logger } from 'src/core/server'; +import { getFoundExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/found_exception_list_item_schema.mock'; +import { listMock } from '../../../../../../lists/server/mocks'; +import { + ExceptionsCache, + Manifest, + buildArtifact, + getFullEndpointExceptionList, +} from '../../../lib/artifacts'; +import { InternalArtifactSchema } from '../../../schemas/artifacts'; +import { getArtifactClientMock } from '../artifact_client.mock'; +import { getManifestClientMock } from '../manifest_client.mock'; +import { ManifestManager } from './manifest_manager'; + +function getMockPackageConfig() { + return { + id: 'c6d16e42-c32d-4dce-8a88-113cfe276ad1', + inputs: [ + { + config: {}, + }, + ], + revision: 1, + version: 'abcd', // TODO: not yet implemented in ingest_manager (https://github.com/elastic/kibana/issues/69992) + updated_at: '2020-06-25T16:03:38.159292', + updated_by: 'kibana', + created_at: '2020-06-25T16:03:38.159292', + created_by: 'kibana', + }; +} + +class PackageConfigServiceMock { + public create = jest.fn().mockResolvedValue(getMockPackageConfig()); + public get = jest.fn().mockResolvedValue(getMockPackageConfig()); + public getByIds = jest.fn().mockResolvedValue([getMockPackageConfig()]); + public list = jest.fn().mockResolvedValue({ + items: [getMockPackageConfig()], + total: 1, + page: 1, + perPage: 20, + }); + public update = jest.fn().mockResolvedValue(getMockPackageConfig()); +} + +export function getPackageConfigServiceMock() { + return new PackageConfigServiceMock(); +} + +async function mockBuildExceptionListArtifacts( + os: string, + schemaVersion: string +): Promise { + const mockExceptionClient = listMock.getExceptionListClient(); + const first = getFoundExceptionListItemSchemaMock(); + mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); + const exceptions = await getFullEndpointExceptionList(mockExceptionClient, os, schemaVersion); + return [await buildArtifact(exceptions, os, schemaVersion)]; +} + +// @ts-ignore +export class ManifestManagerMock extends ManifestManager { + // @ts-ignore + private buildExceptionListArtifacts = async () => { + return mockBuildExceptionListArtifacts('linux', '1.0.0'); + }; + + // @ts-ignore + private getLastDispatchedManifest = jest + .fn() + .mockResolvedValue(new Manifest(new Date(), '1.0.0', 'v0')); + + // @ts-ignore + private getManifestClient = jest + .fn() + .mockReturnValue(getManifestClientMock(this.savedObjectsClient)); +} + +export const getManifestManagerMock = (opts?: { + packageConfigService?: PackageConfigServiceMock; + savedObjectsClient?: ReturnType; +}): ManifestManagerMock => { + let packageConfigService = getPackageConfigServiceMock(); + if (opts?.packageConfigService !== undefined) { + packageConfigService = opts.packageConfigService; + } + + let savedObjectsClient = savedObjectsClientMock.create(); + if (opts?.savedObjectsClient !== undefined) { + savedObjectsClient = opts.savedObjectsClient; + } + + const manifestManager = new ManifestManagerMock({ + artifactClient: getArtifactClientMock(savedObjectsClient), + cache: new ExceptionsCache(5), + // @ts-ignore + packageConfigService, + exceptionListClient: listMock.getExceptionListClient(), + logger: loggingSystemMock.create().get() as jest.Mocked, + savedObjectsClient, + }); + + return manifestManager; +}; diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts new file mode 100644 index 0000000000000..bbb6fdfd50810 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { savedObjectsClientMock } from 'src/core/server/mocks'; +import { ArtifactConstants, ManifestConstants, Manifest } from '../../../lib/artifacts'; +import { getPackageConfigServiceMock, getManifestManagerMock } from './manifest_manager.mock'; + +describe('manifest_manager', () => { + describe('ManifestManager sanity checks', () => { + test('ManifestManager can refresh manifest', async () => { + const manifestManager = getManifestManagerMock(); + const manifestWrapper = await manifestManager.refresh(); + expect(manifestWrapper!.diffs).toEqual([ + { + id: + 'endpoint-exceptionlist-linux-1.0.0-d34a1f6659bd86fc2023d7477aa2e5d2055c9c0fb0a0f10fae76bf8b94bebe49', + type: 'add', + }, + ]); + expect(manifestWrapper!.manifest).toBeInstanceOf(Manifest); + }); + + test('ManifestManager can dispatch manifest', async () => { + const packageConfigService = getPackageConfigServiceMock(); + const manifestManager = getManifestManagerMock({ packageConfigService }); + const manifestWrapperRefresh = await manifestManager.refresh(); + const manifestWrapperDispatch = await manifestManager.dispatch(manifestWrapperRefresh); + expect(manifestWrapperRefresh).toEqual(manifestWrapperDispatch); + const entries = manifestWrapperDispatch!.manifest.getEntries(); + const artifact = Object.values(entries)[0].getArtifact(); + expect( + packageConfigService.update.mock.calls[0][2].inputs[0].config.artifact_manifest.value + ).toEqual({ + manifest_version: 'v0', + schema_version: '1.0.0', + artifacts: { + [artifact.identifier]: { + sha256: artifact.sha256, + size: artifact.size, + url: `/api/endpoint/artifacts/download/${artifact.identifier}/${artifact.sha256}`, + }, + }, + }); + }); + + test('ManifestManager can commit manifest', async () => { + const savedObjectsClient: ReturnType = savedObjectsClientMock.create(); + const manifestManager = getManifestManagerMock({ + savedObjectsClient, + }); + + const manifestWrapperRefresh = await manifestManager.refresh(); + const manifestWrapperDispatch = await manifestManager.dispatch(manifestWrapperRefresh); + const diff = { + id: 'abcd', + type: 'delete', + }; + manifestWrapperDispatch!.diffs.push(diff); + + await manifestManager.commit(manifestWrapperDispatch); + + // created new artifact + expect(savedObjectsClient.create.mock.calls[0][0]).toEqual( + ArtifactConstants.SAVED_OBJECT_TYPE + ); + + // deleted old artifact + expect(savedObjectsClient.delete).toHaveBeenCalledWith( + ArtifactConstants.SAVED_OBJECT_TYPE, + 'abcd' + ); + + // committed new manifest + expect(savedObjectsClient.create.mock.calls[1][0]).toEqual( + ManifestConstants.SAVED_OBJECT_TYPE + ); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts new file mode 100644 index 0000000000000..33b0d5db575c6 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts @@ -0,0 +1,270 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Logger, SavedObjectsClientContract, SavedObject } from 'src/core/server'; +import { PackageConfigServiceInterface } from '../../../../../../ingest_manager/server'; +import { ExceptionListClient } from '../../../../../../lists/server'; +import { ManifestSchemaVersion } from '../../../../../common/endpoint/schema/common'; +import { + ArtifactConstants, + ManifestConstants, + Manifest, + buildArtifact, + getFullEndpointExceptionList, + ExceptionsCache, + ManifestDiff, +} from '../../../lib/artifacts'; +import { InternalArtifactSchema, InternalManifestSchema } from '../../../schemas/artifacts'; +import { ArtifactClient } from '../artifact_client'; +import { ManifestClient } from '../manifest_client'; + +export interface ManifestManagerContext { + savedObjectsClient: SavedObjectsClientContract; + artifactClient: ArtifactClient; + exceptionListClient: ExceptionListClient; + packageConfigService: PackageConfigServiceInterface; + logger: Logger; + cache: ExceptionsCache; +} + +export interface ManifestRefreshOpts { + initialize?: boolean; +} + +export interface WrappedManifest { + manifest: Manifest; + diffs: ManifestDiff[]; +} + +export class ManifestManager { + protected artifactClient: ArtifactClient; + protected exceptionListClient: ExceptionListClient; + protected packageConfigService: PackageConfigServiceInterface; + protected savedObjectsClient: SavedObjectsClientContract; + protected logger: Logger; + protected cache: ExceptionsCache; + + constructor(context: ManifestManagerContext) { + this.artifactClient = context.artifactClient; + this.exceptionListClient = context.exceptionListClient; + this.packageConfigService = context.packageConfigService; + this.savedObjectsClient = context.savedObjectsClient; + this.logger = context.logger; + this.cache = context.cache; + } + + private getManifestClient(schemaVersion: string): ManifestClient { + return new ManifestClient(this.savedObjectsClient, schemaVersion as ManifestSchemaVersion); + } + + private async buildExceptionListArtifacts( + schemaVersion: string + ): Promise { + const artifacts: InternalArtifactSchema[] = []; + + for (const os of ArtifactConstants.SUPPORTED_OPERATING_SYSTEMS) { + const exceptionList = await getFullEndpointExceptionList( + this.exceptionListClient, + os, + schemaVersion + ); + const artifact = await buildArtifact(exceptionList, os, schemaVersion); + + artifacts.push(artifact); + } + + return artifacts; + } + + private async getLastDispatchedManifest(schemaVersion: string): Promise { + return this.getManifestClient(schemaVersion) + .getManifest() + .then(async (manifestSo: SavedObject) => { + if (manifestSo.version === undefined) { + throw new Error('No version returned for manifest.'); + } + const manifest = new Manifest( + new Date(manifestSo.attributes.created), + schemaVersion, + manifestSo.version + ); + + for (const id of manifestSo.attributes.ids) { + const artifactSo = await this.artifactClient.getArtifact(id); + manifest.addEntry(artifactSo.attributes); + } + + return manifest; + }) + .catch((err) => { + if (err.output.statusCode !== 404) { + throw err; + } + return null; + }); + } + + public async refresh(opts?: ManifestRefreshOpts): Promise { + let oldManifest: Manifest | null; + + // Get the last-dispatched manifest + oldManifest = await this.getLastDispatchedManifest(ManifestConstants.SCHEMA_VERSION); + + if (oldManifest === null && opts !== undefined && opts.initialize) { + oldManifest = new Manifest(new Date(), ManifestConstants.SCHEMA_VERSION, 'v0'); // create empty manifest + } else if (oldManifest == null) { + this.logger.debug('Manifest does not exist yet. Waiting...'); + return null; + } + + // Build new exception list artifacts + const artifacts = await this.buildExceptionListArtifacts(ArtifactConstants.SCHEMA_VERSION); + + // Build new manifest + const newManifest = Manifest.fromArtifacts( + artifacts, + ManifestConstants.SCHEMA_VERSION, + oldManifest.getVersion() + ); + + // Get diffs + const diffs = newManifest.diff(oldManifest); + + // Create new artifacts + for (const diff of diffs) { + if (diff.type === 'add') { + const artifact = newManifest.getArtifact(diff.id); + try { + await this.artifactClient.createArtifact(artifact); + // Cache the body of the artifact + this.cache.set(diff.id, artifact.body); + } catch (err) { + if (err.status === 409) { + // This artifact already existed... + this.logger.debug(`Tried to create artifact ${diff.id}, but it already exists.`); + } else { + throw err; + } + } + } + } + + return { + manifest: newManifest, + diffs, + }; + } + + /** + * Dispatches the manifest by writing it to the endpoint packageConfig. + * + * @return {WrappedManifest | null} WrappedManifest if all dispatched, else null + */ + public async dispatch(wrappedManifest: WrappedManifest | null): Promise { + if (wrappedManifest === null) { + this.logger.debug('wrappedManifest was null, aborting dispatch'); + return null; + } + + function showDiffs(diffs: ManifestDiff[]) { + return diffs.map((diff) => { + const op = diff.type === 'add' ? '(+)' : '(-)'; + return `${op}${diff.id}`; + }); + } + + if (wrappedManifest.diffs.length > 0) { + this.logger.info(`Dispatching new manifest with diffs: ${showDiffs(wrappedManifest.diffs)}`); + + let paging = true; + let success = true; + + while (paging) { + const { items, total, page } = await this.packageConfigService.list( + this.savedObjectsClient, + { + page: 1, + perPage: 20, + kuery: 'ingest-package-configs.package.name:endpoint', + } + ); + + for (const packageConfig of items) { + const { id, revision, updated_at, updated_by, ...newPackageConfig } = packageConfig; + + if ( + newPackageConfig.inputs.length > 0 && + newPackageConfig.inputs[0].config !== undefined + ) { + const artifactManifest = newPackageConfig.inputs[0].config.artifact_manifest ?? { + value: {}, + }; + artifactManifest.value = wrappedManifest.manifest.toEndpointFormat(); + newPackageConfig.inputs[0].config.artifact_manifest = artifactManifest; + + await this.packageConfigService + .update(this.savedObjectsClient, id, newPackageConfig) + .then((response) => { + this.logger.debug(`Updated package config ${id}`); + }) + .catch((err) => { + success = false; + this.logger.debug(`Error updating package config ${id}`); + this.logger.error(err); + }); + } else { + success = false; + this.logger.debug(`Package config ${id} has no config.`); + } + } + + paging = page * items.length < total; + } + + return success ? wrappedManifest : null; + } else { + this.logger.debug('No manifest diffs [no-op]'); + } + + return null; + } + + public async commit(wrappedManifest: WrappedManifest | null) { + if (wrappedManifest === null) { + this.logger.debug('wrappedManifest was null, aborting commit'); + return; + } + + const manifestClient = this.getManifestClient(wrappedManifest.manifest.getSchemaVersion()); + + // Commit the new manifest + if (wrappedManifest.manifest.getVersion() === 'v0') { + await manifestClient.createManifest(wrappedManifest.manifest.toSavedObject()); + } else { + const version = wrappedManifest.manifest.getVersion(); + if (version === 'v0') { + throw new Error('Updating existing manifest with baseline version. Bad state.'); + } + await manifestClient.updateManifest(wrappedManifest.manifest.toSavedObject(), { + version, + }); + } + + this.logger.info(`Commited manifest ${wrappedManifest.manifest.getVersion()}`); + + // Clean up old artifacts + for (const diff of wrappedManifest.diffs) { + try { + if (diff.type === 'delete') { + await this.artifactClient.deleteArtifact(diff.id); + this.logger.info(`Cleaned up artifact ${diff.id}`); + } + } catch (err) { + this.logger.error(err); + } + } + } +} diff --git a/x-pack/plugins/security_solution/server/endpoint/services/index.ts b/x-pack/plugins/security_solution/server/endpoint/services/index.ts new file mode 100644 index 0000000000000..a3b6e68e4ada2 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/services/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './artifacts'; diff --git a/x-pack/plugins/security_solution/server/endpoint/types.ts b/x-pack/plugins/security_solution/server/endpoint/types.ts index fbcc5bc833d73..3c6630db8ebd8 100644 --- a/x-pack/plugins/security_solution/server/endpoint/types.ts +++ b/x-pack/plugins/security_solution/server/endpoint/types.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ import { LoggerFactory } from 'kibana/server'; -import { EndpointAppContextService } from './endpoint_app_context_services'; import { ConfigType } from '../config'; +import { EndpointAppContextService } from './endpoint_app_context_services'; /** * The context for Endpoint apps. diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 9bb1bea0949e0..a97f1eee56342 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -11,9 +11,10 @@ import { i18n } from '@kbn/i18n'; import { CoreSetup, CoreStart, + Logger, Plugin as IPlugin, PluginInitializerContext, - Logger, + SavedObjectsClient, } from '../../../../src/core/server'; import { PluginSetupContract as AlertingSetup } from '../../alerts/server'; import { SecurityPluginSetup as SecuritySetup } from '../../security/server'; @@ -24,6 +25,7 @@ import { EncryptedSavedObjectsPluginSetup as EncryptedSavedObjectsSetup } from ' import { SpacesPluginSetup as SpacesSetup } from '../../spaces/server'; import { LicensingPluginSetup } from '../../licensing/server'; import { IngestManagerStartContract } from '../../ingest_manager/server'; +import { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server'; import { initServer } from './init_server'; import { compose } from './lib/compose/kibana'; import { initRoutes } from './routes'; @@ -32,6 +34,7 @@ import { signalRulesAlertType } from './lib/detection_engine/signals/signal_rule import { rulesNotificationAlertType } from './lib/detection_engine/notifications/rules_notification_alert_type'; import { isNotificationAlertExecutor } from './lib/detection_engine/notifications/types'; import { hasListsFeature, listsEnvFeatureFlagName } from './lib/detection_engine/feature_flags'; +import { ManifestTask, ExceptionsCache } from './endpoint/lib/artifacts'; import { initSavedObjects, savedObjectTypes } from './saved_objects'; import { AppClientFactory } from './client'; import { createConfig$, ConfigType } from './config'; @@ -40,8 +43,10 @@ import { APP_ID, APP_ICON, SERVER_APP_ID } from '../common/constants'; import { registerEndpointRoutes } from './endpoint/routes/metadata'; import { registerResolverRoutes } from './endpoint/routes/resolver'; import { registerPolicyRoutes } from './endpoint/routes/policy'; +import { ArtifactClient, ManifestManager } from './endpoint/services'; import { EndpointAppContextService } from './endpoint/endpoint_app_context_services'; import { EndpointAppContext } from './endpoint/types'; +import { registerDownloadExceptionListRoute } from './endpoint/routes/artifacts'; export interface SetupPlugins { alerts: AlertingSetup; @@ -50,12 +55,14 @@ export interface SetupPlugins { licensing: LicensingPluginSetup; security?: SecuritySetup; spaces?: SpacesSetup; + taskManager: TaskManagerSetupContract; ml?: MlSetup; lists?: ListPluginSetup; } export interface StartPlugins { ingestManager: IngestManagerStartContract; + taskManager: TaskManagerStartContract; } // eslint-disable-next-line @typescript-eslint/no-empty-interface @@ -70,11 +77,17 @@ export class Plugin implements IPlugin type.name); diff --git a/x-pack/test/api_integration/apis/endpoint/artifacts/index.ts b/x-pack/test/api_integration/apis/endpoint/artifacts/index.ts new file mode 100644 index 0000000000000..2721592ba3350 --- /dev/null +++ b/x-pack/test/api_integration/apis/endpoint/artifacts/index.ts @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { getSupertestWithoutAuth, setupIngest } from '../../fleet/agents/services'; + +export default function (providerContext: FtrProviderContext) { + const { getService } = providerContext; + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const supertestWithoutAuth = getSupertestWithoutAuth(providerContext); + let agentAccessAPIKey: string; + + describe('artifact download', () => { + setupIngest(providerContext); + before(async () => { + await esArchiver.load('endpoint/artifacts/api_feature', { useCreate: true }); + + const { body: enrollmentApiKeysResponse } = await supertest + .get(`/api/ingest_manager/fleet/enrollment-api-keys`) + .expect(200); + expect(enrollmentApiKeysResponse.list).length(2); + + const { body: enrollmentApiKeyResponse } = await supertest + .get( + `/api/ingest_manager/fleet/enrollment-api-keys/${enrollmentApiKeysResponse.list[0].id}` + ) + .expect(200); + expect(enrollmentApiKeyResponse.item).to.have.key('api_key'); + const enrollmentAPIToken = enrollmentApiKeyResponse.item.api_key; + + // 2. Enroll agent + const { body: enrollmentResponse } = await supertestWithoutAuth + .post(`/api/ingest_manager/fleet/agents/enroll`) + .set('kbn-xsrf', 'xxx') + .set('Authorization', `ApiKey ${enrollmentAPIToken}`) + .send({ + type: 'PERMANENT', + metadata: { + local: { + elastic: { + agent: { + version: '7.0.0', + }, + }, + }, + user_provided: {}, + }, + }) + .expect(200); + expect(enrollmentResponse.success).to.eql(true); + + agentAccessAPIKey = enrollmentResponse.item.access_api_key; + }); + after(() => esArchiver.unload('endpoint/artifacts/api_feature')); + + it('should fail to find artifact with invalid hash', async () => { + await supertestWithoutAuth + .get('/api/endpoint/artifacts/download/endpoint-exceptionlist-windows-1.0.0/abcd') + .set('kbn-xsrf', 'xxx') + .set('authorization', `ApiKey ${agentAccessAPIKey}`) + .send() + .expect(404); + }); + + it('should download an artifact with correct hash', async () => { + await supertestWithoutAuth + .get( + '/api/endpoint/artifacts/download/endpoint-exceptionlist-linux-1.0.0/a4e4586e895fcb46dd25a25358b446f9a425279452afa3ef9a98bca39c39122d' + ) + .set('kbn-xsrf', 'xxx') + .set('authorization', `ApiKey ${agentAccessAPIKey}`) + .send() + .expect(200); + }); + + it('should fail on invalid api key', async () => { + await supertestWithoutAuth + .get( + '/api/endpoint/artifacts/download/endpoint-exceptionlist-macos-1.0.0/1825fb19fcc6dc391cae0bc4a2e96dd7f728a0c3ae9e1469251ada67f9e1b975' + ) + .set('kbn-xsrf', 'xxx') + .set('authorization', `ApiKey iNvAlId`) + .send() + .expect(401); + }); + }); +} diff --git a/x-pack/test/functional/es_archives/endpoint/artifacts/api_feature/data.json b/x-pack/test/functional/es_archives/endpoint/artifacts/api_feature/data.json new file mode 100644 index 0000000000000..a886b60e7e0dc --- /dev/null +++ b/x-pack/test/functional/es_archives/endpoint/artifacts/api_feature/data.json @@ -0,0 +1,179 @@ +{ + "type": "doc", + "value": { + "id": "endpoint:exceptions-artifact:endpoint-exceptionlist-linux-1.0.0-a4e4586e895fcb46dd25a25358b446f9a425279452afa3ef9a98bca39c39122d", + "index": ".kibana", + "source": { + "references": [ + ], + "endpoint:exceptions-artifact": { + "body": "eyJleGNlcHRpb25zX2xpc3QiOltdfQ==", + "created": 1593016187465, + "encoding": "application/json", + "identifier": "endpoint-exceptionlist-linux-1.0.0", + "sha256": "a4e4586e895fcb46dd25a25358b446f9a425279452afa3ef9a98bca39c39122d", + "size": 22 + }, + "type": "endpoint:exceptions-artifact", + "updated_at": "2020-06-24T16:29:47.584Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "endpoint:exceptions-manifest:endpoint-manifest-1.0.0", + "index": ".kibana", + "source": { + "references": [ + ], + "endpoint:exceptions-manifest": { + "created": 1593183699663, + "ids": [ + "endpoint-exceptionlist-linux-1.0.0-a4e4586e895fcb46dd25a25358b446f9a425279452afa3ef9a98bca39c39122d", + "endpoint-exceptionlist-macos-1.0.0-a4e4586e895fcb46dd25a25358b446f9a425279452afa3ef9a98bca39c39122d", + "endpoint-exceptionlist-windows-1.0.0-a4e4586e895fcb46dd25a25358b446f9a425279452afa3ef9a98bca39c39122d" + ] + }, + "type": "endpoint:exceptions-manifest", + "updated_at": "2020-06-26T15:01:39.704Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "exception-list-agnostic:13a7ef40-b63b-11ea-ace9-591c8e572c76", + "index": ".kibana", + "source": { + "exception-list-agnostic": { + "_tags": [ + "endpoint", + "process", + "malware", + "os:linux" + ], + "created_at": "2020-06-24T16:52:23.689Z", + "created_by": "akahan", + "description": "This is a sample agnostic endpoint type exception", + "list_id": "endpoint_list", + "list_type": "list", + "name": "Sample Endpoint Exception List", + "tags": [ + "user added string for a tag", + "malware" + ], + "tie_breaker_id": "e3b20e6e-c023-4575-a033-47990115969c", + "type": "endpoint", + "updated_by": "akahan" + }, + "references": [ + ], + "type": "exception-list-agnostic", + "updated_at": "2020-06-24T16:52:23.732Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "exception-list-agnostic:679b95a0-b714-11ea-a4c9-0963ae39bc3d", + "index": ".kibana", + "source": { + "exception-list-agnostic": { + "_tags": [ + "os:windows" + ], + "comments": [ + ], + "created_at": "2020-06-25T18:48:05.326Z", + "created_by": "akahan", + "description": "This is a sample endpoint type exception", + "entries": [ + { + "field": "actingProcess.file.signer", + "operator": "included", + "type": "match", + "value": "Elastic, N.V." + }, + { + "field": "event.category", + "operator": "included", + "type": "match_any", + "value": [ + "process", + "malware" + ] + } + ], + "item_id": "61142b8f-5876-4709-9952-95160cd58f2f", + "list_id": "endpoint_list", + "list_type": "item", + "name": "Sample Endpoint Exception List", + "tags": [ + "user added string for a tag", + "malware" + ], + "tie_breaker_id": "b36176d2-bc75-4641-a8e3-e811c6bc30d8", + "type": "endpoint", + "updated_by": "akahan" + }, + "references": [ + ], + "type": "exception-list-agnostic", + "updated_at": "2020-06-25T18:48:05.369Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "fleet-agents:a34d87c1-726e-4c30-b2ff-1b4b95f59d2a", + "index": ".kibana", + "source": { + "fleet-agents": { + "access_api_key_id": "8ZnT7HIBwLFvkUEPQaT3", + "active": true, + "config_id": "2dd2a110-b6f6-11ea-a66d-63cf082a3b58", + "enrolled_at": "2020-06-25T18:52:47.290Z", + "local_metadata": { + "os": "macos" + }, + "type": "PERMANENT", + "user_provided_metadata": { + "region": "us-east" + } + }, + "references": [ + ], + "type": "fleet-agents", + "updated_at": "2020-06-25T18:52:48.464Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "fleet-enrollment-api-keys:8178eb66-392f-4b76-9dc9-704ed1a5c56e", + "index": ".kibana", + "source": { + "fleet-enrollment-api-keys": { + "active": true, + "api_key": "8ZnT7HIBwLFvkUEPQaT3", + "api_key_id": "8ZnT7HIBwLFvkUEPQaT3", + "config_id": "2dd2a110-b6f6-11ea-a66d-63cf082a3b58", + "created_at": "2020-06-25T17:25:30.065Z", + "name": "Default (93aa98c8-d650-422e-aa7b-663dae3dff83)" + }, + "references": [ + ], + "type": "fleet-enrollment-api-keys", + "updated_at": "2020-06-25T17:25:30.114Z" + } + } +} \ No newline at end of file From 257c115f6694e38cffd933269dfd6e2f151cbc1b Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Thu, 2 Jul 2020 07:49:01 +0200 Subject: [PATCH 21/34] [SIEM] Reenabling Cypress tests (#70397) * reenabling cypress * skips Overview tests * skips search bar test * skips URL test --- test/scripts/jenkins_security_solution_cypress.sh | 15 +++++---------- .../cypress/integration/overview.spec.ts | 2 +- .../cypress/integration/search_bar.spec.ts | 2 +- .../cypress/integration/url_state.spec.ts | 2 +- 4 files changed, 8 insertions(+), 13 deletions(-) diff --git a/test/scripts/jenkins_security_solution_cypress.sh b/test/scripts/jenkins_security_solution_cypress.sh index 8aa3425be0beb..204911a3eedaa 100644 --- a/test/scripts/jenkins_security_solution_cypress.sh +++ b/test/scripts/jenkins_security_solution_cypress.sh @@ -11,16 +11,11 @@ export KIBANA_INSTALL_DIR="$destDir" echo " -> Running security solution cypress tests" cd "$XPACK_DIR" -# Failures across multiple suites, skipping all -# https://github.com/elastic/kibana/issues/69847 -# https://github.com/elastic/kibana/issues/69848 -# https://github.com/elastic/kibana/issues/69849 - -# checks-reporter-with-killswitch "Security solution Cypress Tests" \ -# node scripts/functional_tests \ -# --debug --bail \ -# --kibana-install-dir "$KIBANA_INSTALL_DIR" \ -# --config test/security_solution_cypress/config.ts +checks-reporter-with-killswitch "Security solution Cypress Tests" \ + node scripts/functional_tests \ + --debug --bail \ + --kibana-install-dir "$KIBANA_INSTALL_DIR" \ + --config test/security_solution_cypress/config.ts echo "" echo "" diff --git a/x-pack/plugins/security_solution/cypress/integration/overview.spec.ts b/x-pack/plugins/security_solution/cypress/integration/overview.spec.ts index b799d487acd08..6fb3840d89764 100644 --- a/x-pack/plugins/security_solution/cypress/integration/overview.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/overview.spec.ts @@ -11,7 +11,7 @@ import { loginAndWaitForPage } from '../tasks/login'; import { OVERVIEW_URL } from '../urls/navigation'; -describe('Overview Page', () => { +describe.skip('Overview Page', () => { before(() => { cy.stubSecurityApi('overview'); loginAndWaitForPage(OVERVIEW_URL); diff --git a/x-pack/plugins/security_solution/cypress/integration/search_bar.spec.ts b/x-pack/plugins/security_solution/cypress/integration/search_bar.spec.ts index ce053d1ac7616..9104f494e3e6b 100644 --- a/x-pack/plugins/security_solution/cypress/integration/search_bar.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/search_bar.spec.ts @@ -12,7 +12,7 @@ import { hostIpFilter } from '../objects/filter'; import { HOSTS_URL } from '../urls/navigation'; import { waitForAllHostsToBeLoaded } from '../tasks/hosts/all_hosts'; -describe('SearchBar', () => { +describe.skip('SearchBar', () => { before(() => { loginAndWaitForPage(HOSTS_URL); waitForAllHostsToBeLoaded(); diff --git a/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts b/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts index 6c456c2f5e100..a3a927cbea7d4 100644 --- a/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts @@ -234,7 +234,7 @@ describe('url state', () => { cy.get(KQL_INPUT).should('have.attr', 'value', 'source.ip: "10.142.0.9"'); }); - it('sets and reads the url state for timeline by id', () => { + it.skip('sets and reads the url state for timeline by id', () => { loginAndWaitForPage(HOSTS_URL); openTimeline(); executeTimelineKQL('host.name: *'); From eca4cc5d3e2b02342977e839844a426cd2ea1b82 Mon Sep 17 00:00:00 2001 From: Spencer Date: Wed, 1 Jul 2020 23:01:21 -0700 Subject: [PATCH 22/34] Skip failing endgame tests (#70548) Co-authored-by: spalger --- x-pack/test/endpoint_api_integration_no_ingest/apis/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/endpoint_api_integration_no_ingest/apis/index.ts b/x-pack/test/endpoint_api_integration_no_ingest/apis/index.ts index ff42e322f6857..cafb1c2a2ea69 100644 --- a/x-pack/test/endpoint_api_integration_no_ingest/apis/index.ts +++ b/x-pack/test/endpoint_api_integration_no_ingest/apis/index.ts @@ -7,7 +7,8 @@ import { FtrProviderContext } from '../ftr_provider_context'; export default function endpointAPIIntegrationTests({ loadTestFile }: FtrProviderContext) { - describe('Endpoint plugin', function () { + // Failing ES snapshot promotion: https://github.com/elastic/kibana/issues/70535 + describe.skip('Endpoint plugin', function () { this.tags('ciGroup7'); loadTestFile(require.resolve('./metadata')); }); From 8fe5d154c177f140d3ad703d135c2c5e621600bb Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Thu, 2 Jul 2020 08:05:08 +0200 Subject: [PATCH 23/34] [Lens] fix dimension label performance issues (#69978) --- .../dimension_panel/dimension_panel.test.tsx | 8 +++++ .../dimension_panel/popover_editor.tsx | 35 +++++++++++++++---- 2 files changed, 37 insertions(+), 6 deletions(-) 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 a1c084f83e447..e4dbc64184528 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 @@ -27,6 +27,14 @@ import { OperationMetadata } from '../../types'; jest.mock('../loader'); jest.mock('../state_helpers'); +jest.mock('lodash', () => { + const original = jest.requireActual('lodash'); + + return { + ...original, + debounce: (fn: unknown) => fn, + }; +}); const expectedIndexPatterns = { 1: { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx index eb2475756417e..34a4384ec0d40 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx @@ -6,7 +6,7 @@ import './popover_editor.scss'; import _ from 'lodash'; -import React, { useState, useMemo } from 'react'; +import React, { useState, useMemo, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFlexItem, @@ -56,6 +56,31 @@ function asOperationOptions(operationTypes: OperationType[], compatibleWithCurre })); } +const LabelInput = ({ value, onChange }: { value: string; onChange: (value: string) => void }) => { + const [inputValue, setInputValue] = useState(value); + + useEffect(() => { + setInputValue(value); + }, [value, setInputValue]); + + const onChangeDebounced = useMemo(() => _.debounce(onChange, 256), [onChange]); + + const handleInputChange = (e: React.ChangeEvent) => { + const val = String(e.target.value); + setInputValue(val); + onChangeDebounced(val); + }; + + return ( + + ); +}; + export function PopoverEditor(props: PopoverEditorProps) { const { selectedColumn, @@ -320,11 +345,9 @@ export function PopoverEditor(props: PopoverEditorProps) { })} display="rowCompressed" > - { + onChange={(value) => { setState({ ...state, layers: { @@ -335,7 +358,7 @@ export function PopoverEditor(props: PopoverEditorProps) { ...state.layers[layerId].columns, [columnId]: { ...selectedColumn, - label: e.target.value, + label: value, customLabel: true, }, }, From 45f0322fbcabf47883f7e571a41bd3846dfb82a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Thu, 2 Jul 2020 08:08:01 +0100 Subject: [PATCH 24/34] Reduce SavedObjects mappings for Application Usage (#70475) --- .../application_usage/saved_objects_types.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/saved_objects_types.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/saved_objects_types.ts index a0de79da565e6..8d6a2d110efe0 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/saved_objects_types.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/saved_objects_types.ts @@ -35,10 +35,12 @@ export function registerMappings(registerType: SavedObjectsServiceSetup['registe hidden: false, namespaceType: 'agnostic', mappings: { + dynamic: false, properties: { - appId: { type: 'keyword' }, - numberOfClicks: { type: 'long' }, - minutesOnScreen: { type: 'float' }, + // Disabled the mapping of these fields since they are not searched and we need to reduce the amount of indexed fields (#43673) + // appId: { type: 'keyword' }, + // numberOfClicks: { type: 'long' }, + // minutesOnScreen: { type: 'float' }, }, }, }); @@ -48,11 +50,13 @@ export function registerMappings(registerType: SavedObjectsServiceSetup['registe hidden: false, namespaceType: 'agnostic', mappings: { + dynamic: false, properties: { timestamp: { type: 'date' }, - appId: { type: 'keyword' }, - numberOfClicks: { type: 'long' }, - minutesOnScreen: { type: 'float' }, + // Disabled the mapping of these fields since they are not searched and we need to reduce the amount of indexed fields (#43673) + // appId: { type: 'keyword' }, + // numberOfClicks: { type: 'long' }, + // minutesOnScreen: { type: 'float' }, }, }, }); From 6607bf7b49c710b0870367ceef1444c24b2c0d6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Thu, 2 Jul 2020 08:08:35 +0100 Subject: [PATCH 25/34] [Telemetry] Report data shippers (#64935) Co-authored-by: Christiane (Tina) Heiligers Co-authored-by: Elastic Machine --- src/plugins/telemetry/server/index.ts | 4 + .../__tests__/get_local_stats.js | 19 +- .../get_data_telemetry/constants.ts | 136 ++++++++++ .../get_data_telemetry.test.ts | 251 +++++++++++++++++ .../get_data_telemetry/get_data_telemetry.ts | 253 +++++++++++++++++ .../get_data_telemetry/index.ts | 27 ++ .../telemetry_collection/get_local_stats.ts | 7 +- .../server/telemetry_collection/index.ts | 6 + .../apis/telemetry/telemetry_local.js | 20 ++ .../lib/elasticsearch/indices/get_indices.js | 40 ++- .../server/lib/elasticsearch/indices/index.js | 2 +- .../get_stats_with_xpack.test.ts.snap | 254 +++++++++--------- .../get_stats_with_xpack.test.ts | 12 +- .../apis/telemetry/fixtures/basiccluster.json | 8 +- .../apis/telemetry/fixtures/multicluster.json | 72 ++--- 15 files changed, 903 insertions(+), 208 deletions(-) create mode 100644 src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/constants.ts create mode 100644 src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.test.ts create mode 100644 src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.ts create mode 100644 src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/index.ts diff --git a/src/plugins/telemetry/server/index.ts b/src/plugins/telemetry/server/index.ts index d048c8f5e9427..42259d2e5187c 100644 --- a/src/plugins/telemetry/server/index.ts +++ b/src/plugins/telemetry/server/index.ts @@ -47,4 +47,8 @@ export { getLocalLicense, getLocalStats, TelemetryLocalStats, + DATA_TELEMETRY_ID, + DataTelemetryIndex, + DataTelemetryPayload, + buildDataTelemetryPayload, } from './telemetry_collection'; diff --git a/src/plugins/telemetry/server/telemetry_collection/__tests__/get_local_stats.js b/src/plugins/telemetry/server/telemetry_collection/__tests__/get_local_stats.js index e78b92498e6e7..8541745faea3b 100644 --- a/src/plugins/telemetry/server/telemetry_collection/__tests__/get_local_stats.js +++ b/src/plugins/telemetry/server/telemetry_collection/__tests__/get_local_stats.js @@ -179,23 +179,36 @@ describe('get_local_stats', () => { describe('handleLocalStats', () => { it('returns expected object without xpack and kibana data', () => { - const result = handleLocalStats(clusterInfo, clusterStatsWithNodesUsage, void 0, context); + const result = handleLocalStats( + clusterInfo, + clusterStatsWithNodesUsage, + void 0, + void 0, + context + ); expect(result.cluster_uuid).to.eql(combinedStatsResult.cluster_uuid); expect(result.cluster_name).to.eql(combinedStatsResult.cluster_name); expect(result.cluster_stats).to.eql(combinedStatsResult.cluster_stats); expect(result.version).to.be('2.3.4'); expect(result.collection).to.be('local'); expect(result.license).to.be(undefined); - expect(result.stack_stats).to.eql({ kibana: undefined }); + expect(result.stack_stats).to.eql({ kibana: undefined, data: undefined }); }); it('returns expected object with xpack', () => { - const result = handleLocalStats(clusterInfo, clusterStatsWithNodesUsage, void 0, context); + const result = handleLocalStats( + clusterInfo, + clusterStatsWithNodesUsage, + void 0, + void 0, + context + ); const { stack_stats: stack, ...cluster } = result; expect(cluster.collection).to.be(combinedStatsResult.collection); expect(cluster.cluster_uuid).to.be(combinedStatsResult.cluster_uuid); expect(cluster.cluster_name).to.be(combinedStatsResult.cluster_name); expect(stack.kibana).to.be(undefined); // not mocked for this test + expect(stack.data).to.be(undefined); // not mocked for this test expect(cluster.version).to.eql(combinedStatsResult.version); expect(cluster.cluster_stats).to.eql(combinedStatsResult.cluster_stats); diff --git a/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/constants.ts b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/constants.ts new file mode 100644 index 0000000000000..2d0864b1cb75f --- /dev/null +++ b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/constants.ts @@ -0,0 +1,136 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const DATA_TELEMETRY_ID = 'data'; + +export const DATA_KNOWN_TYPES = ['logs', 'traces', 'metrics'] as const; + +export type DataTelemetryType = typeof DATA_KNOWN_TYPES[number]; + +export type DataPatternName = typeof DATA_DATASETS_INDEX_PATTERNS[number]['patternName']; + +// TODO: Ideally this list should be updated from an external public URL (similar to the newsfeed) +// But it's good to have a minimum list shipped with the build. +export const DATA_DATASETS_INDEX_PATTERNS = [ + // Enterprise Search - Elastic + { pattern: '.ent-search-*', patternName: 'enterprise-search' }, + { pattern: '.app-search-*', patternName: 'app-search' }, + // Enterprise Search - 3rd party + { pattern: '*magento2*', patternName: 'magento2' }, + { pattern: '*magento*', patternName: 'magento' }, + { pattern: '*shopify*', patternName: 'shopify' }, + { pattern: '*wordpress*', patternName: 'wordpress' }, + // { pattern: '*wp*', patternName: 'wordpress' }, // TODO: Too vague? + { pattern: '*drupal*', patternName: 'drupal' }, + { pattern: '*joomla*', patternName: 'joomla' }, + { pattern: '*search*', patternName: 'search' }, // TODO: Too vague? + // { pattern: '*wix*', patternName: 'wix' }, // TODO: Too vague? + { pattern: '*sharepoint*', patternName: 'sharepoint' }, + { pattern: '*squarespace*', patternName: 'squarespace' }, + // { pattern: '*aem*', patternName: 'aem' }, // TODO: Too vague? + { pattern: '*sitecore*', patternName: 'sitecore' }, + { pattern: '*weebly*', patternName: 'weebly' }, + { pattern: '*acquia*', patternName: 'acquia' }, + + // Observability - Elastic + { pattern: 'filebeat-*', patternName: 'filebeat', shipper: 'filebeat' }, + { pattern: 'metricbeat-*', patternName: 'metricbeat', shipper: 'metricbeat' }, + { pattern: 'apm-*', patternName: 'apm', shipper: 'apm' }, + { pattern: 'functionbeat-*', patternName: 'functionbeat', shipper: 'functionbeat' }, + { pattern: 'heartbeat-*', patternName: 'heartbeat', shipper: 'heartbeat' }, + { pattern: 'logstash-*', patternName: 'logstash', shipper: 'logstash' }, + // Observability - 3rd party + { pattern: 'fluentd*', patternName: 'fluentd' }, + { pattern: 'telegraf*', patternName: 'telegraf' }, + { pattern: 'prometheusbeat*', patternName: 'prometheusbeat' }, + { pattern: 'fluentbit*', patternName: 'fluentbit' }, + { pattern: '*nginx*', patternName: 'nginx' }, + { pattern: '*apache*', patternName: 'apache' }, // Already in Security (keeping it in here for documentation) + // { pattern: '*logs*', patternName: 'third-party-logs' }, Disabled for now + + // Security - Elastic + { pattern: 'logstash-*', patternName: 'logstash', shipper: 'logstash' }, + { pattern: 'endgame-*', patternName: 'endgame', shipper: 'endgame' }, + { pattern: 'logs-endpoint.*', patternName: 'logs-endpoint', shipper: 'endpoint' }, // It should be caught by the `mappings` logic, but just in case + { pattern: 'metrics-endpoint.*', patternName: 'metrics-endpoint', shipper: 'endpoint' }, // It should be caught by the `mappings` logic, but just in case + { pattern: '.siem-signals-*', patternName: 'siem-signals' }, + { pattern: 'auditbeat-*', patternName: 'auditbeat', shipper: 'auditbeat' }, + { pattern: 'winlogbeat-*', patternName: 'winlogbeat', shipper: 'winlogbeat' }, + { pattern: 'packetbeat-*', patternName: 'packetbeat', shipper: 'packetbeat' }, + { pattern: 'filebeat-*', patternName: 'filebeat', shipper: 'filebeat' }, + // Security - 3rd party + { pattern: '*apache*', patternName: 'apache' }, // Already in Observability (keeping it in here for documentation) + { pattern: '*tomcat*', patternName: 'tomcat' }, + { pattern: '*artifactory*', patternName: 'artifactory' }, + { pattern: '*aruba*', patternName: 'aruba' }, + { pattern: '*barracuda*', patternName: 'barracuda' }, + { pattern: '*bluecoat*', patternName: 'bluecoat' }, + { pattern: 'arcsight-*', patternName: 'arcsight', shipper: 'arcsight' }, + // { pattern: '*cef*', patternName: 'cef' }, // Disabled because it's too vague + { pattern: '*checkpoint*', patternName: 'checkpoint' }, + { pattern: '*cisco*', patternName: 'cisco' }, + { pattern: '*citrix*', patternName: 'citrix' }, + { pattern: '*cyberark*', patternName: 'cyberark' }, + { pattern: '*cylance*', patternName: 'cylance' }, + { pattern: '*fireeye*', patternName: 'fireeye' }, + { pattern: '*fortinet*', patternName: 'fortinet' }, + { pattern: '*infoblox*', patternName: 'infoblox' }, + { pattern: '*kaspersky*', patternName: 'kaspersky' }, + { pattern: '*mcafee*', patternName: 'mcafee' }, + // paloaltonetworks + { pattern: '*paloaltonetworks*', patternName: 'paloaltonetworks' }, + { pattern: 'pan-*', patternName: 'paloaltonetworks' }, + { pattern: 'pan_*', patternName: 'paloaltonetworks' }, + { pattern: 'pan.*', patternName: 'paloaltonetworks' }, + + // rsa + { pattern: 'rsa.*', patternName: 'rsa' }, + { pattern: 'rsa-*', patternName: 'rsa' }, + { pattern: 'rsa_*', patternName: 'rsa' }, + + // snort + { pattern: 'snort-*', patternName: 'snort' }, + { pattern: 'logstash-snort*', patternName: 'snort' }, + + { pattern: '*sonicwall*', patternName: 'sonicwall' }, + { pattern: '*sophos*', patternName: 'sophos' }, + + // squid + { pattern: 'squid-*', patternName: 'squid' }, + { pattern: 'squid_*', patternName: 'squid' }, + { pattern: 'squid.*', patternName: 'squid' }, + + { pattern: '*symantec*', patternName: 'symantec' }, + { pattern: '*tippingpoint*', patternName: 'tippingpoint' }, + { pattern: '*trendmicro*', patternName: 'trendmicro' }, + { pattern: '*tripwire*', patternName: 'tripwire' }, + { pattern: '*zscaler*', patternName: 'zscaler' }, + { pattern: '*zeek*', patternName: 'zeek' }, + { pattern: '*sigma_doc*', patternName: 'sigma_doc' }, + // { pattern: '*bro*', patternName: 'bro' }, // Disabled because it's too vague + { pattern: 'ecs-corelight*', patternName: 'ecs-corelight' }, + { pattern: '*suricata*', patternName: 'suricata' }, + // { pattern: '*fsf*', patternName: 'fsf' }, // Disabled because it's too vague + { pattern: '*wazuh*', patternName: 'wazuh' }, +] as const; + +// Get the unique list of index patterns (some are duplicated for documentation purposes) +export const DATA_DATASETS_INDEX_PATTERNS_UNIQUE = DATA_DATASETS_INDEX_PATTERNS.filter( + (entry, index, array) => !array.slice(0, index).find(({ pattern }) => entry.pattern === pattern) +); diff --git a/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.test.ts b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.test.ts new file mode 100644 index 0000000000000..8bffc5d012a74 --- /dev/null +++ b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.test.ts @@ -0,0 +1,251 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { buildDataTelemetryPayload, getDataTelemetry } from './get_data_telemetry'; +import { DATA_DATASETS_INDEX_PATTERNS, DATA_DATASETS_INDEX_PATTERNS_UNIQUE } from './constants'; + +describe('get_data_telemetry', () => { + describe('DATA_DATASETS_INDEX_PATTERNS', () => { + DATA_DATASETS_INDEX_PATTERNS.forEach((entry, index, array) => { + describe(`Pattern ${entry.pattern}`, () => { + test('there should only be one in DATA_DATASETS_INDEX_PATTERNS_UNIQUE', () => { + expect( + DATA_DATASETS_INDEX_PATTERNS_UNIQUE.filter(({ pattern }) => pattern === entry.pattern) + ).toHaveLength(1); + }); + + // This test is to make us sure that we don't update one of the duplicated entries and forget about any other repeated ones + test('when a document is duplicated, the duplicates should be identical', () => { + array.slice(0, index).forEach((previousEntry) => { + if (entry.pattern === previousEntry.pattern) { + expect(entry).toStrictEqual(previousEntry); + } + }); + }); + }); + }); + }); + + describe('buildDataTelemetryPayload', () => { + test('return the base object when no indices provided', () => { + expect(buildDataTelemetryPayload([])).toStrictEqual([]); + }); + + test('return the base object when no matching indices provided', () => { + expect( + buildDataTelemetryPayload([ + { name: 'no__way__this__can_match_anything', sizeInBytes: 10 }, + { name: '.kibana-event-log-8.0.0' }, + ]) + ).toStrictEqual([]); + }); + + test('matches some indices and puts them in their own category', () => { + expect( + buildDataTelemetryPayload([ + // APM Indices have known shipper (so we can infer the datasetType from mapping constant) + { name: 'apm-7.7.0-error-000001', shipper: 'apm', isECS: true }, + { name: 'apm-7.7.0-metric-000001', shipper: 'apm', isECS: true }, + { name: 'apm-7.7.0-onboarding-2020.05.17', shipper: 'apm', isECS: true }, + { name: 'apm-7.7.0-profile-000001', shipper: 'apm', isECS: true }, + { name: 'apm-7.7.0-span-000001', shipper: 'apm', isECS: true }, + { name: 'apm-7.7.0-transaction-000001', shipper: 'apm', isECS: true }, + // Packetbeat indices with known shipper (we can infer datasetType from mapping constant) + { name: 'packetbeat-7.7.0-2020.06.11-000001', shipper: 'packetbeat', isECS: true }, + // Matching patterns from the list => known datasetName but the rest is unknown + { name: 'filebeat-12314', docCount: 100, sizeInBytes: 10 }, + { name: 'metricbeat-1234', docCount: 100, sizeInBytes: 10, isECS: false }, + { name: '.app-search-1234', docCount: 0 }, + { name: 'logs-endpoint.1234', docCount: 0 }, // Matching pattern with a dot in the name + // New Indexing strategy: everything can be inferred from the constant_keyword values + { + name: 'logs-nginx.access-default-000001', + datasetName: 'nginx.access', + datasetType: 'logs', + shipper: 'filebeat', + isECS: true, + docCount: 1000, + sizeInBytes: 1000, + }, + { + name: 'logs-nginx.access-default-000002', + datasetName: 'nginx.access', + datasetType: 'logs', + shipper: 'filebeat', + isECS: true, + docCount: 1000, + sizeInBytes: 60, + }, + ]) + ).toStrictEqual([ + { + shipper: 'apm', + index_count: 6, + ecs_index_count: 6, + }, + { + shipper: 'packetbeat', + index_count: 1, + ecs_index_count: 1, + }, + { + pattern_name: 'filebeat', + shipper: 'filebeat', + index_count: 1, + doc_count: 100, + size_in_bytes: 10, + }, + { + pattern_name: 'metricbeat', + shipper: 'metricbeat', + index_count: 1, + ecs_index_count: 0, + doc_count: 100, + size_in_bytes: 10, + }, + { + pattern_name: 'app-search', + index_count: 1, + doc_count: 0, + }, + { + pattern_name: 'logs-endpoint', + shipper: 'endpoint', + index_count: 1, + doc_count: 0, + }, + { + dataset: { name: 'nginx.access', type: 'logs' }, + shipper: 'filebeat', + index_count: 2, + ecs_index_count: 2, + doc_count: 2000, + size_in_bytes: 1060, + }, + ]); + }); + }); + + describe('getDataTelemetry', () => { + test('it returns the base payload (all 0s) because no indices are found', async () => { + const callCluster = mockCallCluster(); + await expect(getDataTelemetry(callCluster)).resolves.toStrictEqual([]); + }); + + test('can only see the index mappings, but not the stats', async () => { + const callCluster = mockCallCluster(['filebeat-12314']); + await expect(getDataTelemetry(callCluster)).resolves.toStrictEqual([ + { + pattern_name: 'filebeat', + shipper: 'filebeat', + index_count: 1, + ecs_index_count: 0, + }, + ]); + }); + + test('can see the mappings and the stats', async () => { + const callCluster = mockCallCluster( + ['filebeat-12314'], + { isECS: true }, + { + indices: { + 'filebeat-12314': { total: { docs: { count: 100 }, store: { size_in_bytes: 10 } } }, + }, + } + ); + await expect(getDataTelemetry(callCluster)).resolves.toStrictEqual([ + { + pattern_name: 'filebeat', + shipper: 'filebeat', + index_count: 1, + ecs_index_count: 1, + doc_count: 100, + size_in_bytes: 10, + }, + ]); + }); + + test('find an index that does not match any index pattern but has mappings metadata', async () => { + const callCluster = mockCallCluster( + ['cannot_match_anything'], + { isECS: true, datasetType: 'traces', shipper: 'my-beat' }, + { + indices: { + cannot_match_anything: { + total: { docs: { count: 100 }, store: { size_in_bytes: 10 } }, + }, + }, + } + ); + await expect(getDataTelemetry(callCluster)).resolves.toStrictEqual([ + { + dataset: { name: undefined, type: 'traces' }, + shipper: 'my-beat', + index_count: 1, + ecs_index_count: 1, + doc_count: 100, + size_in_bytes: 10, + }, + ]); + }); + + test('return empty array when there is an error', async () => { + const callCluster = jest.fn().mockRejectedValue(new Error('Something went terribly wrong')); + await expect(getDataTelemetry(callCluster)).resolves.toStrictEqual([]); + }); + }); +}); + +function mockCallCluster( + indicesMappings: string[] = [], + { isECS = false, datasetName = '', datasetType = '', shipper = '' } = {}, + indexStats: any = {} +) { + return jest.fn().mockImplementation(async (method: string, opts: any) => { + if (method === 'indices.getMapping') { + return Object.fromEntries( + indicesMappings.map((index) => [ + index, + { + mappings: { + ...(shipper && { _meta: { beat: shipper } }), + properties: { + ...(isECS && { ecs: { properties: { version: { type: 'keyword' } } } }), + ...((datasetType || datasetName) && { + dataset: { + properties: { + ...(datasetName && { + name: { type: 'constant_keyword', value: datasetName }, + }), + ...(datasetType && { + type: { type: 'constant_keyword', value: datasetType }, + }), + }, + }, + }), + }, + }, + }, + ]) + ); + } + return indexStats; + }); +} diff --git a/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.ts b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.ts new file mode 100644 index 0000000000000..cf906bc5c86cf --- /dev/null +++ b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/get_data_telemetry.ts @@ -0,0 +1,253 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { LegacyAPICaller } from 'kibana/server'; +import { + DATA_DATASETS_INDEX_PATTERNS_UNIQUE, + DataPatternName, + DataTelemetryType, +} from './constants'; + +export interface DataTelemetryBasePayload { + index_count: number; + ecs_index_count?: number; + doc_count?: number; + size_in_bytes?: number; +} + +export interface DataTelemetryDocument extends DataTelemetryBasePayload { + dataset?: { + name?: string; + type?: DataTelemetryType | 'unknown' | string; // The union of types is to help autocompletion with some known `dataset.type`s + }; + shipper?: string; + pattern_name?: DataPatternName; +} + +export type DataTelemetryPayload = DataTelemetryDocument[]; + +export interface DataTelemetryIndex { + name: string; + datasetName?: string; // To be obtained from `mappings.dataset.name` if it's a constant keyword + datasetType?: string; // To be obtained from `mappings.dataset.type` if it's a constant keyword + shipper?: string; // To be obtained from `_meta.beat` if it's set + isECS?: boolean; // Optional because it can't be obtained via Monitoring. + + // The fields below are optional because we might not be able to obtain them if the user does not + // have access to the index. + docCount?: number; + sizeInBytes?: number; +} + +type AtLeastOne }> = Partial & U[keyof U]; + +type DataDescriptor = AtLeastOne<{ + datasetName: string; + datasetType: string; + shipper: string; + patternName: DataPatternName; // When found from the list of the index patterns +}>; + +function findMatchingDescriptors({ + name, + shipper, + datasetName, + datasetType, +}: DataTelemetryIndex): DataDescriptor[] { + // If we already have the data from the indices' mappings... + if ([shipper, datasetName, datasetType].some(Boolean)) { + return [ + { + ...(shipper && { shipper }), + ...(datasetName && { datasetName }), + ...(datasetType && { datasetType }), + } as AtLeastOne<{ datasetName: string; datasetType: string; shipper: string }>, // Using casting here because TS doesn't infer at least one exists from the if clause + ]; + } + + // Otherwise, try with the list of known index patterns + return DATA_DATASETS_INDEX_PATTERNS_UNIQUE.filter(({ pattern }) => { + if (!pattern.startsWith('.') && name.startsWith('.')) { + // avoid system indices caught by very fuzzy index patterns (i.e.: *log* would catch `.kibana-log-...`) + return false; + } + return new RegExp(`^${pattern.replace(/\./g, '\\.').replace(/\*/g, '.*')}$`).test(name); + }); +} + +function increaseCounters( + previousValue: DataTelemetryBasePayload = { index_count: 0 }, + { isECS, docCount, sizeInBytes }: DataTelemetryIndex +) { + return { + ...previousValue, + index_count: previousValue.index_count + 1, + ...(typeof isECS === 'boolean' + ? { + ecs_index_count: (previousValue.ecs_index_count || 0) + (isECS ? 1 : 0), + } + : {}), + ...(typeof docCount === 'number' + ? { doc_count: (previousValue.doc_count || 0) + docCount } + : {}), + ...(typeof sizeInBytes === 'number' + ? { size_in_bytes: (previousValue.size_in_bytes || 0) + sizeInBytes } + : {}), + }; +} + +export function buildDataTelemetryPayload(indices: DataTelemetryIndex[]): DataTelemetryPayload { + const startingDotPatternsUntilTheFirstAsterisk = DATA_DATASETS_INDEX_PATTERNS_UNIQUE.map( + ({ pattern }) => pattern.replace(/^\.(.+)\*.*$/g, '.$1') + ).filter(Boolean); + + // Filter out the system indices unless they are required by the patterns + const indexCandidates = indices.filter( + ({ name }) => + !( + name.startsWith('.') && + !startingDotPatternsUntilTheFirstAsterisk.find((pattern) => name.startsWith(pattern)) + ) + ); + + const acc = new Map(); + + for (const indexCandidate of indexCandidates) { + const matchingDescriptors = findMatchingDescriptors(indexCandidate); + for (const { datasetName, datasetType, shipper, patternName } of matchingDescriptors) { + const key = `${datasetName}-${datasetType}-${shipper}-${patternName}`; + acc.set(key, { + ...((datasetName || datasetType) && { dataset: { name: datasetName, type: datasetType } }), + ...(shipper && { shipper }), + ...(patternName && { pattern_name: patternName }), + ...increaseCounters(acc.get(key), indexCandidate), + }); + } + } + + return [...acc.values()]; +} + +interface IndexStats { + indices: { + [indexName: string]: { + total: { + docs: { + count: number; + deleted: number; + }; + store: { + size_in_bytes: number; + }; + }; + }; + }; +} + +interface IndexMappings { + [indexName: string]: { + mappings: { + _meta?: { + beat?: string; + }; + properties: { + dataset?: { + properties: { + name?: { + type: string; + value?: string; + }; + type?: { + type: string; + value?: string; + }; + }; + }; + ecs?: { + properties: { + version?: { + type: string; + }; + }; + }; + }; + }; + }; +} + +export async function getDataTelemetry(callCluster: LegacyAPICaller) { + try { + const index = [ + ...DATA_DATASETS_INDEX_PATTERNS_UNIQUE.map(({ pattern }) => pattern), + '*-*-*-*', // Include new indexing strategy indices {type}-{dataset}-{namespace}-{rollover_counter} + ]; + const [indexMappings, indexStats]: [IndexMappings, IndexStats] = await Promise.all([ + // GET */_mapping?filter_path=*.mappings._meta.beat,*.mappings.properties.ecs.properties.version.type,*.mappings.properties.dataset.properties.type.value,*.mappings.properties.dataset.properties.name.value + callCluster('indices.getMapping', { + index: '*', // Request all indices because filter_path already filters out the indices without any of those fields + filterPath: [ + // _meta.beat tells the shipper + '*.mappings._meta.beat', + // Does it have `ecs.version` in the mappings? => It follows the ECS conventions + '*.mappings.properties.ecs.properties.version.type', + + // Disable the fields below because they are still pending to be confirmed: + // https://github.com/elastic/ecs/pull/845 + // TODO: Re-enable when the final fields are confirmed + // // If `dataset.type` is a `constant_keyword`, it can be reported as a type + // '*.mappings.properties.dataset.properties.type.value', + // // If `dataset.name` is a `constant_keyword`, it can be reported as the dataset + // '*.mappings.properties.dataset.properties.name.value', + ], + }), + // GET /_stats/docs,store?level=indices&filter_path=indices.*.total + callCluster('indices.stats', { + index, + level: 'indices', + metric: ['docs', 'store'], + filterPath: ['indices.*.total'], + }), + ]); + + const indexNames = Object.keys({ ...indexMappings, ...indexStats?.indices }); + const indices = indexNames.map((name) => { + const isECS = !!indexMappings[name]?.mappings?.properties.ecs?.properties.version?.type; + const shipper = indexMappings[name]?.mappings?._meta?.beat; + const datasetName = indexMappings[name]?.mappings?.properties.dataset?.properties.name?.value; + const datasetType = indexMappings[name]?.mappings?.properties.dataset?.properties.type?.value; + + const stats = (indexStats?.indices || {})[name]; + if (stats) { + return { + name, + datasetName, + datasetType, + shipper, + isECS, + docCount: stats.total?.docs?.count, + sizeInBytes: stats.total?.store?.size_in_bytes, + }; + } + return { name, datasetName, datasetType, shipper, isECS }; + }); + return buildDataTelemetryPayload(indices); + } catch (e) { + return []; + } +} diff --git a/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/index.ts b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/index.ts new file mode 100644 index 0000000000000..d056d1c9f299f --- /dev/null +++ b/src/plugins/telemetry/server/telemetry_collection/get_data_telemetry/index.ts @@ -0,0 +1,27 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { DATA_TELEMETRY_ID } from './constants'; + +export { + DataTelemetryIndex, + DataTelemetryPayload, + getDataTelemetry, + buildDataTelemetryPayload, +} from './get_data_telemetry'; diff --git a/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts b/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts index b42edde2f55ca..4d4031bb428ba 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts +++ b/src/plugins/telemetry/server/telemetry_collection/get_local_stats.ts @@ -25,6 +25,7 @@ import { getClusterInfo, ESClusterInfo } from './get_cluster_info'; import { getClusterStats } from './get_cluster_stats'; import { getKibana, handleKibanaStats, KibanaUsageStats } from './get_kibana'; import { getNodesUsage } from './get_nodes_usage'; +import { getDataTelemetry, DATA_TELEMETRY_ID, DataTelemetryPayload } from './get_data_telemetry'; /** * Handle the separate local calls by combining them into a single object response that looks like the @@ -39,6 +40,7 @@ export function handleLocalStats( { cluster_name, cluster_uuid, version }: ESClusterInfo, { _nodes, cluster_name: clusterName, ...clusterStats }: any, kibana: KibanaUsageStats, + dataTelemetry: DataTelemetryPayload, context: StatsCollectionContext ) { return { @@ -49,6 +51,7 @@ export function handleLocalStats( cluster_stats: clusterStats, collection: 'local', stack_stats: { + [DATA_TELEMETRY_ID]: dataTelemetry, kibana: handleKibanaStats(context, kibana), }, }; @@ -68,11 +71,12 @@ export const getLocalStats: StatsGetter<{}, TelemetryLocalStats> = async ( return await Promise.all( clustersDetails.map(async (clustersDetail) => { - const [clusterInfo, clusterStats, nodesUsage, kibana] = await Promise.all([ + const [clusterInfo, clusterStats, nodesUsage, kibana, dataTelemetry] = await Promise.all([ getClusterInfo(callCluster), // cluster info getClusterStats(callCluster), // cluster stats (not to be confused with cluster _state_) getNodesUsage(callCluster), // nodes_usage info getKibana(usageCollection, callCluster), + getDataTelemetry(callCluster), ]); return handleLocalStats( clusterInfo, @@ -81,6 +85,7 @@ export const getLocalStats: StatsGetter<{}, TelemetryLocalStats> = async ( nodes: { ...clusterStats.nodes, usage: nodesUsage }, }, kibana, + dataTelemetry, context ); }) diff --git a/src/plugins/telemetry/server/telemetry_collection/index.ts b/src/plugins/telemetry/server/telemetry_collection/index.ts index 377ddab7b877c..40cbf0e4caa1d 100644 --- a/src/plugins/telemetry/server/telemetry_collection/index.ts +++ b/src/plugins/telemetry/server/telemetry_collection/index.ts @@ -17,6 +17,12 @@ * under the License. */ +export { + DATA_TELEMETRY_ID, + DataTelemetryIndex, + DataTelemetryPayload, + buildDataTelemetryPayload, +} from './get_data_telemetry'; export { getLocalStats, TelemetryLocalStats } from './get_local_stats'; export { getLocalLicense } from './get_local_license'; export { getClusterUuids } from './get_cluster_stats'; diff --git a/test/api_integration/apis/telemetry/telemetry_local.js b/test/api_integration/apis/telemetry/telemetry_local.js index e74cd180185ab..88e6b3a29052e 100644 --- a/test/api_integration/apis/telemetry/telemetry_local.js +++ b/test/api_integration/apis/telemetry/telemetry_local.js @@ -37,8 +37,17 @@ function flatKeys(source) { export default function ({ getService }) { const supertest = getService('supertest'); + const es = getService('es'); describe('/api/telemetry/v2/clusters/_stats', () => { + before('create some telemetry-data tracked indices', async () => { + return es.indices.create({ index: 'filebeat-telemetry_tests_logs' }); + }); + + after('cleanup telemetry-data tracked indices', () => { + return es.indices.delete({ index: 'filebeat-telemetry_tests_logs' }); + }); + it('should pull local stats and validate data types', async () => { const timeRange = { min: '2018-07-23T22:07:00Z', @@ -71,6 +80,17 @@ export default function ({ getService }) { expect(stats.stack_stats.kibana.plugins.csp.strict).to.be(true); expect(stats.stack_stats.kibana.plugins.csp.warnLegacyBrowsers).to.be(true); expect(stats.stack_stats.kibana.plugins.csp.rulesChangedFromDefault).to.be(false); + + // Testing stack_stats.data + expect(stats.stack_stats.data).to.be.an('object'); + expect(stats.stack_stats.data).to.be.an('array'); + expect(stats.stack_stats.data[0]).to.be.an('object'); + expect(stats.stack_stats.data[0].pattern_name).to.be('filebeat'); + expect(stats.stack_stats.data[0].shipper).to.be('filebeat'); + expect(stats.stack_stats.data[0].index_count).to.be(1); + expect(stats.stack_stats.data[0].doc_count).to.be(0); + expect(stats.stack_stats.data[0].ecs_index_count).to.be(0); + expect(stats.stack_stats.data[0].size_in_bytes).to.be.greaterThan(0); }); it('should pull local stats and validate fields', async () => { diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/get_indices.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/get_indices.js index c087d20a97db1..ba6d0cb926f06 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/get_indices.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/get_indices.js @@ -77,11 +77,11 @@ export function handleResponse(resp, min, max, shardStats) { }); } -export function getIndices(req, esIndexPattern, showSystemIndices = false, shardStats) { - checkParam(esIndexPattern, 'esIndexPattern in elasticsearch/getIndices'); - - const { min, max } = req.payload.timeRange; - +export function buildGetIndicesQuery( + esIndexPattern, + clusterUuid, + { start, end, size, showSystemIndices = false } +) { const filters = []; if (!showSystemIndices) { filters.push({ @@ -90,14 +90,11 @@ export function getIndices(req, esIndexPattern, showSystemIndices = false, shard }, }); } - - const clusterUuid = req.params.clusterUuid; const metricFields = ElasticsearchMetric.getMetricFields(); - const config = req.server.config(); - const params = { + + return { index: esIndexPattern, - // TODO: composite aggregation - size: config.get('monitoring.ui.max_bucket_size'), + size, ignoreUnavailable: true, filterPath: [ // only filter path can filter for inner_hits @@ -118,8 +115,8 @@ export function getIndices(req, esIndexPattern, showSystemIndices = false, shard body: { query: createQuery({ type: 'index_stats', - start: min, - end: max, + start, + end, clusterUuid, metric: metricFields, filters, @@ -135,9 +132,24 @@ export function getIndices(req, esIndexPattern, showSystemIndices = false, shard sort: [{ timestamp: { order: 'desc' } }], }, }; +} + +export function getIndices(req, esIndexPattern, showSystemIndices = false, shardStats) { + checkParam(esIndexPattern, 'esIndexPattern in elasticsearch/getIndices'); + + const { min: start, max: end } = req.payload.timeRange; + + const clusterUuid = req.params.clusterUuid; + const config = req.server.config(); + const params = buildGetIndicesQuery(esIndexPattern, clusterUuid, { + start, + end, + showSystemIndices, + size: config.get('monitoring.ui.max_bucket_size'), + }); const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('monitoring'); return callWithRequest(req, 'search', params).then((resp) => - handleResponse(resp, min, max, shardStats) + handleResponse(resp, start, end, shardStats) ); } diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/index.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/index.js index 0ac2610bbba62..b07e3511d4804 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/index.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/indices/index.js @@ -4,5 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export { getIndices } from './get_indices'; +export { getIndices, buildGetIndicesQuery } from './get_indices'; export { getIndexSummary } from './get_index_summary'; diff --git a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/__snapshots__/get_stats_with_xpack.test.ts.snap b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/__snapshots__/get_stats_with_xpack.test.ts.snap index ed82dc65eb410..b9bb206b8056f 100644 --- a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/__snapshots__/get_stats_with_xpack.test.ts.snap +++ b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/__snapshots__/get_stats_with_xpack.test.ts.snap @@ -1,158 +1,158 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Telemetry Collection: Get Aggregated Stats OSS-like telemetry (no license nor X-Pack telemetry) 1`] = ` -Array [ - Object { - "cluster_name": "test", - "cluster_stats": Object { - "nodes": Object { - "usage": Object { - "nodes": Array [ - Object { - "aggregations": Object { - "terms": Object { - "bytes": 2, - }, - }, - "node_id": "some_node_id", - "rest_actions": Object { - "nodes_usage_action": 1, +Object { + "cluster_name": "test", + "cluster_stats": Object { + "nodes": Object { + "usage": Object { + "nodes": Array [ + Object { + "aggregations": Object { + "terms": Object { + "bytes": 2, }, - "since": 1588616945163, - "timestamp": 1588617023177, }, - ], - }, - }, - }, - "cluster_uuid": "test", - "collection": "local", - "stack_stats": Object { - "kibana": Object { - "count": 1, - "great": "googlymoogly", - "indices": 1, - "os": Object { - "platformReleases": Array [ - Object { - "count": 1, - "platformRelease": "iv", - }, - ], - "platforms": Array [ - Object { - "count": 1, - "platform": "rocky", + "node_id": "some_node_id", + "rest_actions": Object { + "nodes_usage_action": 1, }, - ], - }, - "plugins": Object { - "clouds": Object { - "chances": 95, + "since": 1588616945163, + "timestamp": 1588617023177, }, - "localization": Object { - "integrities": Object {}, - "labelsCount": 0, - "locale": "en", - }, - "rain": Object { - "chances": 2, - }, - "snow": Object { - "chances": 0, - }, - "sun": Object { - "chances": 5, + ], + }, + }, + }, + "cluster_uuid": "test", + "collection": "local", + "stack_stats": Object { + "data": Array [], + "kibana": Object { + "count": 1, + "great": "googlymoogly", + "indices": 1, + "os": Object { + "platformReleases": Array [ + Object { + "count": 1, + "platformRelease": "iv", }, - }, - "versions": Array [ + ], + "platforms": Array [ Object { "count": 1, - "version": "8675309", + "platform": "rocky", }, ], }, + "plugins": Object { + "clouds": Object { + "chances": 95, + }, + "localization": Object { + "integrities": Object {}, + "labelsCount": 0, + "locale": "en", + }, + "rain": Object { + "chances": 2, + }, + "snow": Object { + "chances": 0, + }, + "sun": Object { + "chances": 5, + }, + }, + "versions": Array [ + Object { + "count": 1, + "version": "8675309", + }, + ], }, - "version": "8.0.0", }, -] + "timestamp": Any, + "version": "8.0.0", +} `; exports[`Telemetry Collection: Get Aggregated Stats X-Pack telemetry (license + X-Pack) 1`] = ` -Array [ - Object { - "cluster_name": "test", - "cluster_stats": Object { - "nodes": Object { - "usage": Object { - "nodes": Array [ - Object { - "aggregations": Object { - "terms": Object { - "bytes": 2, - }, - }, - "node_id": "some_node_id", - "rest_actions": Object { - "nodes_usage_action": 1, +Object { + "cluster_name": "test", + "cluster_stats": Object { + "nodes": Object { + "usage": Object { + "nodes": Array [ + Object { + "aggregations": Object { + "terms": Object { + "bytes": 2, }, - "since": 1588616945163, - "timestamp": 1588617023177, }, - ], - }, - }, - }, - "cluster_uuid": "test", - "collection": "local", - "stack_stats": Object { - "kibana": Object { - "count": 1, - "great": "googlymoogly", - "indices": 1, - "os": Object { - "platformReleases": Array [ - Object { - "count": 1, - "platformRelease": "iv", - }, - ], - "platforms": Array [ - Object { - "count": 1, - "platform": "rocky", + "node_id": "some_node_id", + "rest_actions": Object { + "nodes_usage_action": 1, }, - ], - }, - "plugins": Object { - "clouds": Object { - "chances": 95, + "since": 1588616945163, + "timestamp": 1588617023177, }, - "localization": Object { - "integrities": Object {}, - "labelsCount": 0, - "locale": "en", - }, - "rain": Object { - "chances": 2, - }, - "snow": Object { - "chances": 0, - }, - "sun": Object { - "chances": 5, + ], + }, + }, + }, + "cluster_uuid": "test", + "collection": "local", + "stack_stats": Object { + "data": Array [], + "kibana": Object { + "count": 1, + "great": "googlymoogly", + "indices": 1, + "os": Object { + "platformReleases": Array [ + Object { + "count": 1, + "platformRelease": "iv", }, - }, - "versions": Array [ + ], + "platforms": Array [ Object { "count": 1, - "version": "8675309", + "platform": "rocky", }, ], }, - "xpack": Object {}, + "plugins": Object { + "clouds": Object { + "chances": 95, + }, + "localization": Object { + "integrities": Object {}, + "labelsCount": 0, + "locale": "en", + }, + "rain": Object { + "chances": 2, + }, + "snow": Object { + "chances": 0, + }, + "sun": Object { + "chances": 5, + }, + }, + "versions": Array [ + Object { + "count": 1, + "version": "8675309", + }, + ], }, - "version": "8.0.0", + "xpack": Object {}, }, -] + "timestamp": Any, + "version": "8.0.0", +} `; diff --git a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.test.ts b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.test.ts index a8311933f0531..24382fb89d337 100644 --- a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.test.ts +++ b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.test.ts @@ -85,7 +85,11 @@ describe('Telemetry Collection: Get Aggregated Stats', () => { } as any, context ); - expect(stats.map(({ timestamp, ...rest }) => rest)).toMatchSnapshot(); + stats.forEach((entry) => { + expect(entry).toMatchSnapshot({ + timestamp: expect.any(String), + }); + }); }); test('X-Pack telemetry (license + X-Pack)', async () => { @@ -123,6 +127,10 @@ describe('Telemetry Collection: Get Aggregated Stats', () => { } as any, context ); - expect(stats.map(({ timestamp, ...rest }) => rest)).toMatchSnapshot(); + stats.forEach((entry) => { + expect(entry).toMatchSnapshot({ + timestamp: expect.any(String), + }); + }); }); }); diff --git a/x-pack/test/api_integration/apis/telemetry/fixtures/basiccluster.json b/x-pack/test/api_integration/apis/telemetry/fixtures/basiccluster.json index a0097f53ac93b..74d91a6215c79 100644 --- a/x-pack/test/api_integration/apis/telemetry/fixtures/basiccluster.json +++ b/x-pack/test/api_integration/apis/telemetry/fixtures/basiccluster.json @@ -153,9 +153,7 @@ "min": 223 } }, - "versions": [ - "6.3.1" - ] + "versions": ["6.3.1"] }, "status": "yellow", "timestamp": 1532386499084 @@ -297,9 +295,7 @@ }, "audit": { "enabled": false, - "outputs": [ - "logfile" - ] + "outputs": ["logfile"] }, "available": false, "enabled": true, diff --git a/x-pack/test/api_integration/apis/telemetry/fixtures/multicluster.json b/x-pack/test/api_integration/apis/telemetry/fixtures/multicluster.json index 6cc9c55157b28..7d408e39247ee 100644 --- a/x-pack/test/api_integration/apis/telemetry/fixtures/multicluster.json +++ b/x-pack/test/api_integration/apis/telemetry/fixtures/multicluster.json @@ -92,9 +92,7 @@ "master": 1, "ingest": 1 }, - "versions": [ - "7.0.0-alpha1" - ], + "versions": ["7.0.0-alpha1"], "os": { "available_processors": 4, "allocated_processors": 1, @@ -214,9 +212,7 @@ } }, "audit": { - "outputs": [ - "logfile" - ], + "outputs": ["logfile"], "enabled": false }, "ipfilter": { @@ -383,9 +379,7 @@ "master": 1, "ingest": 1 }, - "versions": [ - "7.0.0-alpha1" - ], + "versions": ["7.0.0-alpha1"], "os": { "available_processors": 4, "allocated_processors": 1, @@ -461,34 +455,22 @@ "enabled": true, "realms": { "file": { - "name": [ - "default_file" - ], + "name": ["default_file"], "available": true, - "size": [ - 0 - ], + "size": [0], "enabled": true, - "order": [ - 2147483647 - ] + "order": [2147483647] }, "ldap": { "available": true, "enabled": false }, "native": { - "name": [ - "default_native" - ], + "name": ["default_native"], "available": true, - "size": [ - 2 - ], + "size": [2], "enabled": true, - "order": [ - 2147483647 - ] + "order": [2147483647] }, "active_directory": { "available": true, @@ -523,9 +505,7 @@ } }, "audit": { - "outputs": [ - "logfile" - ], + "outputs": ["logfile"], "enabled": false }, "ipfilter": { @@ -700,9 +680,7 @@ "master": 2, "ingest": 2 }, - "versions": [ - "7.0.0-alpha1" - ], + "versions": ["7.0.0-alpha1"], "os": { "available_processors": 8, "allocated_processors": 2, @@ -778,34 +756,22 @@ "enabled": true, "realms": { "file": { - "name": [ - "default_file" - ], + "name": ["default_file"], "available": true, - "size": [ - 0 - ], + "size": [0], "enabled": true, - "order": [ - 2147483647 - ] + "order": [2147483647] }, "ldap": { "available": true, "enabled": false }, "native": { - "name": [ - "default_native" - ], + "name": ["default_native"], "available": true, - "size": [ - 1 - ], + "size": [1], "enabled": true, - "order": [ - 2147483647 - ] + "order": [2147483647] }, "active_directory": { "available": true, @@ -840,9 +806,7 @@ } }, "audit": { - "outputs": [ - "logfile" - ], + "outputs": ["logfile"], "enabled": false }, "ipfilter": { From f86afd4b070b219595dc444b43e86f24364ae30d Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Thu, 2 Jul 2020 11:02:35 +0300 Subject: [PATCH 26/34] [Visualizations] Each visType returns its supported triggers (#70177) * Refactor hardcoded supportedTriggers function with a callback function of visType * use typescript to check if function exists * Remove brush event from heatmap Co-authored-by: Elastic Machine --- .../vis_type_table/public/table_vis_type.ts | 4 +++ .../public/tag_cloud_type.ts | 4 +++ src/plugins/vis_type_vislib/public/area.ts | 4 +++ src/plugins/vis_type_vislib/public/heatmap.ts | 4 +++ .../vis_type_vislib/public/histogram.ts | 4 +++ .../vis_type_vislib/public/horizontal_bar.ts | 4 +++ src/plugins/vis_type_vislib/public/line.ts | 4 +++ src/plugins/vis_type_vislib/public/pie.ts | 4 +++ .../public/embeddable/visualize_embeddable.ts | 25 +------------------ .../public/vis_types/base_vis_type.ts | 4 +++ .../public/vis_types/types_service.ts | 2 ++ .../vis_types/vis_type_alias_registry.ts | 3 +++ 12 files changed, 42 insertions(+), 24 deletions(-) diff --git a/src/plugins/vis_type_table/public/table_vis_type.ts b/src/plugins/vis_type_table/public/table_vis_type.ts index c3bc72497007e..80d53021b7866 100644 --- a/src/plugins/vis_type_table/public/table_vis_type.ts +++ b/src/plugins/vis_type_table/public/table_vis_type.ts @@ -26,6 +26,7 @@ import { tableVisResponseHandler } from './table_vis_response_handler'; import tableVisTemplate from './table_vis.html'; import { TableOptions } from './components/table_vis_options_lazy'; import { getTableVisualizationControllerClass } from './vis_controller'; +import { VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; export function getTableVisTypeDefinition(core: CoreSetup, context: PluginInitializerContext) { return { @@ -39,6 +40,9 @@ export function getTableVisTypeDefinition(core: CoreSetup, context: PluginInitia defaultMessage: 'Display values in a table', }), visualization: getTableVisualizationControllerClass(core, context), + getSupportedTriggers: () => { + return [VIS_EVENT_TO_TRIGGER.filter]; + }, visConfig: { defaults: { perPage: 10, diff --git a/src/plugins/vis_type_tagcloud/public/tag_cloud_type.ts b/src/plugins/vis_type_tagcloud/public/tag_cloud_type.ts index 5a8cc3004a315..023489c6d2e87 100644 --- a/src/plugins/vis_type_tagcloud/public/tag_cloud_type.ts +++ b/src/plugins/vis_type_tagcloud/public/tag_cloud_type.ts @@ -22,6 +22,7 @@ import { i18n } from '@kbn/i18n'; import { Schemas } from '../../vis_default_editor/public'; import { TagCloudOptions } from './components/tag_cloud_options'; +import { VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; // @ts-ignore import { createTagCloudVisualization } from './components/tag_cloud_visualization'; @@ -31,6 +32,9 @@ export const createTagCloudVisTypeDefinition = (deps: TagCloudVisDependencies) = name: 'tagcloud', title: i18n.translate('visTypeTagCloud.vis.tagCloudTitle', { defaultMessage: 'Tag Cloud' }), icon: 'visTagCloud', + getSupportedTriggers: () => { + return [VIS_EVENT_TO_TRIGGER.filter]; + }, description: i18n.translate('visTypeTagCloud.vis.tagCloudDescription', { defaultMessage: 'A group of words, sized according to their importance', }), diff --git a/src/plugins/vis_type_vislib/public/area.ts b/src/plugins/vis_type_vislib/public/area.ts index c42962ad50a4b..ec90fbd1746a1 100644 --- a/src/plugins/vis_type_vislib/public/area.ts +++ b/src/plugins/vis_type_vislib/public/area.ts @@ -40,6 +40,7 @@ import { getAreaOptionTabs, countLabel } from './utils/common_config'; import { createVislibVisController } from './vis_controller'; import { VisTypeVislibDependencies } from './plugin'; import { Rotates } from '../../charts/public'; +import { VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; export const createAreaVisTypeDefinition = (deps: VisTypeVislibDependencies) => ({ name: 'area', @@ -49,6 +50,9 @@ export const createAreaVisTypeDefinition = (deps: VisTypeVislibDependencies) => defaultMessage: 'Emphasize the quantity beneath a line chart', }), visualization: createVislibVisController(deps), + getSupportedTriggers: () => { + return [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.brush]; + }, visConfig: { defaults: { type: 'area', diff --git a/src/plugins/vis_type_vislib/public/heatmap.ts b/src/plugins/vis_type_vislib/public/heatmap.ts index ced7a38568ffd..bd3d02029cb23 100644 --- a/src/plugins/vis_type_vislib/public/heatmap.ts +++ b/src/plugins/vis_type_vislib/public/heatmap.ts @@ -28,6 +28,7 @@ import { TimeMarker } from './vislib/visualizations/time_marker'; import { CommonVislibParams, ValueAxis } from './types'; import { VisTypeVislibDependencies } from './plugin'; import { ColorSchemas, ColorSchemaParams } from '../../charts/public'; +import { VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; export interface HeatmapVisParams extends CommonVislibParams, ColorSchemaParams { type: 'heatmap'; @@ -48,6 +49,9 @@ export const createHeatmapVisTypeDefinition = (deps: VisTypeVislibDependencies) description: i18n.translate('visTypeVislib.heatmap.heatmapDescription', { defaultMessage: 'Shade cells within a matrix', }), + getSupportedTriggers: () => { + return [VIS_EVENT_TO_TRIGGER.filter]; + }, visualization: createVislibVisController(deps), visConfig: { defaults: { diff --git a/src/plugins/vis_type_vislib/public/histogram.ts b/src/plugins/vis_type_vislib/public/histogram.ts index 52242ad11e8f5..8aeeb4ec533ab 100644 --- a/src/plugins/vis_type_vislib/public/histogram.ts +++ b/src/plugins/vis_type_vislib/public/histogram.ts @@ -39,6 +39,7 @@ import { getAreaOptionTabs, countLabel } from './utils/common_config'; import { createVislibVisController } from './vis_controller'; import { VisTypeVislibDependencies } from './plugin'; import { Rotates } from '../../charts/public'; +import { VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; export const createHistogramVisTypeDefinition = (deps: VisTypeVislibDependencies) => ({ name: 'histogram', @@ -50,6 +51,9 @@ export const createHistogramVisTypeDefinition = (deps: VisTypeVislibDependencies defaultMessage: 'Assign a continuous variable to each axis', }), visualization: createVislibVisController(deps), + getSupportedTriggers: () => { + return [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.brush]; + }, visConfig: { defaults: { type: 'histogram', diff --git a/src/plugins/vis_type_vislib/public/horizontal_bar.ts b/src/plugins/vis_type_vislib/public/horizontal_bar.ts index a58c15f136431..702581828e60d 100644 --- a/src/plugins/vis_type_vislib/public/horizontal_bar.ts +++ b/src/plugins/vis_type_vislib/public/horizontal_bar.ts @@ -37,6 +37,7 @@ import { getAreaOptionTabs, countLabel } from './utils/common_config'; import { createVislibVisController } from './vis_controller'; import { VisTypeVislibDependencies } from './plugin'; import { Rotates } from '../../charts/public'; +import { VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; export const createHorizontalBarVisTypeDefinition = (deps: VisTypeVislibDependencies) => ({ name: 'horizontal_bar', @@ -48,6 +49,9 @@ export const createHorizontalBarVisTypeDefinition = (deps: VisTypeVislibDependen defaultMessage: 'Assign a continuous variable to each axis', }), visualization: createVislibVisController(deps), + getSupportedTriggers: () => { + return [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.brush]; + }, visConfig: { defaults: { type: 'histogram', diff --git a/src/plugins/vis_type_vislib/public/line.ts b/src/plugins/vis_type_vislib/public/line.ts index a94fd3f3945ab..6e9190229114b 100644 --- a/src/plugins/vis_type_vislib/public/line.ts +++ b/src/plugins/vis_type_vislib/public/line.ts @@ -38,6 +38,7 @@ import { getAreaOptionTabs, countLabel } from './utils/common_config'; import { createVislibVisController } from './vis_controller'; import { VisTypeVislibDependencies } from './plugin'; import { Rotates } from '../../charts/public'; +import { VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; export const createLineVisTypeDefinition = (deps: VisTypeVislibDependencies) => ({ name: 'line', @@ -47,6 +48,9 @@ export const createLineVisTypeDefinition = (deps: VisTypeVislibDependencies) => defaultMessage: 'Emphasize trends', }), visualization: createVislibVisController(deps), + getSupportedTriggers: () => { + return [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.brush]; + }, visConfig: { defaults: { type: 'line', diff --git a/src/plugins/vis_type_vislib/public/pie.ts b/src/plugins/vis_type_vislib/public/pie.ts index a68bc5893406f..1e81dbdde3f68 100644 --- a/src/plugins/vis_type_vislib/public/pie.ts +++ b/src/plugins/vis_type_vislib/public/pie.ts @@ -26,6 +26,7 @@ import { getPositions, Positions } from './utils/collections'; import { createVislibVisController } from './vis_controller'; import { CommonVislibParams } from './types'; import { VisTypeVislibDependencies } from './plugin'; +import { VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; export interface PieVisParams extends CommonVislibParams { type: 'pie'; @@ -47,6 +48,9 @@ export const createPieVisTypeDefinition = (deps: VisTypeVislibDependencies) => ( defaultMessage: 'Compare parts of a whole', }), visualization: createVislibVisController(deps), + getSupportedTriggers: () => { + return [VIS_EVENT_TO_TRIGGER.filter]; + }, visConfig: { defaults: { type: 'pie', diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts index 26fdd665192a6..2f9cda32fccdc 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts @@ -377,29 +377,6 @@ export class VisualizeEmbeddable extends Embeddable Array; icon?: string; image?: string; stage?: 'experimental' | 'beta' | 'production'; @@ -44,6 +46,7 @@ export class BaseVisType { name: string; title: string; description: string; + getSupportedTriggers?: () => Array; icon?: string; image?: string; stage: 'experimental' | 'beta' | 'production'; @@ -77,6 +80,7 @@ export class BaseVisType { this.name = opts.name; this.description = opts.description || ''; + this.getSupportedTriggers = opts.getSupportedTriggers; this.title = opts.title; this.icon = opts.icon; this.image = opts.image; diff --git a/src/plugins/visualizations/public/vis_types/types_service.ts b/src/plugins/visualizations/public/vis_types/types_service.ts index 321f96180fd68..14c2a9c50ab0e 100644 --- a/src/plugins/visualizations/public/vis_types/types_service.ts +++ b/src/plugins/visualizations/public/vis_types/types_service.ts @@ -23,11 +23,13 @@ import { visTypeAliasRegistry, VisTypeAlias } from './vis_type_alias_registry'; import { BaseVisType } from './base_vis_type'; // @ts-ignore import { ReactVisType } from './react_vis_type'; +import { TriggerContextMapping } from '../../../ui_actions/public'; export interface VisType { name: string; title: string; description?: string; + getSupportedTriggers?: () => Array; visualization: any; isAccessible?: boolean; requestHandler: string | unknown; diff --git a/src/plugins/visualizations/public/vis_types/vis_type_alias_registry.ts b/src/plugins/visualizations/public/vis_types/vis_type_alias_registry.ts index bc80d549c81e6..f6d27b54c7c64 100644 --- a/src/plugins/visualizations/public/vis_types/vis_type_alias_registry.ts +++ b/src/plugins/visualizations/public/vis_types/vis_type_alias_registry.ts @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import { TriggerContextMapping } from '../../../ui_actions/public'; export interface VisualizationListItem { editUrl: string; @@ -26,6 +27,7 @@ export interface VisualizationListItem { savedObjectType: string; title: string; description?: string; + getSupportedTriggers?: () => Array; typeTitle: string; image?: string; } @@ -53,6 +55,7 @@ export interface VisTypeAlias { icon: string; promotion?: VisTypeAliasPromotion; description: string; + getSupportedTriggers?: () => Array; stage: 'experimental' | 'beta' | 'production'; appExtensions?: { From dc2737b8686f6f96b357fcea91d3e1060683fb9a Mon Sep 17 00:00:00 2001 From: Daniil Suleiman <31325372+sulemanof@users.noreply.github.com> Date: Thu, 2 Jul 2020 11:38:46 +0300 Subject: [PATCH 27/34] Filter out error when calculating a label (#69934) Co-authored-by: Elastic Machine --- .../public/search/tabify/get_columns.test.ts | 16 ++++++++++++++++ .../data/public/search/tabify/get_columns.ts | 9 ++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/plugins/data/public/search/tabify/get_columns.test.ts b/src/plugins/data/public/search/tabify/get_columns.test.ts index 0c5551d95690f..35f0181f63302 100644 --- a/src/plugins/data/public/search/tabify/get_columns.test.ts +++ b/src/plugins/data/public/search/tabify/get_columns.test.ts @@ -161,4 +161,20 @@ describe('get columns', () => { 'Sum of @timestamp', ]); }); + + test('should not fail if there is no field for date histogram agg', () => { + const columns = tabifyGetColumns( + createAggConfigs([ + { + type: 'date_histogram', + schema: 'segment', + params: {}, + }, + { type: 'sum', schema: 'metric', params: { field: '@timestamp' } }, + ]).aggs, + false + ); + + expect(columns.map((c) => c.name)).toEqual(['', 'Sum of @timestamp']); + }); }); diff --git a/src/plugins/data/public/search/tabify/get_columns.ts b/src/plugins/data/public/search/tabify/get_columns.ts index 8c538288d2fea..8e907d4b0cb88 100644 --- a/src/plugins/data/public/search/tabify/get_columns.ts +++ b/src/plugins/data/public/search/tabify/get_columns.ts @@ -22,10 +22,17 @@ import { IAggConfig } from '../aggs'; import { TabbedAggColumn } from './types'; const getColumn = (agg: IAggConfig, i: number): TabbedAggColumn => { + let name = ''; + try { + name = agg.makeLabel(); + } catch (e) { + // skip the case when makeLabel throws an error (e.x. no appropriate field for an aggregation) + } + return { aggConfig: agg, id: `col-${i}-${agg.id}`, - name: agg.makeLabel(), + name, }; }; From 6aeda644c8a31fdd58ae43f1f0ea3f1e569476b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Thu, 2 Jul 2020 10:01:10 +0100 Subject: [PATCH 28/34] [APM] Show transaction rate per minute on Observability Overview page (#70336) * changing transaction count to transaction rate per second * sanity check coordinates before calculate the mean * sanity check coordinates before calculate the mean * removing extend_bounds to return empty when no data is available --- .../rest/observability.dashboard.test.ts | 42 ++++++++++++++++++- .../services/rest/observability_dashboard.ts | 9 +++- .../get_transaction_coordinates.ts | 5 ++- 3 files changed, 51 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/apm/public/services/rest/observability.dashboard.test.ts b/x-pack/plugins/apm/public/services/rest/observability.dashboard.test.ts index dbb5d6029d0f1..a14d827eeaec5 100644 --- a/x-pack/plugins/apm/public/services/rest/observability.dashboard.test.ts +++ b/x-pack/plugins/apm/public/services/rest/observability.dashboard.test.ts @@ -58,7 +58,7 @@ describe('Observability dashboard data', () => { transactions: { type: 'number', label: 'Transactions', - value: 6, + value: 2, color: '#6092c0', }, }, @@ -115,5 +115,45 @@ describe('Observability dashboard data', () => { }, }); }); + it('returns transaction stat as 0 when y is undefined', async () => { + callApmApiMock.mockImplementation(() => + Promise.resolve({ + serviceCount: 0, + transactionCoordinates: [{ x: 1 }, { x: 2 }, { x: 3 }], + }) + ); + const response = await fetchLandingPageData( + { + startTime: '1', + endTime: '2', + bucketSize: '3', + }, + { theme } + ); + expect(response).toEqual({ + title: 'APM', + appLink: '/app/apm', + stats: { + services: { + type: 'number', + label: 'Services', + value: 0, + }, + transactions: { + type: 'number', + label: 'Transactions', + value: 0, + color: '#6092c0', + }, + }, + series: { + transactions: { + label: 'Transactions', + coordinates: [{ x: 1 }, { x: 2 }, { x: 3 }], + color: '#6092c0', + }, + }, + }); + }); }); }); diff --git a/x-pack/plugins/apm/public/services/rest/observability_dashboard.ts b/x-pack/plugins/apm/public/services/rest/observability_dashboard.ts index 2107565c5facf..589199221d7a9 100644 --- a/x-pack/plugins/apm/public/services/rest/observability_dashboard.ts +++ b/x-pack/plugins/apm/public/services/rest/observability_dashboard.ts @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { sum } from 'lodash'; +import mean from 'lodash.mean'; import { Theme } from '@kbn/ui-shared-deps/theme'; import { ApmFetchDataResponse, @@ -48,7 +48,12 @@ export const fetchLandingPageData = async ( 'xpack.apm.observabilityDashboard.stats.transactions', { defaultMessage: 'Transactions' } ), - value: sum(transactionCoordinates.map((coordinates) => coordinates.y)), + value: + mean( + transactionCoordinates + .map(({ y }) => y) + .filter((y) => y && isFinite(y)) + ) || 0, color: theme.euiColorVis1, }, }, diff --git a/x-pack/plugins/apm/server/lib/observability_dashboard/get_transaction_coordinates.ts b/x-pack/plugins/apm/server/lib/observability_dashboard/get_transaction_coordinates.ts index e78a3c1cec24a..0d1a4274c16dc 100644 --- a/x-pack/plugins/apm/server/lib/observability_dashboard/get_transaction_coordinates.ts +++ b/x-pack/plugins/apm/server/lib/observability_dashboard/get_transaction_coordinates.ts @@ -41,17 +41,18 @@ export async function getTransactionCoordinates({ field: '@timestamp', fixed_interval: bucketSize, min_doc_count: 0, - extended_bounds: { min: start, max: end }, }, }, }, }, }); + const deltaAsMinutes = (end - start) / 1000 / 60; + return ( aggregations?.distribution.buckets.map((bucket) => ({ x: bucket.key, - y: bucket.doc_count, + y: bucket.doc_count / deltaAsMinutes, })) || [] ); } From 83beede50cb57f012411cc31952692d8cd888d52 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Thu, 2 Jul 2020 11:02:52 +0200 Subject: [PATCH 29/34] [Ingest Pipelines] Error messages (#70167) * improved error messages * traverse recursive error struct * add check for object with keys * update button position and copy * size adjustments * Refactor i18n texts and change wording Also added missing translation and refactored maximum errors in collapsed state to external constant * use io-ts, add CIT and unit tests * refactor error utilities to separate file Co-authored-by: Elastic Machine --- .../__jest__/client_integration/fixtures.ts | 117 ++++++++++++++++++ .../helpers/pipeline_form.helpers.ts | 2 + .../ingest_pipelines_create.test.tsx | 21 ++++ .../pipeline_form/pipeline_form.tsx | 13 +- .../pipeline_form/pipeline_form_error.tsx | 34 ----- .../pipeline_form_error/error_utils.test.ts | 67 ++++++++++ .../pipeline_form_error/error_utils.ts | 85 +++++++++++++ .../pipeline_form_error/i18n_texts.ts | 38 ++++++ .../pipeline_form_error/index.ts | 7 ++ .../pipeline_form_error.tsx | 99 +++++++++++++++ .../server/routes/api/create.ts | 8 +- .../server/routes/api/shared/index.ts | 7 ++ .../routes/api/shared/is_object_with_keys.ts | 9 ++ .../server/routes/api/update.ts | 8 +- 14 files changed, 473 insertions(+), 42 deletions(-) create mode 100644 x-pack/plugins/ingest_pipelines/__jest__/client_integration/fixtures.ts delete mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error/error_utils.test.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error/error_utils.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error/i18n_texts.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error/index.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error/pipeline_form_error.tsx create mode 100644 x-pack/plugins/ingest_pipelines/server/routes/api/shared/index.ts create mode 100644 x-pack/plugins/ingest_pipelines/server/routes/api/shared/is_object_with_keys.ts diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/fixtures.ts b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/fixtures.ts new file mode 100644 index 0000000000000..8dddb2421f03d --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/fixtures.ts @@ -0,0 +1,117 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const nestedProcessorsErrorFixture = { + attributes: { + error: { + root_cause: [ + { + type: 'parse_exception', + reason: '[field] required property is missing', + property_name: 'field', + processor_type: 'circle', + suppressed: [ + { + type: 'parse_exception', + reason: '[field] required property is missing', + property_name: 'field', + processor_type: 'circle', + }, + { + type: 'parse_exception', + reason: '[field] required property is missing', + property_name: 'field', + processor_type: 'circle', + suppressed: [ + { + type: 'parse_exception', + reason: '[field] required property is missing', + property_name: 'field', + processor_type: 'csv', + }, + ], + }, + { + type: 'parse_exception', + reason: '[field] required property is missing', + property_name: 'field', + processor_type: 'circle', + }, + { + type: 'parse_exception', + reason: '[field] required property is missing', + property_name: 'field', + processor_type: 'circle', + }, + { + type: 'parse_exception', + reason: '[field] required property is missing', + property_name: 'field', + processor_type: 'circle', + }, + { + type: 'parse_exception', + reason: '[field] required property is missing', + property_name: 'field', + processor_type: 'circle', + }, + ], + }, + ], + type: 'parse_exception', + reason: '[field] required property is missing', + property_name: 'field', + processor_type: 'circle', + suppressed: [ + { + type: 'parse_exception', + reason: '[field] required property is missing', + property_name: 'field', + processor_type: 'circle', + }, + { + type: 'parse_exception', + reason: '[field] required property is missing', + property_name: 'field', + processor_type: 'circle', + suppressed: [ + { + type: 'parse_exception', + reason: '[field] required property is missing', + property_name: 'field', + processor_type: 'csv', + }, + ], + }, + { + type: 'parse_exception', + reason: '[field] required property is missing', + property_name: 'field', + processor_type: 'circle', + }, + { + type: 'parse_exception', + reason: '[field] required property is missing', + property_name: 'field', + processor_type: 'circle', + }, + { + type: 'parse_exception', + reason: '[field] required property is missing', + property_name: 'field', + processor_type: 'circle', + }, + { + type: 'parse_exception', + reason: '[field] required property is missing', + property_name: 'field', + processor_type: 'circle', + }, + ], + }, + status: 400, + }, +}; diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipeline_form.helpers.ts b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipeline_form.helpers.ts index 8a14ed13f2022..85848b3d2f73c 100644 --- a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipeline_form.helpers.ts +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipeline_form.helpers.ts @@ -42,6 +42,8 @@ export type PipelineFormTestSubjects = | 'submitButton' | 'pageTitle' | 'savePipelineError' + | 'savePipelineError.showErrorsButton' + | 'savePipelineError.hideErrorsButton' | 'pipelineForm' | 'versionToggle' | 'versionField' diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_create.test.tsx b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_create.test.tsx index 2cfccbdc6d578..813057813f139 100644 --- a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_create.test.tsx +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_create.test.tsx @@ -9,6 +9,8 @@ import { act } from 'react-dom/test-utils'; import { setupEnvironment, pageHelpers, nextTick } from './helpers'; import { PipelinesCreateTestBed } from './helpers/pipelines_create.helpers'; +import { nestedProcessorsErrorFixture } from './fixtures'; + const { setup } = pageHelpers.pipelinesCreate; jest.mock('@elastic/eui', () => { @@ -163,6 +165,25 @@ describe('', () => { expect(exists('savePipelineError')).toBe(true); expect(find('savePipelineError').text()).toContain(error.message); }); + + test('displays nested pipeline errors as a flat list', async () => { + const { actions, find, exists, waitFor } = testBed; + httpRequestsMockHelpers.setCreatePipelineResponse(undefined, { + body: nestedProcessorsErrorFixture, + }); + + await act(async () => { + actions.clickSubmitButton(); + await waitFor('savePipelineError'); + }); + + expect(exists('savePipelineError')).toBe(true); + expect(exists('savePipelineError.showErrorsButton')).toBe(true); + find('savePipelineError.showErrorsButton').simulate('click'); + expect(exists('savePipelineError.hideErrorsButton')).toBe(true); + expect(exists('savePipelineError.showErrorsButton')).toBe(false); + expect(find('savePipelineError').find('li').length).toBe(8); + }); }); describe('test pipeline', () => { diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx index 05c9f0a08b0c7..a68e667f4ab43 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx @@ -11,17 +11,18 @@ import { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiSpacer } from import { useForm, Form, FormConfig } from '../../../shared_imports'; import { Pipeline } from '../../../../common/types'; -import { PipelineRequestFlyout } from './pipeline_request_flyout'; -import { PipelineTestFlyout } from './pipeline_test_flyout'; -import { PipelineFormFields } from './pipeline_form_fields'; -import { PipelineFormError } from './pipeline_form_error'; -import { pipelineFormSchema } from './schema'; import { OnUpdateHandlerArg, OnUpdateHandler, SerializeResult, } from '../pipeline_processors_editor'; +import { PipelineRequestFlyout } from './pipeline_request_flyout'; +import { PipelineTestFlyout } from './pipeline_test_flyout'; +import { PipelineFormFields } from './pipeline_form_fields'; +import { PipelineFormError } from './pipeline_form_error'; +import { pipelineFormSchema } from './schema'; + export interface PipelineFormProps { onSave: (pipeline: Pipeline) => void; onCancel: () => void; @@ -116,7 +117,7 @@ export const PipelineForm: React.FunctionComponent = ({ error={form.getErrors()} > {/* Request error */} - {saveError && } + {saveError && } {/* All form fields */} = ({ errorMessage }) => { - return ( - <> - - } - color="danger" - iconType="alert" - data-test-subj="savePipelineError" - > -

{errorMessage}

-
- - - ); -}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error/error_utils.test.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error/error_utils.test.ts new file mode 100644 index 0000000000000..1739365eb197d --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error/error_utils.test.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { toKnownError } from './error_utils'; +import { nestedProcessorsErrorFixture } from '../../../../../__jest__/client_integration/fixtures'; + +describe('toKnownError', () => { + test('undefined, null, numbers, arrays and bad objects', () => { + expect(toKnownError(undefined)).toEqual({ errors: [{ reason: 'An unknown error occurred.' }] }); + expect(toKnownError(null)).toEqual({ errors: [{ reason: 'An unknown error occurred.' }] }); + expect(toKnownError(123)).toEqual({ errors: [{ reason: 'An unknown error occurred.' }] }); + expect(toKnownError([])).toEqual({ errors: [{ reason: 'An unknown error occurred.' }] }); + expect(toKnownError({})).toEqual({ errors: [{ reason: 'An unknown error occurred.' }] }); + expect(toKnownError({ attributes: {} })).toEqual({ + errors: [{ reason: 'An unknown error occurred.' }], + }); + }); + + test('non-processors errors', () => { + expect(toKnownError(new Error('my error'))).toEqual({ errors: [{ reason: 'my error' }] }); + expect(toKnownError({ message: 'my error' })).toEqual({ errors: [{ reason: 'my error' }] }); + }); + + test('processors errors', () => { + expect(toKnownError(nestedProcessorsErrorFixture)).toMatchInlineSnapshot(` + Object { + "errors": Array [ + Object { + "processorType": "circle", + "reason": "[field] required property is missing", + }, + Object { + "processorType": "circle", + "reason": "[field] required property is missing", + }, + Object { + "processorType": "circle", + "reason": "[field] required property is missing", + }, + Object { + "processorType": "csv", + "reason": "[field] required property is missing", + }, + Object { + "processorType": "circle", + "reason": "[field] required property is missing", + }, + Object { + "processorType": "circle", + "reason": "[field] required property is missing", + }, + Object { + "processorType": "circle", + "reason": "[field] required property is missing", + }, + Object { + "processorType": "circle", + "reason": "[field] required property is missing", + }, + ], + } + `); + }); +}); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error/error_utils.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error/error_utils.ts new file mode 100644 index 0000000000000..7f32f962f657c --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error/error_utils.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as t from 'io-ts'; +import { flow } from 'fp-ts/lib/function'; +import { isRight } from 'fp-ts/lib/Either'; + +import { i18nTexts } from './i18n_texts'; + +export interface PipelineError { + reason: string; + processorType?: string; +} +interface PipelineErrors { + errors: PipelineError[]; +} + +interface ErrorNode { + reason: string; + processor_type?: string; + suppressed?: ErrorNode[]; +} + +// This is a runtime type (RT) for an error node which is a recursive type +const errorNodeRT = t.recursion('ErrorNode', (ErrorNode) => + t.intersection([ + t.interface({ + reason: t.string, + }), + t.partial({ + processor_type: t.string, + suppressed: t.array(ErrorNode), + }), + ]) +); + +// This is a runtime type for the attributes object we expect to receive from the server +// for processor errors +const errorAttributesObjectRT = t.interface({ + attributes: t.interface({ + error: t.interface({ + root_cause: t.array(errorNodeRT), + }), + }), +}); + +const isProcessorsError = flow(errorAttributesObjectRT.decode, isRight); + +type ErrorAttributesObject = t.TypeOf; + +const flattenErrorsTree = (node: ErrorNode): PipelineError[] => { + const result: PipelineError[] = []; + const recurse = (_node: ErrorNode) => { + result.push({ reason: _node.reason, processorType: _node.processor_type }); + if (_node.suppressed && Array.isArray(_node.suppressed)) { + _node.suppressed.forEach(recurse); + } + }; + recurse(node); + return result; +}; + +export const toKnownError = (error: unknown): PipelineErrors => { + if (typeof error === 'object' && error != null && isProcessorsError(error)) { + const errorAttributes = error as ErrorAttributesObject; + const rootCause = errorAttributes.attributes.error.root_cause[0]; + return { errors: flattenErrorsTree(rootCause) }; + } + + if (typeof error === 'string') { + return { errors: [{ reason: error }] }; + } + + if ( + error instanceof Error || + (typeof error === 'object' && error != null && (error as any).message) + ) { + return { errors: [{ reason: (error as any).message }] }; + } + + return { errors: [{ reason: i18nTexts.errors.unknownError }] }; +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error/i18n_texts.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error/i18n_texts.ts new file mode 100644 index 0000000000000..e354541db8e7b --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error/i18n_texts.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const i18nTexts = { + title: i18n.translate('xpack.ingestPipelines.form.savePipelineError', { + defaultMessage: 'Unable to create pipeline', + }), + errors: { + processor: (processorType: string) => + i18n.translate('xpack.ingestPipelines.form.savePipelineError.processorLabel', { + defaultMessage: '{type} processor', + values: { type: processorType }, + }), + showErrors: (hiddenErrorsCount: number) => + i18n.translate('xpack.ingestPipelines.form.savePipelineError.showAllButton', { + defaultMessage: + 'Show {hiddenErrorsCount, plural, one {# more error} other {# more errors}}', + values: { + hiddenErrorsCount, + }, + }), + hideErrors: (hiddenErrorsCount: number) => + i18n.translate('xpack.ingestPipelines.form.savePip10mbelineError.showFewerButton', { + defaultMessage: 'Hide {hiddenErrorsCount, plural, one {# error} other {# errors}}', + values: { + hiddenErrorsCount, + }, + }), + unknownError: i18n.translate('xpack.ingestPipelines.form.unknownError', { + defaultMessage: 'An unknown error occurred.', + }), + }, +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error/index.ts new file mode 100644 index 0000000000000..656691f639498 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { PipelineFormError } from './pipeline_form_error'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error/pipeline_form_error.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error/pipeline_form_error.tsx new file mode 100644 index 0000000000000..23fb9a1648434 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error/pipeline_form_error.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState, useEffect } from 'react'; + +import { EuiSpacer, EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiButtonEmpty } from '@elastic/eui'; +import { useKibana } from '../../../../shared_imports'; + +import { i18nTexts } from './i18n_texts'; +import { toKnownError, PipelineError } from './error_utils'; + +interface Props { + error: unknown; +} + +const numberOfErrorsToDisplay = 5; + +export const PipelineFormError: React.FunctionComponent = ({ error }) => { + const { services } = useKibana(); + const [isShowingAllErrors, setIsShowingAllErrors] = useState(false); + const safeErrorResult = toKnownError(error); + const hasMoreErrors = safeErrorResult.errors.length > numberOfErrorsToDisplay; + const hiddenErrorsCount = safeErrorResult.errors.length - numberOfErrorsToDisplay; + const results = isShowingAllErrors + ? safeErrorResult.errors + : safeErrorResult.errors.slice(0, numberOfErrorsToDisplay); + + const renderErrorListItem = ({ processorType, reason }: PipelineError) => { + return ( + <> + {processorType ? <>{i18nTexts.errors.processor(processorType) + ':'}  : undefined} + {reason} + + ); + }; + + useEffect(() => { + services.notifications.toasts.addDanger({ title: i18nTexts.title }); + }, [services, error]); + return ( + <> + + {results.length > 1 ? ( +
    + {results.map((e, idx) => ( +
  • {renderErrorListItem(e)}
  • + ))} +
+ ) : ( + renderErrorListItem(results[0]) + )} + {hasMoreErrors ? ( + + + {isShowingAllErrors ? ( + setIsShowingAllErrors(false)} + color="danger" + iconSide="right" + iconType="arrowUp" + data-test-subj="hideErrorsButton" + > + {i18nTexts.errors.hideErrors(hiddenErrorsCount)} + + ) : ( + setIsShowingAllErrors(true)} + color="danger" + iconSide="right" + iconType="arrowDown" + data-test-subj="showErrorsButton" + > + {i18nTexts.errors.showErrors(hiddenErrorsCount)} + + )} + + + ) : undefined} +
+ + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/create.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/create.ts index c1ab3852ee784..c2328bcc9d0ab 100644 --- a/x-pack/plugins/ingest_pipelines/server/routes/api/create.ts +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/create.ts @@ -10,6 +10,7 @@ import { Pipeline } from '../../../common/types'; import { API_BASE_PATH } from '../../../common/constants'; import { RouteDependencies } from '../../types'; import { pipelineSchema } from './pipeline_schema'; +import { isObjectWithKeys } from './shared'; const bodySchema = schema.object({ name: schema.string(), @@ -70,7 +71,12 @@ export const registerCreateRoute = ({ if (isEsError(error)) { return res.customError({ statusCode: error.statusCode, - body: error, + body: isObjectWithKeys(error.body) + ? { + message: error.message, + attributes: error.body, + } + : error, }); } diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/shared/index.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/shared/index.ts new file mode 100644 index 0000000000000..1fa794a4fb996 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/shared/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { isObjectWithKeys } from './is_object_with_keys'; diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/shared/is_object_with_keys.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/shared/is_object_with_keys.ts new file mode 100644 index 0000000000000..0617bde26cfb6 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/shared/is_object_with_keys.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; + * you may not use this file except in compliance with the Elastic License. + */ + +export const isObjectWithKeys = (value: unknown) => { + return typeof value === 'object' && !!value && Object.keys(value).length > 0; +}; diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/update.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/update.ts index 214b293a43c6c..cd0e3568f0f60 100644 --- a/x-pack/plugins/ingest_pipelines/server/routes/api/update.ts +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/update.ts @@ -8,6 +8,7 @@ import { schema } from '@kbn/config-schema'; import { API_BASE_PATH } from '../../../common/constants'; import { RouteDependencies } from '../../types'; import { pipelineSchema } from './pipeline_schema'; +import { isObjectWithKeys } from './shared'; const bodySchema = schema.object(pipelineSchema); @@ -52,7 +53,12 @@ export const registerUpdateRoute = ({ if (isEsError(error)) { return res.customError({ statusCode: error.statusCode, - body: error, + body: isObjectWithKeys(error.body) + ? { + message: error.message, + attributes: error.body, + } + : error, }); } From a0e263038c6c7cf077386a1a3da18d6fc6b92def Mon Sep 17 00:00:00 2001 From: Rudolf Meijering Date: Thu, 2 Jul 2020 11:24:39 +0200 Subject: [PATCH 30/34] Use dynamic: false for config saved object mappings (#70436) --- src/core/server/ui_settings/saved_objects/ui_settings.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/core/server/ui_settings/saved_objects/ui_settings.ts b/src/core/server/ui_settings/saved_objects/ui_settings.ts index 26704f46a509c..452d1954b6e23 100644 --- a/src/core/server/ui_settings/saved_objects/ui_settings.ts +++ b/src/core/server/ui_settings/saved_objects/ui_settings.ts @@ -25,10 +25,7 @@ export const uiSettingsType: SavedObjectsType = { hidden: false, namespaceType: 'single', mappings: { - // we don't want to allow `true` in the public `SavedObjectsTypeMappingDefinition` type, however - // this is needed for the config that is kinda a special type. To avoid adding additional internal types - // just for this, we hardcast to any here. - dynamic: true as any, + dynamic: false, properties: { buildNum: { type: 'keyword', From a8347fad1c9c6ef47436cb947154f935615ea1d5 Mon Sep 17 00:00:00 2001 From: Daniil Suleiman <31325372+sulemanof@users.noreply.github.com> Date: Thu, 2 Jul 2020 13:24:00 +0300 Subject: [PATCH 31/34] [Visualize] Add missing advanced settings and custom label for pipeline aggs (#69688) * Show advanced settings in pipeline aggs * Add functional tests * Make sub metric in sibling pipeline to have custom label Co-authored-by: Elastic Machine --- .../public/components/agg_group.tsx | 2 +- .../public/components/agg_params_helper.ts | 6 +- .../public/components/controls/sub_metric.tsx | 7 +- test/functional/apps/visualize/_line_chart.js | 74 +++++++++++++++++++ 4 files changed, 84 insertions(+), 5 deletions(-) diff --git a/src/plugins/vis_default_editor/public/components/agg_group.tsx b/src/plugins/vis_default_editor/public/components/agg_group.tsx index 3030601236687..4cde33b8fbc31 100644 --- a/src/plugins/vis_default_editor/public/components/agg_group.tsx +++ b/src/plugins/vis_default_editor/public/components/agg_group.tsx @@ -152,7 +152,7 @@ function DefaultEditorAggGroup({ {bucketsError && ( <> - {bucketsError} + {bucketsError} )} diff --git a/src/plugins/vis_default_editor/public/components/agg_params_helper.ts b/src/plugins/vis_default_editor/public/components/agg_params_helper.ts index 45abbf8d2b2dd..39abddb3de853 100644 --- a/src/plugins/vis_default_editor/public/components/agg_params_helper.ts +++ b/src/plugins/vis_default_editor/public/components/agg_params_helper.ts @@ -111,7 +111,11 @@ function getAggParamsToRender({ const aggType = agg.type.type; const aggName = agg.type.name; const aggParams = get(aggParamsMap, [aggType, aggName], {}); - paramEditor = get(aggParams, param.name) || get(aggParamsMap, ['common', param.type]); + paramEditor = get(aggParams, param.name); + } + + if (!paramEditor) { + paramEditor = get(aggParamsMap, ['common', param.type]); } // show params with an editor component diff --git a/src/plugins/vis_default_editor/public/components/controls/sub_metric.tsx b/src/plugins/vis_default_editor/public/components/controls/sub_metric.tsx index 361eeba9abdbf..fc79ba703c2b4 100644 --- a/src/plugins/vis_default_editor/public/components/controls/sub_metric.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/sub_metric.tsx @@ -45,9 +45,10 @@ function SubMetricParamEditor({ defaultMessage: 'Bucket', }); const type = aggParam.name; + const isCustomMetric = type === 'customMetric'; - const aggTitle = type === 'customMetric' ? metricTitle : bucketTitle; - const aggGroup = type === 'customMetric' ? AggGroupNames.Metrics : AggGroupNames.Buckets; + const aggTitle = isCustomMetric ? metricTitle : bucketTitle; + const aggGroup = isCustomMetric ? AggGroupNames.Metrics : AggGroupNames.Buckets; useMount(() => { if (agg.params[type]) { @@ -87,7 +88,7 @@ function SubMetricParamEditor({ setValidity={setValidity} setTouched={setTouched} schemas={schemas} - hideCustomLabel={true} + hideCustomLabel={!isCustomMetric} /> ); diff --git a/test/functional/apps/visualize/_line_chart.js b/test/functional/apps/visualize/_line_chart.js index 5c510617fbb01..a492f3858b524 100644 --- a/test/functional/apps/visualize/_line_chart.js +++ b/test/functional/apps/visualize/_line_chart.js @@ -279,5 +279,79 @@ export default function ({ getService, getPageObjects }) { expect(labels).to.eql(expectedLabels); }); }); + + describe('pipeline aggregations', () => { + before(async () => { + log.debug('navigateToApp visualize'); + await PageObjects.visualize.navigateToNewVisualization(); + log.debug('clickLineChart'); + await PageObjects.visualize.clickLineChart(); + await PageObjects.visualize.clickNewSearch(); + await PageObjects.timePicker.setDefaultAbsoluteRange(); + }); + + describe('parent pipeline', () => { + it('should have an error if bucket is not selected', async () => { + await PageObjects.visEditor.clickMetricEditor(); + log.debug('Metrics agg = Serial diff'); + await PageObjects.visEditor.selectAggregation('Serial diff', 'metrics'); + await testSubjects.existOrFail('bucketsError'); + }); + + it('should apply with selected bucket', async () => { + log.debug('Bucket = X-axis'); + await PageObjects.visEditor.clickBucket('X-axis'); + log.debug('Aggregation = Date Histogram'); + await PageObjects.visEditor.selectAggregation('Date Histogram'); + await PageObjects.visEditor.clickGo(); + const title = await PageObjects.visChart.getYAxisTitle(); + expect(title).to.be('Serial Diff of Count'); + }); + + it('should change y-axis label to custom', async () => { + log.debug('set custom label of y-axis to "Custom"'); + await PageObjects.visEditor.setCustomLabel('Custom', 1); + await PageObjects.visEditor.clickGo(); + const title = await PageObjects.visChart.getYAxisTitle(); + expect(title).to.be('Custom'); + }); + + it('should have advanced accordion and json input', async () => { + await testSubjects.click('advancedParams-1'); + await testSubjects.existOrFail('advancedParams-1 > codeEditorContainer'); + }); + }); + + describe('sibling pipeline', () => { + it('should apply with selected bucket', async () => { + log.debug('Metrics agg = Average Bucket'); + await PageObjects.visEditor.selectAggregation('Average Bucket', 'metrics'); + await PageObjects.visEditor.clickGo(); + const title = await PageObjects.visChart.getYAxisTitle(); + expect(title).to.be('Overall Average of Count'); + }); + + it('should change sub metric custom label and calculate y-axis title', async () => { + log.debug('set custom label of sub metric to "Cats"'); + await PageObjects.visEditor.setCustomLabel('Cats', '1-metric'); + await PageObjects.visEditor.clickGo(); + const title = await PageObjects.visChart.getYAxisTitle(); + expect(title).to.be('Overall Average of Cats'); + }); + + it('should outer custom label', async () => { + log.debug('set custom label to "Custom"'); + await PageObjects.visEditor.setCustomLabel('Custom', 1); + await PageObjects.visEditor.clickGo(); + const title = await PageObjects.visChart.getYAxisTitle(); + expect(title).to.be('Custom'); + }); + + it('should have advanced accordion and json input', async () => { + await testSubjects.click('advancedParams-1'); + await testSubjects.existOrFail('advancedParams-1 > codeEditorContainer'); + }); + }); + }); }); } From 1cfc9356bd36c54b8005089decc36970c11c101d Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Thu, 2 Jul 2020 13:17:33 +0200 Subject: [PATCH 32/34] add getVisibleTypes API to SO type registry (#70559) * add getVisibleTypes API * doc nit * fix mocking in tests --- ...ver.savedobjecttyperegistry.getalltypes.md | 4 ++- ...savedobjecttyperegistry.getvisibletypes.md | 19 ++++++++++++ ...gin-core-server.savedobjecttyperegistry.md | 3 +- .../saved_objects_type_registry.mock.ts | 2 ++ .../saved_objects_type_registry.test.ts | 29 ++++++++++++++++++- .../saved_objects_type_registry.ts | 13 ++++++++- src/core/server/server.api.md | 1 + .../saved_objects/saved_objects_mixin.js | 2 +- x-pack/plugins/features/server/plugin.test.ts | 8 +---- x-pack/plugins/features/server/plugin.ts | 5 +--- 10 files changed, 70 insertions(+), 16 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getvisibletypes.md diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getalltypes.md b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getalltypes.md index 1e0e89767c4e6..c839dd16d9a47 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getalltypes.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getalltypes.md @@ -4,7 +4,9 @@ ## SavedObjectTypeRegistry.getAllTypes() method -Return all [types](./kibana-plugin-core-server.savedobjectstype.md) currently registered. +Return all [types](./kibana-plugin-core-server.savedobjectstype.md) currently registered, including the hidden ones. + +To only get the visible types (which is the most common use case), use `getVisibleTypes` instead. Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getvisibletypes.md b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getvisibletypes.md new file mode 100644 index 0000000000000..a773c6a0a674f --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getvisibletypes.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectTypeRegistry](./kibana-plugin-core-server.savedobjecttyperegistry.md) > [getVisibleTypes](./kibana-plugin-core-server.savedobjecttyperegistry.getvisibletypes.md) + +## SavedObjectTypeRegistry.getVisibleTypes() method + +Returns all visible [types](./kibana-plugin-core-server.savedobjectstype.md). + +A visible type is a type that doesn't explicitly define `hidden=true` during registration. + +Signature: + +```typescript +getVisibleTypes(): SavedObjectsType[]; +``` +Returns: + +`SavedObjectsType[]` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.md b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.md index 69a94e4ad8c88..55ad7ca137de0 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.md @@ -16,10 +16,11 @@ export declare class SavedObjectTypeRegistry | Method | Modifiers | Description | | --- | --- | --- | -| [getAllTypes()](./kibana-plugin-core-server.savedobjecttyperegistry.getalltypes.md) | | Return all [types](./kibana-plugin-core-server.savedobjectstype.md) currently registered. | +| [getAllTypes()](./kibana-plugin-core-server.savedobjecttyperegistry.getalltypes.md) | | Return all [types](./kibana-plugin-core-server.savedobjectstype.md) currently registered, including the hidden ones.To only get the visible types (which is the most common use case), use getVisibleTypes instead. | | [getImportableAndExportableTypes()](./kibana-plugin-core-server.savedobjecttyperegistry.getimportableandexportabletypes.md) | | Return all [types](./kibana-plugin-core-server.savedobjectstype.md) currently registered that are importable/exportable. | | [getIndex(type)](./kibana-plugin-core-server.savedobjecttyperegistry.getindex.md) | | Returns the indexPattern property for given type, or undefined if the type is not registered. | | [getType(type)](./kibana-plugin-core-server.savedobjecttyperegistry.gettype.md) | | Return the [type](./kibana-plugin-core-server.savedobjectstype.md) definition for given type name. | +| [getVisibleTypes()](./kibana-plugin-core-server.savedobjecttyperegistry.getvisibletypes.md) | | Returns all visible [types](./kibana-plugin-core-server.savedobjectstype.md).A visible type is a type that doesn't explicitly define hidden=true during registration. | | [isHidden(type)](./kibana-plugin-core-server.savedobjecttyperegistry.ishidden.md) | | Returns the hidden property for given type, or false if the type is not registered. | | [isImportableAndExportable(type)](./kibana-plugin-core-server.savedobjecttyperegistry.isimportableandexportable.md) | | Returns the management.importableAndExportable property for given type, or false if the type is not registered or does not define a management section. | | [isMultiNamespace(type)](./kibana-plugin-core-server.savedobjecttyperegistry.ismultinamespace.md) | | Returns whether the type is multi-namespace (shareable); resolves to false if the type is not registered | diff --git a/src/core/server/saved_objects/saved_objects_type_registry.mock.ts b/src/core/server/saved_objects/saved_objects_type_registry.mock.ts index 5636dcadb444e..44490228490cc 100644 --- a/src/core/server/saved_objects/saved_objects_type_registry.mock.ts +++ b/src/core/server/saved_objects/saved_objects_type_registry.mock.ts @@ -25,6 +25,7 @@ const createRegistryMock = (): jest.Mocked< const mock = { registerType: jest.fn(), getType: jest.fn(), + getVisibleTypes: jest.fn(), getAllTypes: jest.fn(), getImportableAndExportableTypes: jest.fn(), isNamespaceAgnostic: jest.fn(), @@ -35,6 +36,7 @@ const createRegistryMock = (): jest.Mocked< isImportableAndExportable: jest.fn(), }; + mock.getVisibleTypes.mockReturnValue([]); mock.getAllTypes.mockReturnValue([]); mock.getImportableAndExportableTypes.mockReturnValue([]); mock.getIndex.mockReturnValue('.kibana-test'); diff --git a/src/core/server/saved_objects/saved_objects_type_registry.test.ts b/src/core/server/saved_objects/saved_objects_type_registry.test.ts index e0f4d6fa28e50..25c94324c8f01 100644 --- a/src/core/server/saved_objects/saved_objects_type_registry.test.ts +++ b/src/core/server/saved_objects/saved_objects_type_registry.test.ts @@ -99,10 +99,37 @@ describe('SavedObjectTypeRegistry', () => { }); }); + describe('#getVisibleTypes', () => { + it('returns only visible registered types', () => { + const typeA = createType({ name: 'typeA', hidden: false }); + const typeB = createType({ name: 'typeB', hidden: true }); + const typeC = createType({ name: 'typeC', hidden: false }); + registry.registerType(typeA); + registry.registerType(typeB); + registry.registerType(typeC); + + const registered = registry.getVisibleTypes(); + expect(registered.length).toEqual(2); + expect(registered).toContainEqual(typeA); + expect(registered).toContainEqual(typeC); + }); + + it('does not mutate the registered types when altering the list', () => { + registry.registerType(createType({ name: 'typeA', hidden: false })); + registry.registerType(createType({ name: 'typeB', hidden: true })); + registry.registerType(createType({ name: 'typeC', hidden: false })); + + const types = registry.getVisibleTypes(); + types.splice(0, 2); + + expect(registry.getVisibleTypes().length).toEqual(2); + }); + }); + describe('#getAllTypes', () => { it('returns all registered types', () => { const typeA = createType({ name: 'typeA' }); - const typeB = createType({ name: 'typeB' }); + const typeB = createType({ name: 'typeB', hidden: true }); const typeC = createType({ name: 'typeC' }); registry.registerType(typeA); registry.registerType(typeB); diff --git a/src/core/server/saved_objects/saved_objects_type_registry.ts b/src/core/server/saved_objects/saved_objects_type_registry.ts index 99262d7a31e21..d0035294226ea 100644 --- a/src/core/server/saved_objects/saved_objects_type_registry.ts +++ b/src/core/server/saved_objects/saved_objects_type_registry.ts @@ -54,7 +54,18 @@ export class SavedObjectTypeRegistry { } /** - * Return all {@link SavedObjectsType | types} currently registered. + * Returns all visible {@link SavedObjectsType | types}. + * + * A visible type is a type that doesn't explicitly define `hidden=true` during registration. + */ + public getVisibleTypes() { + return [...this.types.values()].filter((type) => !this.isHidden(type.name)); + } + + /** + * Return all {@link SavedObjectsType | types} currently registered, including the hidden ones. + * + * To only get the visible types (which is the most common use case), use `getVisibleTypes` instead. */ public getAllTypes() { return [...this.types.values()]; diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 9cc5a8a386b0b..1cabaa57e519c 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -2468,6 +2468,7 @@ export class SavedObjectTypeRegistry { getImportableAndExportableTypes(): SavedObjectsType[]; getIndex(type: string): string | undefined; getType(type: string): SavedObjectsType | undefined; + getVisibleTypes(): SavedObjectsType[]; isHidden(type: string): boolean; isImportableAndExportable(type: string): boolean; isMultiNamespace(type: string): boolean; diff --git a/src/legacy/server/saved_objects/saved_objects_mixin.js b/src/legacy/server/saved_objects/saved_objects_mixin.js index 63839b9d0f1d7..185c8807ae8b5 100644 --- a/src/legacy/server/saved_objects/saved_objects_mixin.js +++ b/src/legacy/server/saved_objects/saved_objects_mixin.js @@ -34,8 +34,8 @@ export function savedObjectsMixin(kbnServer, server) { const typeRegistry = kbnServer.newPlatform.start.core.savedObjects.getTypeRegistry(); const mappings = migrator.getActiveMappings(); const allTypes = typeRegistry.getAllTypes().map((t) => t.name); + const visibleTypes = typeRegistry.getVisibleTypes().map((t) => t.name); const schema = new SavedObjectsSchema(convertTypesToLegacySchema(typeRegistry.getAllTypes())); - const visibleTypes = allTypes.filter((type) => !schema.isHiddenType(type)); server.decorate('server', 'kibanaMigrator', migrator); diff --git a/x-pack/plugins/features/server/plugin.test.ts b/x-pack/plugins/features/server/plugin.test.ts index 79fd012337b00..3d85c2e9eb751 100644 --- a/x-pack/plugins/features/server/plugin.test.ts +++ b/x-pack/plugins/features/server/plugin.test.ts @@ -10,19 +10,13 @@ const initContext = coreMock.createPluginInitializerContext(); const coreSetup = coreMock.createSetup(); const coreStart = coreMock.createStart(); const typeRegistry = savedObjectsServiceMock.createTypeRegistryMock(); -typeRegistry.getAllTypes.mockReturnValue([ +typeRegistry.getVisibleTypes.mockReturnValue([ { name: 'foo', hidden: false, mappings: { properties: {} }, namespaceType: 'single' as 'single', }, - { - name: 'bar', - hidden: true, - mappings: { properties: {} }, - namespaceType: 'agnostic' as 'agnostic', - }, ]); coreStart.savedObjects.getTypeRegistry.mockReturnValue(typeRegistry); diff --git a/x-pack/plugins/features/server/plugin.ts b/x-pack/plugins/features/server/plugin.ts index 149c1acfb5086..5783b20eae648 100644 --- a/x-pack/plugins/features/server/plugin.ts +++ b/x-pack/plugins/features/server/plugin.ts @@ -80,10 +80,7 @@ export class Plugin { private registerOssFeatures(savedObjects: SavedObjectsServiceStart) { const registry = savedObjects.getTypeRegistry(); - const savedObjectTypes = registry - .getAllTypes() - .filter((t) => !t.hidden) - .map((t) => t.name); + const savedObjectTypes = registry.getVisibleTypes().map((t) => t.name); this.logger.debug( `Registering OSS features with SO types: ${savedObjectTypes.join(', ')}. "includeTimelion": ${ From 7d63cafd5d15ce5e85ba551472f6565f361b39d8 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Thu, 2 Jul 2020 12:31:51 +0100 Subject: [PATCH 33/34] chore(NA): disable alerts_detection_rules cypress suites (#70577) --- .../cypress/integration/alerts_detection_rules_custom.spec.ts | 3 ++- .../cypress/integration/alerts_detection_rules_export.spec.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts index 9e9732a403f8f..2a1a2d2c8e194 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts @@ -64,7 +64,8 @@ import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; import { ALERTS_URL } from '../urls/navigation'; -describe('Detection rules, custom', () => { +// // Skipped as was causing failures on master +describe.skip('Detection rules, custom', () => { before(() => { esArchiverLoad('custom_rule_with_timeline'); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_export.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_export.spec.ts index 25fc1fc3a7c11..06e9228de4f49 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_export.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_export.spec.ts @@ -17,7 +17,8 @@ import { ALERTS_URL } from '../urls/navigation'; const EXPECTED_EXPORTED_RULE_FILE_PATH = 'cypress/test_files/expected_rules_export.ndjson'; -describe('Export rules', () => { +// Skipped as was causing failures on master +describe.skip('Export rules', () => { before(() => { esArchiverLoad('custom_rules'); cy.server(); From 7b74094e0fa33bd4803dbe105b2a4ea3843ef170 Mon Sep 17 00:00:00 2001 From: Eric Davis Date: Thu, 2 Jul 2020 07:55:52 -0400 Subject: [PATCH 34/34] Update docs for api authentication usage (#66819) Co-authored-by: Kaarina Tungseth --- docs/api/using-api.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api/using-api.asciidoc b/docs/api/using-api.asciidoc index aba65f2e921c2..e58d9c39ee8c4 100644 --- a/docs/api/using-api.asciidoc +++ b/docs/api/using-api.asciidoc @@ -10,7 +10,7 @@ NOTE: The {kib} Console supports only Elasticsearch APIs. You are unable to inte [float] [[api-authentication]] === Authentication -{kib} supports token-based authentication with the same username and password that you use to log into the {kib} Console. +{kib} supports token-based authentication with the same username and password that you use to log into the {kib} Console. In a given HTTP tool, and when available, you can select to use its 'Basic Authentication' option, which is where the username and password are stored in order to be passed as part of the call. [float] [[api-calls]]