feat(core-shared): truncateIp helper (/24 IPv4, /48 IPv6) per DPA
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
37
packages/core-shared/src/audit/truncate-ip.test.ts
Normal file
37
packages/core-shared/src/audit/truncate-ip.test.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { truncateIp } from "./truncate-ip";
|
||||
|
||||
describe("truncateIp", () => {
|
||||
describe("IPv4", () => {
|
||||
it("truncates to /24 (zeros the last octet)", () => {
|
||||
expect(truncateIp("192.168.1.42")).toBe("192.168.1.0");
|
||||
expect(truncateIp("10.0.0.255")).toBe("10.0.0.0");
|
||||
expect(truncateIp("8.8.8.8")).toBe("8.8.8.0");
|
||||
});
|
||||
|
||||
it("throws on malformed input", () => {
|
||||
expect(() => truncateIp("192.168.1")).toThrow(/malformed IPv4/);
|
||||
expect(() => truncateIp("192.168.1.foo")).toThrow(/malformed IPv4/);
|
||||
expect(() => truncateIp("a.b.c.d")).toThrow(/malformed IPv4/);
|
||||
});
|
||||
|
||||
it("throws on empty string", () => {
|
||||
expect(() => truncateIp("")).toThrow(/malformed IPv4/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("IPv6", () => {
|
||||
it("truncates to /48 (keeps first 3 hextets)", () => {
|
||||
expect(truncateIp("2001:0db8:1234:5678:abcd:ef00:1234:5678")).toBe("2001:0db8:1234::");
|
||||
expect(truncateIp("2001:0db8:abcd:1234::")).toBe("2001:0db8:abcd::");
|
||||
});
|
||||
|
||||
it("lowercases hextets", () => {
|
||||
expect(truncateIp("2001:0DB8:ABCD:5678::")).toBe("2001:0db8:abcd::");
|
||||
});
|
||||
|
||||
it("throws on too-few hextets", () => {
|
||||
expect(() => truncateIp("2001:0db8")).toThrow(/malformed IPv6/);
|
||||
});
|
||||
});
|
||||
});
|
||||
27
packages/core-shared/src/audit/truncate-ip.ts
Normal file
27
packages/core-shared/src/audit/truncate-ip.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Truncates an IP address per DPA:
|
||||
* IPv4 → /24 ("192.168.1.42" → "192.168.1.0")
|
||||
* IPv6 → /48 ("2001:0db8:1234:5678:..." → "2001:0db8:1234::")
|
||||
*
|
||||
* Throws on malformed input rather than silently returning the raw value —
|
||||
* compliance regimes prefer hard failures over partial scrubbing.
|
||||
*/
|
||||
export function truncateIp(raw: string): string {
|
||||
if (raw.includes(":")) {
|
||||
// IPv6: keep first 3 hextets (48 bits)
|
||||
const parts = raw.toLowerCase().split(":").filter((p) => p !== "");
|
||||
if (parts.length < 3) {
|
||||
throw new Error(`truncateIp: malformed IPv6 address "${raw}"`);
|
||||
}
|
||||
return `${parts[0]}:${parts[1]}:${parts[2]}::`;
|
||||
}
|
||||
// IPv4: keep first 3 octets (24 bits)
|
||||
const parts = raw.split(".");
|
||||
if (
|
||||
parts.length !== 4 ||
|
||||
parts.some((p) => p === "" || isNaN(Number(p)) || !/^\d+$/.test(p))
|
||||
) {
|
||||
throw new Error(`truncateIp: malformed IPv4 address "${raw}"`);
|
||||
}
|
||||
return `${parts[0]}.${parts[1]}.${parts[2]}.0`;
|
||||
}
|
||||
Reference in New Issue
Block a user