Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cdp): add ip anon transformation #28377

Merged
merged 5 commits into from
Feb 6, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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',
},
})
})
meikelmosby marked this conversation as resolved.
Show resolved Hide resolved

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' },
]
meikelmosby marked this conversation as resolved.
Show resolved Hide resolved

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,
},
})
}
})
})
Original file line number Diff line number Diff line change
@@ -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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: No validation of IP address octets. Values like '999.999.999.999' or 'a.b.c.d' will be processed as valid IPs if they contain 4 parts.

Suggested change
let parts := splitByString('.', ip)
let parts := splitByString('.', ip)
// Validate each octet is a number between 0-255
if (length(parts) = 4 and all(x -> toInt(x) >= 0 and toInt(x) <= 255, parts)) {

Comment on lines +20 to +21
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Consider trimming the IP string to handle whitespace in input


// 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)
meikelmosby marked this conversation as resolved.
Show resolved Hide resolved
return event
`,
inputs_schema: [],
}
2 changes: 2 additions & 0 deletions plugin-server/src/cdp/templates/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ 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]

export const HOG_FUNCTION_TEMPLATES_TRANSFORMATIONS: HogFunctionTemplate[] = [
defaultTransformationTemplate,
geoipTemplate,
ipAnonymizationTemplate,
]

export const HOG_FUNCTION_TEMPLATES_DESTINATIONS_DEPRECATED: HogFunctionTemplate[] = DESTINATION_PLUGINS.map(
Expand Down
Loading