From bfc0b87f8257ec993b4b6898235e945329126d24 Mon Sep 17 00:00:00 2001 From: meikel Date: Thu, 6 Feb 2025 14:59:24 +0100 Subject: [PATCH 1/5] feat(cdp): add ip anon transformation --- .../ip-anonymization.template.test.ts | 97 +++++++++++++++++++ .../ip-anonymization.template.ts | 47 +++++++++ plugin-server/src/cdp/templates/index.ts | 2 + 3 files changed, 146 insertions(+) create mode 100644 plugin-server/src/cdp/templates/_transformations/ip-anonymization/ip-anonymization.template.test.ts create mode 100644 plugin-server/src/cdp/templates/_transformations/ip-anonymization/ip-anonymization.template.ts diff --git a/plugin-server/src/cdp/templates/_transformations/ip-anonymization/ip-anonymization.template.test.ts b/plugin-server/src/cdp/templates/_transformations/ip-anonymization/ip-anonymization.template.test.ts new file mode 100644 index 0000000000000..c702af330a650 --- /dev/null +++ b/plugin-server/src/cdp/templates/_transformations/ip-anonymization/ip-anonymization.template.test.ts @@ -0,0 +1,97 @@ +import { HogFunctionInvocationGlobals } from '../../../types' +import { TemplateTester } from '../../test/test-helpers' +import { template } from './ip-anonymization.template' + +describe('ip-anonymization.template', () => { + const tester = new TemplateTester(template) + let mockGlobals: HogFunctionInvocationGlobals + + beforeEach(async () => { + await tester.beforeEach() + }) + + it('should anonymize IPv4 address by zeroing last octet', async () => { + mockGlobals = tester.createGlobals({ + event: { + properties: { + $ip: '89.160.20.129', + }, + }, + }) + + const response = await tester.invoke({}, mockGlobals) + + expect(response.finished).toBe(true) + expect(response.error).toBeUndefined() + expect(response.execResult).toMatchObject({ + properties: { + $ip: '89.160.20.0', + }, + }) + }) + + it('should handle event with no IP address', async () => { + mockGlobals = tester.createGlobals({ + event: { + properties: {}, + }, + }) + + const response = await tester.invoke({}, mockGlobals) + + expect(response.finished).toBe(true) + expect(response.error).toBeUndefined() + expect(response.execResult).toMatchObject({ + properties: {}, + }) + }) + + it('should handle invalid IP address format', async () => { + mockGlobals = tester.createGlobals({ + event: { + properties: { + $ip: '89.160.20', // Invalid IP - missing octet + }, + }, + }) + + const response = await tester.invoke({}, mockGlobals) + + expect(response.finished).toBe(true) + expect(response.error).toBeUndefined() + // Should return unchanged event when IP format is invalid + expect(response.execResult).toMatchObject({ + properties: { + $ip: '89.160.20', + }, + }) + }) + + it('should handle various IPv4 formats', async () => { + const testCases = [ + { input: '192.168.1.1', expected: '192.168.1.0' }, + { input: '10.0.0.255', expected: '10.0.0.0' }, + { input: '172.16.254.1', expected: '172.16.254.0' }, + ] + + for (const testCase of testCases) { + mockGlobals = tester.createGlobals({ + event: { + properties: { + $ip: testCase.input, + }, + }, + }) + + const response = await tester.invoke({}, mockGlobals) + + expect(response.finished).toBe(true) + expect(response.error).toBeUndefined() + expect(response.execResult).toMatchObject({ + properties: { + $ip: testCase.expected, + }, + }) + } + }) +}) diff --git a/plugin-server/src/cdp/templates/_transformations/ip-anonymization/ip-anonymization.template.ts b/plugin-server/src/cdp/templates/_transformations/ip-anonymization/ip-anonymization.template.ts new file mode 100644 index 0000000000000..b391eaa2e8794 --- /dev/null +++ b/plugin-server/src/cdp/templates/_transformations/ip-anonymization/ip-anonymization.template.ts @@ -0,0 +1,47 @@ +import { HogFunctionTemplate } from '../../types' + +export const template: HogFunctionTemplate = { + free: true, + status: 'alpha', + type: 'transformation', + id: 'template-ip-anonymization', + name: 'IP Anonymization', + description: + 'This transformation sets the last octet of an IP address to zero (e.g., 12.214.31.144 → 12.214.31.0), protecting user privacy and reducing disclosure risk.', + icon_url: '/static/hedgehog/builder-hog-01.png', + category: ['Custom'], + hog: ` +// Check if the event has an IP address +if (empty(event.properties?.$ip)) { + print('No IP address found in event') + return event +} + +let ip := event.properties.$ip +let parts := splitByString('.', ip) + +// Check if we have a valid IPv4 address +if (length(parts) = 4) { + // Replace the last octet with '0' + let anonymizedIp := concat( + parts[1], + '.', + parts[2], + '.', + parts[3], + '.0' + ) + + let returnEvent := event + returnEvent.properties.$ip := anonymizedIp + + print('Anonymized IP', ip, '->', anonymizedIp) + return returnEvent +} + +// If we don't have a valid IPv4, return original event +print('Invalid IPv4 address format:', ip) +return event + `, + inputs_schema: [], +} diff --git a/plugin-server/src/cdp/templates/index.ts b/plugin-server/src/cdp/templates/index.ts index 9038b57969157..d130c79c2eb74 100644 --- a/plugin-server/src/cdp/templates/index.ts +++ b/plugin-server/src/cdp/templates/index.ts @@ -2,6 +2,7 @@ import { DESTINATION_PLUGINS, TRANSFORMATION_PLUGINS } from '../legacy-plugins' import { template as webhookTemplate } from './_destinations/webhook/webhook.template' import { template as defaultTransformationTemplate } from './_transformations/default/default.template' import { template as geoipTemplate } from './_transformations/geoip/geoip.template' +import { template as ipAnonymizationTemplate } from './_transformations/ip-anonymization/ip-anonymization.template' import { HogFunctionTemplate } from './types' export const HOG_FUNCTION_TEMPLATES_DESTINATIONS: HogFunctionTemplate[] = [webhookTemplate] @@ -9,6 +10,7 @@ export const HOG_FUNCTION_TEMPLATES_DESTINATIONS: HogFunctionTemplate[] = [webho export const HOG_FUNCTION_TEMPLATES_TRANSFORMATIONS: HogFunctionTemplate[] = [ defaultTransformationTemplate, geoipTemplate, + ipAnonymizationTemplate, ] export const HOG_FUNCTION_TEMPLATES_DESTINATIONS_DEPRECATED: HogFunctionTemplate[] = DESTINATION_PLUGINS.map( From 2350ead9f0ceb1c50ec359ebba21553182d0c056 Mon Sep 17 00:00:00 2001 From: meikel Date: Thu, 6 Feb 2025 15:12:43 +0100 Subject: [PATCH 2/5] remove print statements --- .../ip-anonymization/ip-anonymization.template.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/plugin-server/src/cdp/templates/_transformations/ip-anonymization/ip-anonymization.template.ts b/plugin-server/src/cdp/templates/_transformations/ip-anonymization/ip-anonymization.template.ts index b391eaa2e8794..09d03bea39cb8 100644 --- a/plugin-server/src/cdp/templates/_transformations/ip-anonymization/ip-anonymization.template.ts +++ b/plugin-server/src/cdp/templates/_transformations/ip-anonymization/ip-anonymization.template.ts @@ -34,13 +34,10 @@ if (length(parts) = 4) { let returnEvent := event returnEvent.properties.$ip := anonymizedIp - - print('Anonymized IP', ip, '->', anonymizedIp) return returnEvent } // If we don't have a valid IPv4, return original event -print('Invalid IPv4 address format:', ip) return event `, inputs_schema: [], From eda6b8ebfd5475f079b63b67da8bc5b3fee742bc Mon Sep 17 00:00:00 2001 From: Meikel Ratz Date: Thu, 6 Feb 2025 15:19:04 +0100 Subject: [PATCH 3/5] Update plugin-server/src/cdp/templates/_transformations/ip-anonymization/ip-anonymization.template.test.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- .../ip-anonymization.template.test.ts | 40 ++++++++++++------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/plugin-server/src/cdp/templates/_transformations/ip-anonymization/ip-anonymization.template.test.ts b/plugin-server/src/cdp/templates/_transformations/ip-anonymization/ip-anonymization.template.test.ts index c702af330a650..0a69657658268 100644 --- a/plugin-server/src/cdp/templates/_transformations/ip-anonymization/ip-anonymization.template.test.ts +++ b/plugin-server/src/cdp/templates/_transformations/ip-anonymization/ip-anonymization.template.test.ts @@ -47,24 +47,34 @@ describe('ip-anonymization.template', () => { }) it('should handle invalid IP address format', async () => { - mockGlobals = tester.createGlobals({ - event: { - properties: { - $ip: '89.160.20', // Invalid IP - missing octet + const invalidIPs = [ + '89.160.20', // missing octet + '', // empty string + 'abc.def.ghi.jkl', // non-numeric + '256.256.256.256', // values > 255 + '1.2.3.4.5', // too many octets + 'not an ip' + ] + + for (const invalidIP of invalidIPs) { + mockGlobals = tester.createGlobals({ + event: { + properties: { + $ip: invalidIP, + }, }, - }, - }) + }) - const response = await tester.invoke({}, mockGlobals) + const response = await tester.invoke({}, mockGlobals) - expect(response.finished).toBe(true) - expect(response.error).toBeUndefined() - // Should return unchanged event when IP format is invalid - expect(response.execResult).toMatchObject({ - properties: { - $ip: '89.160.20', - }, - }) + expect(response.finished).toBe(true) + expect(response.error).toBeUndefined() + expect(response.execResult).toMatchObject({ + properties: { + $ip: invalidIP, // should return unchanged + }, + }) + } }) it('should handle various IPv4 formats', async () => { From 0283e72d6a3857f684874c46a374c49b64edd88a Mon Sep 17 00:00:00 2001 From: meikel Date: Thu, 6 Feb 2025 15:39:46 +0100 Subject: [PATCH 4/5] catch edge cases --- .../ip-anonymization.template.ts | 36 ++++++++++--------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/plugin-server/src/cdp/templates/_transformations/ip-anonymization/ip-anonymization.template.ts b/plugin-server/src/cdp/templates/_transformations/ip-anonymization/ip-anonymization.template.ts index 09d03bea39cb8..54cee73e617f1 100644 --- a/plugin-server/src/cdp/templates/_transformations/ip-anonymization/ip-anonymization.template.ts +++ b/plugin-server/src/cdp/templates/_transformations/ip-anonymization/ip-anonymization.template.ts @@ -20,25 +20,27 @@ if (empty(event.properties?.$ip)) { let ip := event.properties.$ip let parts := splitByString('.', ip) -// Check if we have a valid IPv4 address -if (length(parts) = 4) { - // Replace the last octet with '0' - let anonymizedIp := concat( - parts[1], - '.', - parts[2], - '.', - parts[3], - '.0' - ) - - let returnEvent := event - returnEvent.properties.$ip := anonymizedIp - return returnEvent +// Check if we have exactly 4 parts for IPv4 +if (length(parts) != 4) { + print('Invalid IP address format: wrong number of octets') + return event } -// If we don't have a valid IPv4, return original event -return event +// Validate each octet is a number between 0 and 255 +for (let i := 1; i <= 4; i := i + 1) { + let octet := toInt(parts[i]) + if (octet = null or octet < 0 or octet > 255) { + print('Invalid IP address: octets must be numbers between 0 and 255') + return event + } +} + +// Replace the last octet with '0' +let anonymizedIp := concat(parts[1], '.', parts[2], '.', parts[3], '.0') + +let returnEvent := event +returnEvent.properties.$ip := anonymizedIp +return returnEvent `, inputs_schema: [], } From 76d48fbb43ed5895d94acf83da158daded5abe46 Mon Sep 17 00:00:00 2001 From: meikel Date: Thu, 6 Feb 2025 16:13:47 +0100 Subject: [PATCH 5/5] prettier duh --- .../ip-anonymization.template.test.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/plugin-server/src/cdp/templates/_transformations/ip-anonymization/ip-anonymization.template.test.ts b/plugin-server/src/cdp/templates/_transformations/ip-anonymization/ip-anonymization.template.test.ts index 0a69657658268..fd945574f7ac9 100644 --- a/plugin-server/src/cdp/templates/_transformations/ip-anonymization/ip-anonymization.template.test.ts +++ b/plugin-server/src/cdp/templates/_transformations/ip-anonymization/ip-anonymization.template.test.ts @@ -48,12 +48,12 @@ describe('ip-anonymization.template', () => { it('should handle invalid IP address format', async () => { const invalidIPs = [ - '89.160.20', // missing octet - '', // empty string - 'abc.def.ghi.jkl', // non-numeric - '256.256.256.256', // values > 255 - '1.2.3.4.5', // too many octets - 'not an ip' + '89.160.20', // missing octet + '', // empty string + 'abc.def.ghi.jkl', // non-numeric + '256.256.256.256', // values > 255 + '1.2.3.4.5', // too many octets + 'not an ip', ] for (const invalidIP of invalidIPs) { @@ -71,7 +71,7 @@ describe('ip-anonymization.template', () => { expect(response.error).toBeUndefined() expect(response.execResult).toMatchObject({ properties: { - $ip: invalidIP, // should return unchanged + $ip: invalidIP, }, }) }