diff --git a/README.md b/README.md index e82486d..70ff7a5 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,20 @@ const name = PII("Thomas") const lowercaseName = tap(n => console.log(n), name) // Logs "Thomas" ``` +#### Detecting PII in Data + +Recurses through a data structure and uses a callback to detect values that should become PII. + +```typescript +import { PII, detect } from "@tdreyno/pii" + +const person = { name: "Thomas" } +const lowercaseName = detect( + data => isObject(person) && Object.keys().some(k => k.includes(name)), + person, +) // Returns PII({ name: "Thomas" }) +``` + #### Custom PII Redaction ```typescript diff --git a/src/__tests__/detect.spec.ts b/src/__tests__/detect.spec.ts new file mode 100644 index 0000000..5916b32 --- /dev/null +++ b/src/__tests__/detect.spec.ts @@ -0,0 +1,28 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { detect, isPII } from "../index" + +const detector = (data: unknown) => Array.isArray(data) + +describe("detect", () => { + it("should detect variables", () => { + expect(detect(detector, "test")).toBe("test") + expect(isPII(detect(detector, []))).toBeTruthy() + }) + + it("should handle Map", () => { + const map = new Map([ + ["a", 1], + ["b", []], + ]) + + const detectedArrays = detect(detector, { test: map }) + expect(isPII((detectedArrays as any).test.get("b"))).toBeTruthy() + }) + + it("should handle Set", () => { + const set = new Set([[]]) + + const detectedArrays = detect(detector, { test: set }) + expect(isPII(Array.from((detectedArrays as any).test)[0])).toBeTruthy() + }) +}) diff --git a/src/pii.ts b/src/pii.ts index 8586108..f77bd0e 100644 --- a/src/pii.ts +++ b/src/pii.ts @@ -7,11 +7,11 @@ export interface PII { } // eslint-disable-next-line @typescript-eslint/no-explicit-any -const isPIIType = (val: any): val is PII => +export const isPII = (val: any): val is PII => isRecord(val) && val.__brand === "PII" export const PII = (val: T, msg = "REDACTED"): PII => - isPIIType(val) + isPII(val) ? val : ({ __brand: "PII", @@ -24,7 +24,7 @@ export function unwrap(item: PII): Exclude> export function unwrap(item: T): Exclude> export function unwrap(item: T | PII): Exclude> { // eslint-disable-next-line @typescript-eslint/no-explicit-any - return isPIIType(item) + return isPII(item) ? (item as any)[ "__fire_me_if_you_see_me_accessing_this_property_outside_pii_ts" ] @@ -135,7 +135,7 @@ export const visitPII = ( } export const containsPII = (input: unknown): boolean => - isPIIType(input) + isPII(input) ? true : visitPII(input, { record: o => Object.values(o).some(containsPII), @@ -143,12 +143,12 @@ export const containsPII = (input: unknown): boolean => Array.from(m).some(([k, v]) => containsPII(k) || containsPII(v)), array: a => a.some(containsPII), set: s => Array.from(s).some(containsPII), - primitive: p => isPIIType(p), - object: p => isPIIType(p), + primitive: p => isPII(p), + object: p => isPII(p), }) export const unwrapObject = (input: unknown): unknown => - visitPII(isPIIType(input) ? unwrap(input) : input, { + visitPII(isPII(input) ? unwrap(input) : input, { record: o => Object.keys(o).reduce((sum, key) => { sum[key] = unwrapObject(o[key]) @@ -165,7 +165,7 @@ export const unwrapObject = (input: unknown): unknown => }) export const redact = (redactor: (data: any) => any, input: unknown): unknown => - visitPII(isPIIType(input) ? redactor(input) : input, { + visitPII(isPII(input) ? redactor(input) : input, { record: o => Object.keys(o).reduce((sum, key) => { sum[key] = redact(redactor, o[key]) @@ -183,3 +183,30 @@ export const redact = (redactor: (data: any) => any, input: unknown): unknown => primitive: p => p, object: p => p, }) + +export const detect = ( + detector: (data: any) => boolean, + input: unknown, +): unknown => + isPII(input) + ? input + : detector(input) + ? PII(input) + : visitPII(input, { + record: o => + Object.keys(o).reduce((sum, key) => { + sum[key] = detect(detector, o[key]) + return sum + }, {} as Record), + map: m => + new Map( + Array.from(m).map(([k, v]) => [ + detect(detector, k), + detect(detector, v), + ]), + ), + array: a => a.map(x => detect(detector, x)), + set: s => new Set(Array.from(s).map(x => detect(detector, x))), + primitive: p => p, + object: p => p, + })