diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/community_id.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/community_id.test.tsx new file mode 100644 index 00000000000000..0338cb8e04348a --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/community_id.test.tsx @@ -0,0 +1,109 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act } from 'react-dom/test-utils'; +import { setup, SetupResult, getProcessorValue } from './processor.helpers'; + +const COMMUNITY_ID_TYPE = 'community_id'; + +describe('Processor: Community id', () => { + let onUpdate: jest.Mock; + let testBed: SetupResult; + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + beforeEach(async () => { + onUpdate = jest.fn(); + + await act(async () => { + testBed = await setup({ + value: { + processors: [], + }, + onFlyoutOpen: jest.fn(), + onUpdate, + }); + }); + + testBed.component.update(); + + // Open flyout to add new processor + testBed.actions.addProcessor(); + // Add type (the other fields are not visible until a type is selected) + await testBed.actions.addProcessorType(COMMUNITY_ID_TYPE); + }); + + test('can submit if no fields are filled', async () => { + const { + actions: { saveNewProcessor }, + form, + } = testBed; + + // Click submit button with no fields filled + await saveNewProcessor(); + + // Expect no form errors + expect(form.getErrorsMessages()).toHaveLength(0); + }); + + test('allows to set either iana_number or transport', async () => { + const { find, form } = testBed; + + expect(find('ianaField.input').exists()).toBe(true); + expect(find('transportField.input').exists()).toBe(true); + + form.setInputValue('ianaField.input', 'iana_number'); + expect(find('transportField.input').props().disabled).toBe(true); + + form.setInputValue('ianaField.input', ''); + form.setInputValue('transportField.input', 'transport'); + expect(find('ianaField.input').props().disabled).toBe(true); + }); + + test('allows optional parameters to be set', async () => { + const { + actions: { saveNewProcessor }, + form, + } = testBed; + + form.toggleEuiSwitch('ignoreMissingSwitch.input'); + form.toggleEuiSwitch('ignoreFailureSwitch.input'); + form.setInputValue('sourceIpField.input', 'source.ip'); + form.setInputValue('sourcePortField.input', 'source.port'); + form.setInputValue('targetField.input', 'target_field'); + form.setInputValue('destinationIpField.input', 'destination.ip'); + form.setInputValue('destinationPortField.input', 'destination.port'); + form.setInputValue('icmpTypeField.input', 'icmp_type'); + form.setInputValue('icmpCodeField.input', 'icmp_code'); + form.setInputValue('ianaField.input', 'iana'); + form.setInputValue('seedField.input', '10'); + + // Save the field with new changes + await saveNewProcessor(); + + const processors = getProcessorValue(onUpdate, COMMUNITY_ID_TYPE); + expect(processors[0][COMMUNITY_ID_TYPE]).toEqual({ + ignore_failure: true, + ignore_missing: false, + target_field: 'target_field', + source_ip: 'source.ip', + source_port: 'source.port', + destination_ip: 'destination.ip', + destination_port: 'destination.port', + icmp_type: 'icmp_type', + icmp_code: 'icmp_code', + iana_number: 'iana', + seed: 10, + }); + }); +}); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/processor.helpers.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/processor.helpers.tsx index e4024e4ec67f46..183777ca765b43 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/processor.helpers.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/processor.helpers.tsx @@ -180,4 +180,13 @@ type TestSubject = | 'fieldsValueField.input' | 'saltValueField.input' | 'methodsValueField' + | 'sourceIpField.input' + | 'sourcePortField.input' + | 'destinationIpField.input' + | 'destinationPortField.input' + | 'icmpTypeField.input' + | 'icmpCodeField.input' + | 'ianaField.input' + | 'transportField.input' + | 'seedField.input' | 'trimSwitch.input'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/community_id.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/community_id.tsx new file mode 100644 index 00000000000000..cd6f97d0a299e4 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/community_id.tsx @@ -0,0 +1,307 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiSpacer, EuiCode, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +import { FieldsConfig, from } from './shared'; +import { TargetField } from './common_fields/target_field'; +import { IgnoreMissingField } from './common_fields/ignore_missing_field'; +import { + Field, + UseField, + useFormData, + FIELD_TYPES, + NumericField, + SerializerFunc, + fieldFormatters, + fieldValidators, +} from '../../../../../../shared_imports'; + +const SEED_MIN_VALUE = 0; +const SEED_MAX_VALUE = 65535; + +const seedValidator = { + max: fieldValidators.numberSmallerThanField({ + than: SEED_MAX_VALUE, + allowEquality: true, + message: i18n.translate('xpack.ingestPipelines.pipelineEditor.communityId.seedMaxNumberError', { + defaultMessage: `This number must be equal or less than {maxValue}.`, + values: { maxValue: SEED_MAX_VALUE }, + }), + }), + min: fieldValidators.numberGreaterThanField({ + than: SEED_MIN_VALUE, + allowEquality: true, + message: i18n.translate('xpack.ingestPipelines.pipelineEditor.communityId.seedMinNumberError', { + defaultMessage: `This number must be equal or greater than {minValue}.`, + values: { minValue: SEED_MIN_VALUE }, + }), + }), +}; + +const fieldsConfig: FieldsConfig = { + source_ip: { + type: FIELD_TYPES.TEXT, + serializer: from.emptyStringToUndefined, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.communityId.sourceIpLabel', { + defaultMessage: 'Source IP (optional)', + }), + helpText: ( + {'source.ip'} }} + /> + ), + }, + source_port: { + type: FIELD_TYPES.TEXT, + serializer: from.emptyStringToUndefined, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.communityId.sourcePortLabel', { + defaultMessage: 'Source port (optional)', + }), + helpText: ( + {'source.port'} }} + /> + ), + }, + destination_ip: { + type: FIELD_TYPES.TEXT, + serializer: from.emptyStringToUndefined, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.communityId.destinationIpLabel', { + defaultMessage: 'Destination IP (optional)', + }), + helpText: ( + {'destination.ip'} }} + /> + ), + }, + destination_port: { + type: FIELD_TYPES.TEXT, + serializer: from.emptyStringToUndefined, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.communityId.destinationPortLabel', { + defaultMessage: 'Destination port (optional)', + }), + helpText: ( + {'destination.port'} }} + /> + ), + }, + icmp_type: { + type: FIELD_TYPES.TEXT, + serializer: from.emptyStringToUndefined, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.communityId.icmpTypeLabel', { + defaultMessage: 'ICMP type (optional)', + }), + helpText: ( + {'icmp.type'} }} + /> + ), + }, + icmp_code: { + type: FIELD_TYPES.TEXT, + serializer: from.emptyStringToUndefined, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.communityId.icmpCodeLabel', { + defaultMessage: 'ICMP code (optional)', + }), + helpText: ( + {'icmp.code'} }} + /> + ), + }, + iana_number: { + type: FIELD_TYPES.TEXT, + serializer: from.emptyStringToUndefined, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.communityId.ianaLabel', { + defaultMessage: 'IANA number (optional)', + }), + helpText: ( + {'Transport'}, + defaultValue: {'network.iana_number'}, + }} + /> + ), + }, + transport: { + type: FIELD_TYPES.TEXT, + serializer: from.emptyStringToUndefined, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.communityId.transportLabel', { + defaultMessage: 'Transport (optional)', + }), + helpText: ( + {'IANA number'}, + defaultValue: {'network.transport'}, + }} + /> + ), + }, + seed: { + type: FIELD_TYPES.NUMBER, + formatters: [fieldFormatters.toInt], + serializer: from.undefinedIfValue(''), + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.communityId.seedLabel', { + defaultMessage: 'Seed (optional)', + }), + helpText: ( + {'0'} }} + /> + ), + validations: [ + { + validator: (field) => { + if (field.value) { + return seedValidator.max(field) ?? seedValidator.min(field); + } + }, + }, + ], + }, +}; + +export const CommunityId: FunctionComponent = () => { + const [{ fields }] = useFormData({ watch: ['fields.iana_number', 'fields.transport'] }); + + return ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {'network.community_id'}, + }} + /> + } + /> + + } + /> + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/index.ts index f5eb1ab3ec59be..1a2422b40d0b01 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/index.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/index.ts @@ -8,6 +8,7 @@ export { Append } from './append'; export { Bytes } from './bytes'; export { Circle } from './circle'; +export { CommunityId } from './community_id'; export { Convert } from './convert'; export { CSV } from './csv'; export { DateProcessor } from './date'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/shared/map_processor_type_to_form.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/shared/map_processor_type_to_form.tsx index e6ca465bf1a022..2a7067be512aef 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/shared/map_processor_type_to_form.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/shared/map_processor_type_to_form.tsx @@ -14,6 +14,7 @@ import { Append, Bytes, Circle, + CommunityId, Convert, CSV, DateProcessor, @@ -126,6 +127,20 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = { }, }), }, + community_id: { + FieldsComponent: CommunityId, + docLinkPath: '/community-id-processor.html', + label: i18n.translate('xpack.ingestPipelines.processors.label.communityId', { + defaultMessage: 'Community ID', + }), + typeDescription: i18n.translate('xpack.ingestPipelines.processors.description.communityId', { + defaultMessage: 'Computes the Community ID for network flow data.', + }), + getDefaultDescription: () => + i18n.translate('xpack.ingestPipelines.processors.defaultDescription.communityId', { + defaultMessage: 'Computes the Community ID for network flow data.', + }), + }, convert: { FieldsComponent: Convert, docLinkPath: '/convert-processor.html',