From 212f5a22c17d16790003464c80c9b74fae91f29e Mon Sep 17 00:00:00 2001 From: Sonia Sanz Vivas Date: Wed, 29 Apr 2026 16:46:39 +0200 Subject: [PATCH] [Ingest pipelines] Fix Ip location processor bug (#265740) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes https://github.com/elastic/kibana/issues/257233 ## Summary - When an IP Location processor used a local database whose filename coincidentally matched a known managed database display name (e.g. `ASN.mmdb` colliding with the IPinfo "ASN" label), the pipeline editor produced the wrong `database_file` on save and visually selected both the local and the IPinfo entries in the combo box at the same time. - The root cause was that deserialization and serialization of `database_file` relied solely on the display label string (e.g. `"ASN"`) to identify databases, making local and managed entries indistinguishable when their labels collided. - The fix gives local databases a distinct combo box label using their full filename including the `.mmdb` extension (e.g. `"ASN.mmdb"`). The deserializer now returns the full filename for unrecognised databases (local ones) so that `selectedOptions` matches the correct entry. The serializer skips the managed-database lookup when the value already ends in `.mmdb`, returning it as-is. A `normalizeMmdbFilename` helper is added to `utils.ts` (with a guard against double extension, e.g. `ASN.mmdb.mmdb`) and a `getDatabaseOptionLabel` helper centralises the label logic. Both are covered by new unit tests in `utils.test.ts`. ### Test plan 1. Upload a local database named `ASN.mmdb` as a cluster extension (place in `.es//config/ingest-geoip/`). You can use: [ASN.mmdb.zip](https://github.com/user-attachments/files/27123526/ASN.mmdb.zip). 2. From Stack Management → Ingest Pipelines → Manage processors, create a ASN database. Confirm both `ASN.mmdb` (Local) and `ASN` (IPinfo) appear in the list. 3. Create (or open) an ingest pipeline with two IP Location processors: - First processor: `database_file: "ASN.mmdb"` (the local one) - Second processor: `database_file: "standard_asn.mmdb"` (the IPinfo managed one) 4. Open each processor in the editor: - The first should show `ASN.mmdb` selected under the Local group only — no tick on the IPinfo entry. - The second should show `ASN` selected under the IPinfo group only — no tick on the Local entry. 5. Save the pipeline without changes and inspect the request preview — values must round-trip correctly: - First processor: `"database_file": "ASN.mmdb"` - Second processor: `"database_file": "standard_asn.mmdb"` ### Before/after notes ### Evidence (screenshots / screen recording) **Before** Screenshot 2026-04-27 at 12 30 08 Screenshot 2026-04-27 at 12 31 08 **After** Screenshot 2026-04-27 at 12 30 32 Screenshot 2026-04-27 at 12 32 25 --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> (cherry picked from commit 19f4b1c5dbf458d69fba2bf121c8c59db2cd0719) --- .../processor_form/processors/ip_location.tsx | 30 +++++++++---- .../sections/manage_processors/utils.test.ts | 42 ++++++++++++++++++- .../sections/manage_processors/utils.ts | 29 +++++++++++++ 3 files changed, 91 insertions(+), 10 deletions(-) diff --git a/x-pack/platform/plugins/shared/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/ip_location.tsx b/x-pack/platform/plugins/shared/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/ip_location.tsx index 5993d73f980a7..316b1449ef463 100644 --- a/x-pack/platform/plugins/shared/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/ip_location.tsx +++ b/x-pack/platform/plugins/shared/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/ip_location.tsx @@ -27,26 +27,38 @@ import { from, to } from './shared'; import { TargetField } from './common_fields/target_field'; import { PropertiesField } from './common_fields/properties_field'; import type { GeoipDatabase } from '../../../../../../../common/types'; -import { getDatabaseText, getDatabaseValue } from '../../../../../sections/manage_processors/utils'; +import { + getDatabaseOptionLabel, + getDatabaseText, + getDatabaseValue, + normalizeMmdbFilename, + MMDB_EXTENSION, +} from '../../../../../sections/manage_processors/utils'; import { getTypeLabel } from '../../../../../sections/manage_processors/constants'; -const extension = '.mmdb'; - const fieldsConfig: FieldsConfig = { /* Optional field config */ database_file: { type: FIELD_TYPES.COMBO_BOX, deserializer: (v: unknown) => to.arrayOfStrings(v).map((str) => { - const databaseName = str?.split(extension)[0]; - // Use the translated text for this database, if it exists - return getDatabaseText(databaseName) ?? databaseName; + const databaseName = str.split(MMDB_EXTENSION)[0]; + const knownDatabaseText = getDatabaseText(databaseName); + // Known managed DB → return display text (e.g. "ASN" for standard_asn) + // Local DB → return full filename (e.g. "ASN.mmdb") to match the combo box label + return knownDatabaseText ?? str; }), serializer: (v: any[]) => { if (v.length) { const databaseName = v[0]; + // Local databases have the extension already in the label + if (typeof databaseName === 'string' && databaseName.endsWith(MMDB_EXTENSION)) { + return normalizeMmdbFilename(databaseName); + } const databaseValue = getDatabaseValue(databaseName); - return databaseValue ? `${databaseValue}${extension}` : `${databaseName}${extension}`; + return databaseValue + ? `${databaseValue}${MMDB_EXTENSION}` + : `${databaseName}${MMDB_EXTENSION}`; } return undefined; }, @@ -92,8 +104,8 @@ export const IpLocation: FunctionComponent = () => { const dataAsOptions = (data || []).map((item) => ({ id: item.id, type: item.type, - // Use the translated text for this database, if it exists - label: getDatabaseText(item.name) ?? item.name, + // Use the name of the database file for local databases and the translated text for others, if it exists + label: getDatabaseOptionLabel(item), })); const optionsByGroup = groupBy(dataAsOptions, 'type'); const groupedOptions = map(optionsByGroup, (items, groupName) => ({ diff --git a/x-pack/platform/plugins/shared/ingest_pipelines/public/application/sections/manage_processors/utils.test.ts b/x-pack/platform/plugins/shared/ingest_pipelines/public/application/sections/manage_processors/utils.test.ts index c5b12a95bc539..4667924cf2361 100644 --- a/x-pack/platform/plugins/shared/ingest_pipelines/public/application/sections/manage_processors/utils.test.ts +++ b/x-pack/platform/plugins/shared/ingest_pipelines/public/application/sections/manage_processors/utils.test.ts @@ -5,7 +5,12 @@ * 2.0. */ -import { getDatabaseValue, getDatabaseText } from './utils'; +import { + getDatabaseOptionLabel, + getDatabaseText, + getDatabaseValue, + normalizeMmdbFilename, +} from './utils'; describe('getDatabaseValue', () => { it('should return the value for a given database text for maxmind', () => { @@ -58,3 +63,38 @@ describe('getDatabaseText', () => { expect(result).toBe('IP Geolocation'); }); }); + +describe('normalizeMmdbFilename', () => { + it('should add .mmdb when missing', () => { + expect(normalizeMmdbFilename('GeoLite2-City')).toBe('GeoLite2-City.mmdb'); + }); + + it('should keep a single .mmdb suffix', () => { + expect(normalizeMmdbFilename('GeoLite2-City.mmdb')).toBe('GeoLite2-City.mmdb'); + }); + + it('should collapse repeated .mmdb suffixes', () => { + expect(normalizeMmdbFilename('GeoLite2-City.mmdb.mmdb')).toBe('GeoLite2-City.mmdb'); + }); +}); + +describe('getDatabaseOptionLabel', () => { + it('should return the database filename for local databases', () => { + expect(getDatabaseOptionLabel({ type: 'local', name: 'GeoLite2-City' })).toBe( + 'GeoLite2-City.mmdb' + ); + expect(getDatabaseOptionLabel({ type: 'local', name: 'GeoLite2-City.mmdb' })).toBe( + 'GeoLite2-City.mmdb' + ); + }); + + it('should return translated text for known managed databases', () => { + expect(getDatabaseOptionLabel({ type: 'maxmind', name: 'standard_asn' })).toBe('ASN'); + }); + + it('should fallback to name when no translation exists', () => { + expect(getDatabaseOptionLabel({ type: 'maxmind', name: 'something_else' })).toBe( + 'something_else' + ); + }); +}); diff --git a/x-pack/platform/plugins/shared/ingest_pipelines/public/application/sections/manage_processors/utils.ts b/x-pack/platform/plugins/shared/ingest_pipelines/public/application/sections/manage_processors/utils.ts index 8ac853bbac17c..6387c3eb40786 100644 --- a/x-pack/platform/plugins/shared/ingest_pipelines/public/application/sections/manage_processors/utils.ts +++ b/x-pack/platform/plugins/shared/ingest_pipelines/public/application/sections/manage_processors/utils.ts @@ -8,6 +8,9 @@ import type { DatabaseType, DatabaseNameOption } from '../../../../common/types'; import { GEOIP_NAME_OPTIONS, IPINFO_NAME_OPTIONS } from './constants'; +export const MMDB_EXTENSION = '.mmdb'; +const mmdbSuffixRegExp = /(\.mmdb)+$/; + const getDatabaseNameOptions = (type?: DatabaseType): DatabaseNameOption[] => { switch (type) { case 'maxmind': @@ -42,3 +45,29 @@ export const getDatabaseText = (databaseValue: string, type?: DatabaseType): str const options = getDatabaseNameOptions(type); return options.find((opt) => opt.value === databaseValue)?.text; }; + +/** + * Returns the normalized filename of the database. + * @param name The name of the database + * @returns The normalized filename of the database + */ +export const normalizeMmdbFilename = (name: string) => { + if (name.endsWith(MMDB_EXTENSION)) { + return name.replace(mmdbSuffixRegExp, MMDB_EXTENSION); + } + + return `${name}${MMDB_EXTENSION}`; +}; + +/** + * Returns the label of the database, if it exists. + * @param item The database item + * @returns The label of the database + */ +export const getDatabaseOptionLabel = (item: { type: DatabaseType; name: string }) => { + if (item.type === 'local') { + return normalizeMmdbFilename(item.name); + } + + return getDatabaseText(item.name) ?? item.name; +};