@ogcio/o11y-sdk-node 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/dist/lib/exporter/pii-exporter-decorator.d.ts +3 -1
- package/dist/lib/exporter/pii-exporter-decorator.js +33 -17
- package/dist/lib/index.d.ts +5 -0
- package/dist/lib/instrumentation.node.js +0 -8
- package/dist/lib/internals/redaction/pii-detection.d.ts +23 -0
- package/dist/lib/internals/redaction/pii-detection.js +87 -0
- package/dist/lib/internals/redaction/redactors/email.d.ts +8 -0
- package/dist/lib/internals/redaction/redactors/email.js +48 -0
- package/dist/lib/internals/redaction/redactors/index.d.ts +4 -0
- package/dist/lib/internals/redaction/redactors/index.js +6 -0
- package/dist/lib/internals/redaction/redactors/ip.d.ts +10 -0
- package/dist/lib/internals/redaction/redactors/ip.js +54 -0
- package/dist/lib/processor/enrich-logger-processor.d.ts +2 -2
- package/dist/package.json +14 -14
- package/dist/vitest.config.js +4 -4
- package/lib/exporter/pii-exporter-decorator.ts +56 -24
- package/lib/index.ts +5 -0
- package/lib/instrumentation.node.ts +0 -10
- package/lib/internals/redaction/pii-detection.ts +126 -0
- package/lib/internals/redaction/redactors/email.ts +58 -0
- package/lib/internals/redaction/redactors/index.ts +12 -0
- package/lib/internals/redaction/redactors/ip.ts +68 -0
- package/lib/internals/shared-metrics.ts +1 -1
- package/lib/processor/enrich-logger-processor.ts +2 -2
- package/package.json +14 -14
- package/test/exporter/pii-exporter-decorator.test.ts +75 -126
- package/test/internals/pii-detection.test.ts +27 -25
- package/test/internals/redactors/email.test.ts +81 -0
- package/test/internals/redactors/ip.test.ts +89 -0
- package/test/traces/active-span.test.ts +1 -1
- package/vitest.config.ts +4 -4
- package/dist/lib/internals/pii-detection.d.ts +0 -17
- package/dist/lib/internals/pii-detection.js +0 -116
- package/lib/internals/pii-detection.ts +0 -145
|
@@ -1,139 +1,88 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import { ReadableLogRecord } from "@opentelemetry/sdk-logs";
|
|
2
|
+
import { ResourceMetrics } from "@opentelemetry/sdk-metrics";
|
|
3
|
+
import { ReadableSpan } from "@opentelemetry/sdk-trace-base";
|
|
3
4
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
-
import
|
|
5
|
-
import { PIIExporterDecorator } from "../../lib/exporter/pii-exporter-decorator.js";
|
|
6
|
-
import * as pii from "../../lib/internals/pii-detection.js";
|
|
7
|
-
|
|
8
|
-
const mockExporter = {
|
|
9
|
-
export: vi.fn(),
|
|
10
|
-
forceFlush: vi.fn().mockResolvedValue(undefined),
|
|
11
|
-
shutdown: vi.fn().mockResolvedValue(undefined),
|
|
12
|
-
_delegate: {}, // required by OTLPExporterBase
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
const mockCallback = vi.fn();
|
|
16
|
-
|
|
17
|
-
const baseSpan = (): ReadableSpan =>
|
|
18
|
-
({
|
|
19
|
-
name: "john.doe@example.com",
|
|
20
|
-
kind: 1,
|
|
21
|
-
spanContext: () => ({ traceId: "abc", spanId: "def" }),
|
|
22
|
-
attributes: { email: "john.doe@example.com" },
|
|
23
|
-
resource: {
|
|
24
|
-
attributes: { team: "dev@example.com" },
|
|
25
|
-
otherProp: "shouldBeLost",
|
|
26
|
-
},
|
|
27
|
-
links: [{ attributes: { collaborator: "co@example.com" } }],
|
|
28
|
-
events: [{ name: "event@email.com", attributes: { code: "123" } }],
|
|
29
|
-
}) as any;
|
|
30
|
-
|
|
31
|
-
const baseLog = (): ReadableLogRecord =>
|
|
32
|
-
({
|
|
33
|
-
body: "user: john.doe@example.com",
|
|
34
|
-
attributes: { email: "john.doe@example.com" },
|
|
35
|
-
resource: {
|
|
36
|
-
attributes: { service: "log@example.com" },
|
|
37
|
-
otherProp: "shouldBeLost",
|
|
38
|
-
},
|
|
39
|
-
severityText: "INFO",
|
|
40
|
-
severityNumber: 1,
|
|
41
|
-
}) as any;
|
|
42
|
-
|
|
43
|
-
vi.mock("../../lib/internals/pii-detection.js", async () => {
|
|
44
|
-
return {
|
|
45
|
-
_cleanStringPII: vi.fn((v: string) => `[REDACTED(${v})]`),
|
|
46
|
-
_cleanObjectPII: vi.fn((obj: any) =>
|
|
47
|
-
Object.fromEntries(
|
|
48
|
-
Object.entries(obj || {}).map(([k, v]) => [k, `[REDACTED(${v})]`]),
|
|
49
|
-
),
|
|
50
|
-
),
|
|
51
|
-
_cleanLogBodyPII: vi.fn((v: any) => `[REDACTED_BODY(${v})]`),
|
|
52
|
-
};
|
|
53
|
-
});
|
|
5
|
+
import { PIIExporterDecorator } from "../../lib/exporter/pii-exporter-decorator";
|
|
54
6
|
|
|
55
7
|
describe("PIIExporterDecorator", () => {
|
|
56
|
-
let
|
|
8
|
+
let exporterMock: any;
|
|
9
|
+
let config: any;
|
|
10
|
+
let piiExporter: PIIExporterDecorator;
|
|
57
11
|
|
|
58
12
|
beforeEach(() => {
|
|
59
|
-
|
|
13
|
+
exporterMock = {
|
|
14
|
+
export: vi.fn(),
|
|
15
|
+
shutdown: vi.fn(() => Promise.resolve()),
|
|
16
|
+
forceFlush: vi.fn(() => Promise.resolve()),
|
|
17
|
+
_delegate: {},
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
config = { detection: { email: true } };
|
|
21
|
+
piiExporter = new PIIExporterDecorator(exporterMock, config);
|
|
60
22
|
});
|
|
61
23
|
|
|
62
|
-
it("
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
24
|
+
it("should redact emails in span name and attributes", () => {
|
|
25
|
+
const items: ReadableSpan[] = [
|
|
26
|
+
{
|
|
27
|
+
name: "user@example.com",
|
|
28
|
+
kind: 0,
|
|
29
|
+
spanContext: () => ({}),
|
|
30
|
+
attributes: { email: "user@example.com" },
|
|
31
|
+
resource: { attributes: { owner: "user@example.com" } },
|
|
32
|
+
events: [
|
|
33
|
+
{
|
|
34
|
+
name: "Login from user@example.com",
|
|
35
|
+
attributes: { email: "user@example.com" },
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
} as any,
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
const callback = vi.fn();
|
|
42
|
+
piiExporter.export(items, callback);
|
|
43
|
+
|
|
44
|
+
const exportedSpan = exporterMock.export.mock.calls[0][0][0];
|
|
45
|
+
expect(exportedSpan.name).toBe("[REDACTED EMAIL]");
|
|
46
|
+
expect(exportedSpan.attributes.email).toBe("[REDACTED EMAIL]");
|
|
47
|
+
expect(exportedSpan.resource.attributes.owner).toBe("[REDACTED EMAIL]");
|
|
48
|
+
expect(exportedSpan.events[0].name).toBe("Login from [REDACTED EMAIL]");
|
|
49
|
+
expect(exportedSpan.events[0].attributes.email).toBe("[REDACTED EMAIL]");
|
|
71
50
|
});
|
|
72
51
|
|
|
73
|
-
it("
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
52
|
+
it("should redact emails in log records", () => {
|
|
53
|
+
const items: ReadableLogRecord[] = [
|
|
54
|
+
{
|
|
55
|
+
body: "Error from user@example.com",
|
|
56
|
+
attributes: { email: "user@example.com" },
|
|
57
|
+
severityText: "INFO",
|
|
58
|
+
severityNumber: 1,
|
|
59
|
+
resource: { attributes: { owner: "user@example.com" } },
|
|
60
|
+
} as any,
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
const callback = vi.fn();
|
|
64
|
+
piiExporter.export(items, callback);
|
|
65
|
+
|
|
66
|
+
const exportedLog = exporterMock.export.mock.calls[0][0][0];
|
|
67
|
+
expect(exportedLog.body).toBe("Error from [REDACTED EMAIL]");
|
|
68
|
+
expect(exportedLog.attributes.email).toBe("[REDACTED EMAIL]");
|
|
69
|
+
expect(exportedLog.resource.attributes.owner).toBe("[REDACTED EMAIL]");
|
|
83
70
|
});
|
|
84
71
|
|
|
85
|
-
it("
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
expect(
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
// Resource is replaced with only attributes, old props lost
|
|
101
|
-
expect(span.resource).toEqual({
|
|
102
|
-
attributes: { team: "[REDACTED(dev@example.com)]" },
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
expect(span.events[0].name).toMatch(/^\[REDACTED\(/);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it("redacts log record data if detection.email is true", () => {
|
|
109
|
-
const config: NodeSDKConfig = { detection: { email: true } };
|
|
110
|
-
decorator = new PIIExporterDecorator(mockExporter as any, config);
|
|
111
|
-
|
|
112
|
-
const log = baseLog();
|
|
113
|
-
decorator.export([log], mockCallback);
|
|
114
|
-
|
|
115
|
-
expect(pii._cleanLogBodyPII).toHaveBeenCalled();
|
|
116
|
-
expect(pii._cleanObjectPII).toHaveBeenCalled();
|
|
117
|
-
|
|
118
|
-
expect(log.body).toMatch(/^\[REDACTED_BODY\(/);
|
|
119
|
-
expect(log.attributes.email).toMatch(/^\[REDACTED\(/);
|
|
120
|
-
|
|
121
|
-
// Resource replaced with only attributes, other props lost
|
|
122
|
-
expect(log.resource).toEqual({
|
|
123
|
-
attributes: { service: "[REDACTED(log@example.com)]" },
|
|
124
|
-
});
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it("handles a mix of spans and logs", () => {
|
|
128
|
-
const config: NodeSDKConfig = { detection: { email: true } };
|
|
129
|
-
decorator = new PIIExporterDecorator(mockExporter as any, config);
|
|
130
|
-
|
|
131
|
-
const items = [baseSpan(), baseLog()];
|
|
132
|
-
decorator.export(items, mockCallback);
|
|
133
|
-
|
|
134
|
-
expect(mockExporter.export).toHaveBeenCalledWith(items, mockCallback);
|
|
135
|
-
expect(pii._cleanLogBodyPII).toHaveBeenCalled();
|
|
136
|
-
expect(pii._cleanStringPII).toHaveBeenCalled();
|
|
137
|
-
expect(pii._cleanObjectPII).toHaveBeenCalled();
|
|
72
|
+
it("should redact emails in resource metrics", () => {
|
|
73
|
+
const metrics: ResourceMetrics = {
|
|
74
|
+
resource: {
|
|
75
|
+
attributes: { maintainer: "user@example.com" },
|
|
76
|
+
},
|
|
77
|
+
scopeMetrics: [],
|
|
78
|
+
} as any;
|
|
79
|
+
|
|
80
|
+
const callback = vi.fn();
|
|
81
|
+
piiExporter.export(metrics, callback);
|
|
82
|
+
|
|
83
|
+
const exportedMetric = exporterMock.export.mock.calls[0][0];
|
|
84
|
+
expect(exportedMetric.resource.attributes.maintainer).toBe(
|
|
85
|
+
"[REDACTED EMAIL]",
|
|
86
|
+
);
|
|
138
87
|
});
|
|
139
88
|
});
|
|
@@ -2,8 +2,9 @@ import { describe, expect, it, vi, beforeEach } from "vitest";
|
|
|
2
2
|
import {
|
|
3
3
|
_cleanStringPII,
|
|
4
4
|
_cleanLogBodyPII,
|
|
5
|
-
} from "../../lib/internals/pii-detection.js";
|
|
5
|
+
} from "../../lib/internals/redaction/pii-detection.js";
|
|
6
6
|
import * as sharedMetrics from "../../lib/internals/shared-metrics.js";
|
|
7
|
+
import { emailRedactor } from "../../lib/internals/redaction/redactors/email";
|
|
7
8
|
|
|
8
9
|
describe("PII Detection Utils", () => {
|
|
9
10
|
const mockMetricAdd = vi.fn();
|
|
@@ -16,9 +17,9 @@ describe("PII Detection Utils", () => {
|
|
|
16
17
|
});
|
|
17
18
|
|
|
18
19
|
describe("_cleanStringPII", () => {
|
|
19
|
-
it("redacts plain
|
|
20
|
+
it("redacts plain PII", () => {
|
|
20
21
|
const input = "admin@example.com";
|
|
21
|
-
const output = _cleanStringPII(input, "log");
|
|
22
|
+
const output = _cleanStringPII(input, "log", [emailRedactor]);
|
|
22
23
|
|
|
23
24
|
expect(output).toBe("[REDACTED EMAIL]");
|
|
24
25
|
expect(mockMetricAdd).toHaveBeenCalledWith(
|
|
@@ -31,9 +32,9 @@ describe("PII Detection Utils", () => {
|
|
|
31
32
|
);
|
|
32
33
|
});
|
|
33
34
|
|
|
34
|
-
it("redacts
|
|
35
|
+
it("redacts PII in URL-encoded string", () => {
|
|
35
36
|
const input = "user%40gmail.com";
|
|
36
|
-
const output = _cleanStringPII(input, "log");
|
|
37
|
+
const output = _cleanStringPII(input, "log", [emailRedactor]);
|
|
37
38
|
|
|
38
39
|
expect(output).toBe("[REDACTED EMAIL]");
|
|
39
40
|
expect(mockMetricAdd).toHaveBeenCalledWith(
|
|
@@ -45,9 +46,9 @@ describe("PII Detection Utils", () => {
|
|
|
45
46
|
);
|
|
46
47
|
});
|
|
47
48
|
|
|
48
|
-
it("handles strings without
|
|
49
|
+
it("handles strings without PII unchanged", () => {
|
|
49
50
|
const input = "hello world";
|
|
50
|
-
const output = _cleanStringPII(input, "log");
|
|
51
|
+
const output = _cleanStringPII(input, "log", [emailRedactor]);
|
|
51
52
|
|
|
52
53
|
expect(output).toBe("hello world");
|
|
53
54
|
expect(mockMetricAdd).not.toHaveBeenCalled();
|
|
@@ -55,23 +56,25 @@ describe("PII Detection Utils", () => {
|
|
|
55
56
|
|
|
56
57
|
it("handles array of strings", () => {
|
|
57
58
|
const input = ["one@gmail.com", "two@example.com"];
|
|
58
|
-
const output = _cleanStringPII(input, "log");
|
|
59
|
+
const output = _cleanStringPII(input, "log", [emailRedactor]);
|
|
59
60
|
|
|
60
61
|
expect(output).toEqual(["[REDACTED EMAIL]", "[REDACTED EMAIL]"]);
|
|
61
62
|
expect(mockMetricAdd).toHaveBeenCalledTimes(2);
|
|
62
63
|
});
|
|
63
64
|
|
|
64
65
|
it("ignores non-string input", () => {
|
|
65
|
-
expect(_cleanStringPII(1234, "trace")).toBe(1234);
|
|
66
|
-
expect(_cleanStringPII(true, "trace")).toBe(true);
|
|
67
|
-
expect(
|
|
66
|
+
expect(_cleanStringPII(1234, "trace", [emailRedactor])).toBe(1234);
|
|
67
|
+
expect(_cleanStringPII(true, "trace", [emailRedactor])).toBe(true);
|
|
68
|
+
expect(
|
|
69
|
+
_cleanStringPII(undefined, "trace", [emailRedactor]),
|
|
70
|
+
).toBeUndefined();
|
|
68
71
|
expect(mockMetricAdd).not.toHaveBeenCalled();
|
|
69
72
|
});
|
|
70
73
|
});
|
|
71
74
|
|
|
72
75
|
describe("_cleanLogBodyPII", () => {
|
|
73
|
-
it("cleans string
|
|
74
|
-
const result = _cleanLogBodyPII("demo@abc.com");
|
|
76
|
+
it("cleans string PII", () => {
|
|
77
|
+
const result = _cleanLogBodyPII("demo@abc.com", [emailRedactor]);
|
|
75
78
|
expect(result).toBe("[REDACTED EMAIL]");
|
|
76
79
|
});
|
|
77
80
|
|
|
@@ -86,7 +89,7 @@ describe("PII Detection Utils", () => {
|
|
|
86
89
|
status: "active",
|
|
87
90
|
};
|
|
88
91
|
|
|
89
|
-
const result = _cleanLogBodyPII(input);
|
|
92
|
+
const result = _cleanLogBodyPII(input, [emailRedactor]);
|
|
90
93
|
|
|
91
94
|
expect(result).toEqual({
|
|
92
95
|
user: {
|
|
@@ -102,7 +105,7 @@ describe("PII Detection Utils", () => {
|
|
|
102
105
|
it("cleans Uint8Array input", () => {
|
|
103
106
|
const str = "admin@gmail.com";
|
|
104
107
|
const buffer = new TextEncoder().encode(str);
|
|
105
|
-
const result = _cleanLogBodyPII(buffer);
|
|
108
|
+
const result = _cleanLogBodyPII(buffer, [emailRedactor]);
|
|
106
109
|
const decoded = new TextDecoder().decode(result as Uint8Array);
|
|
107
110
|
|
|
108
111
|
expect(decoded).toBe("[REDACTED EMAIL]");
|
|
@@ -110,7 +113,7 @@ describe("PII Detection Utils", () => {
|
|
|
110
113
|
|
|
111
114
|
it("skips malformed Uint8Array decode", () => {
|
|
112
115
|
const corrupted = new Uint8Array([0xff, 0xfe, 0xfd]);
|
|
113
|
-
const result = _cleanLogBodyPII(corrupted);
|
|
116
|
+
const result = _cleanLogBodyPII(corrupted, [emailRedactor]);
|
|
114
117
|
|
|
115
118
|
// Should return a Uint8Array, but unmodified/redaction should not happen
|
|
116
119
|
expect(result).toBeInstanceOf(Uint8Array);
|
|
@@ -118,11 +121,10 @@ describe("PII Detection Utils", () => {
|
|
|
118
121
|
});
|
|
119
122
|
|
|
120
123
|
it("cleans arrays of values", () => {
|
|
121
|
-
const result = _cleanLogBodyPII(
|
|
122
|
-
"bob@abc.com",
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
]);
|
|
124
|
+
const result = _cleanLogBodyPII(
|
|
125
|
+
["bob@abc.com", 123, { nested: "jane@example.com" }],
|
|
126
|
+
[emailRedactor],
|
|
127
|
+
);
|
|
126
128
|
|
|
127
129
|
expect(result).toEqual([
|
|
128
130
|
"[REDACTED EMAIL]",
|
|
@@ -132,10 +134,10 @@ describe("PII Detection Utils", () => {
|
|
|
132
134
|
});
|
|
133
135
|
|
|
134
136
|
it("passes null and boolean through", () => {
|
|
135
|
-
expect(_cleanLogBodyPII(null)).toBeNull();
|
|
136
|
-
expect(_cleanLogBodyPII(undefined)).toBeUndefined();
|
|
137
|
-
expect(_cleanLogBodyPII(true)).toBe(true);
|
|
138
|
-
expect(_cleanLogBodyPII(false)).toBe(false);
|
|
137
|
+
expect(_cleanLogBodyPII(null, [emailRedactor])).toBeNull();
|
|
138
|
+
expect(_cleanLogBodyPII(undefined, [emailRedactor])).toBeUndefined();
|
|
139
|
+
expect(_cleanLogBodyPII(true, [emailRedactor])).toBe(true);
|
|
140
|
+
expect(_cleanLogBodyPII(false, [emailRedactor])).toBe(false);
|
|
139
141
|
});
|
|
140
142
|
});
|
|
141
143
|
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import {
|
|
2
|
+
describe,
|
|
3
|
+
expect,
|
|
4
|
+
it,
|
|
5
|
+
vi,
|
|
6
|
+
beforeEach,
|
|
7
|
+
beforeAll,
|
|
8
|
+
afterAll,
|
|
9
|
+
} from "vitest";
|
|
10
|
+
|
|
11
|
+
import * as sharedMetrics from "../../../lib/internals/shared-metrics.js";
|
|
12
|
+
import { ipRedactor } from "../../../lib/internals/redaction/redactors/ip";
|
|
13
|
+
import { emailRedactor } from "../../../lib/internals/redaction/redactors/email";
|
|
14
|
+
|
|
15
|
+
describe("Email Redaction utils", () => {
|
|
16
|
+
describe("tracks metrics", () => {
|
|
17
|
+
const mockMetricAdd = vi.fn();
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
vi.restoreAllMocks();
|
|
21
|
+
vi.spyOn(sharedMetrics, "_getPIICounterRedactionMetric").mockReturnValue({
|
|
22
|
+
add: mockMetricAdd,
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("redacts plain PII and tracks redaction with metric", () => {
|
|
27
|
+
const input = "admin@example.com";
|
|
28
|
+
const output = emailRedactor(input, "log", "string");
|
|
29
|
+
|
|
30
|
+
expect(output).toBe("[REDACTED EMAIL]");
|
|
31
|
+
expect(mockMetricAdd).toHaveBeenCalledWith(
|
|
32
|
+
1,
|
|
33
|
+
expect.objectContaining({
|
|
34
|
+
pii_email_domain: "example.com",
|
|
35
|
+
pii_type: "email",
|
|
36
|
+
redaction_source: "log",
|
|
37
|
+
}),
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("handles strings without PII unchanged", () => {
|
|
42
|
+
const input = "hello world";
|
|
43
|
+
const output = ipRedactor(input, "log", "string");
|
|
44
|
+
|
|
45
|
+
expect(output).toBe("hello world");
|
|
46
|
+
expect(mockMetricAdd).not.toHaveBeenCalled();
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe("Redacts email addresses", () => {
|
|
51
|
+
beforeAll(() => {
|
|
52
|
+
vi.spyOn(sharedMetrics, "_getPIICounterRedactionMetric").mockReturnValue({
|
|
53
|
+
add: vi.fn(),
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
afterAll(() => {
|
|
58
|
+
vi.restoreAllMocks();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it.each`
|
|
62
|
+
value | expectedRedactedValue
|
|
63
|
+
${"user+tag@example.com"} | ${"[REDACTED EMAIL]"}
|
|
64
|
+
${"user.name+tag+sorting@example.com"} | ${"[REDACTED EMAIL]"}
|
|
65
|
+
${"x@example.museum"} | ${"[REDACTED EMAIL]"}
|
|
66
|
+
${"a.b-c_d@example.co.uk"} | ${"[REDACTED EMAIL]"}
|
|
67
|
+
${"üser@example.de"} | ${"[REDACTED EMAIL]"}
|
|
68
|
+
${"john.doe@xn--exmple-cua.com"} | ${"[REDACTED EMAIL]"}
|
|
69
|
+
${"üser@example.de"} | ${"[REDACTED EMAIL]"}
|
|
70
|
+
${"plainaddress"} | ${"plainaddress"}
|
|
71
|
+
${"@missinglocal.org"} | ${"@missinglocal.org"}
|
|
72
|
+
${"user@invalid_domain.com"} | ${"user@invalid_domain.com"}
|
|
73
|
+
`(
|
|
74
|
+
"returns $expectedRedactedValue for value '$value'",
|
|
75
|
+
async ({ value, expectedRedactedValue }: Record<string, string>) => {
|
|
76
|
+
const result = emailRedactor(value, "log", "string");
|
|
77
|
+
expect(result).toBe(expectedRedactedValue);
|
|
78
|
+
},
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import {
|
|
2
|
+
describe,
|
|
3
|
+
expect,
|
|
4
|
+
it,
|
|
5
|
+
vi,
|
|
6
|
+
beforeEach,
|
|
7
|
+
beforeAll,
|
|
8
|
+
afterAll,
|
|
9
|
+
} from "vitest";
|
|
10
|
+
|
|
11
|
+
import * as sharedMetrics from "../../../lib/internals/shared-metrics.js";
|
|
12
|
+
import { ipRedactor } from "../../../lib/internals/redaction/redactors/ip";
|
|
13
|
+
|
|
14
|
+
describe("IP Redaction utils", () => {
|
|
15
|
+
describe("tracks metrics", () => {
|
|
16
|
+
const mockMetricAdd = vi.fn();
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
vi.restoreAllMocks();
|
|
20
|
+
vi.spyOn(sharedMetrics, "_getPIICounterRedactionMetric").mockReturnValue({
|
|
21
|
+
add: mockMetricAdd,
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("redacts plain PII and tracks redaction with metric", () => {
|
|
26
|
+
const input = "255.255.255.255";
|
|
27
|
+
const output = ipRedactor(input, "log", "string");
|
|
28
|
+
|
|
29
|
+
expect(output).toBe("[REDACTED IPV4]");
|
|
30
|
+
expect(mockMetricAdd).toHaveBeenCalledWith(
|
|
31
|
+
1,
|
|
32
|
+
expect.objectContaining({
|
|
33
|
+
pii_format: "string",
|
|
34
|
+
pii_type: "IPv4",
|
|
35
|
+
redaction_source: "log",
|
|
36
|
+
}),
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("handles strings without PII unchanged", () => {
|
|
41
|
+
const input = "hello world";
|
|
42
|
+
const output = ipRedactor(input, "log", "string");
|
|
43
|
+
|
|
44
|
+
expect(output).toBe("hello world");
|
|
45
|
+
expect(mockMetricAdd).not.toHaveBeenCalled();
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe("Redacts IPv4 and IPv6", () => {
|
|
50
|
+
beforeAll(() => {
|
|
51
|
+
vi.spyOn(sharedMetrics, "_getPIICounterRedactionMetric").mockReturnValue({
|
|
52
|
+
add: vi.fn(),
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
afterAll(() => {
|
|
57
|
+
vi.restoreAllMocks();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it.each`
|
|
61
|
+
value | expectedRedactedValue
|
|
62
|
+
${"hello world"} | ${"hello world"}
|
|
63
|
+
${"hello 127.0.0.1"} | ${"hello [REDACTED IPV4]"}
|
|
64
|
+
${"127.0.0.1, hello!"} | ${"[REDACTED IPV4], hello!"}
|
|
65
|
+
${"127.0.0.1,127.0.0.1"} | ${"[REDACTED IPV4],[REDACTED IPV4]"}
|
|
66
|
+
${"127.0.0.1127.0.0.1"} | ${"127.0.0.1127.0.0.1"}
|
|
67
|
+
${"256.1.1.1"} | ${"256.1.1.1"}
|
|
68
|
+
${"0.0.0.0!"} | ${"[REDACTED IPV4]!"}
|
|
69
|
+
${"text0.0.0.0"} | ${"text[REDACTED IPV4]"}
|
|
70
|
+
${"0.0.text0.0"} | ${"0.0.text0.0"}
|
|
71
|
+
${"2001:0db8::1"} | ${"[REDACTED IPV6]"}
|
|
72
|
+
${"::1"} | ${"[REDACTED IPV6]"}
|
|
73
|
+
${"text::1"} | ${"text[REDACTED IPV6]"}
|
|
74
|
+
${"::1text"} | ${"[REDACTED IPV6]text"}
|
|
75
|
+
${"sentence ending with f::1"} | ${"sentence ending with [REDACTED IPV6]"}
|
|
76
|
+
${"sentence ending with :::1"} | ${"sentence ending with :::1"}
|
|
77
|
+
${"2001:0DB8:85A3:0000:0000:8A2E:0370:7334::1"} | ${"2001:0DB8:85A3:0000:0000:8A2E:0370:7334::1"}
|
|
78
|
+
${"2001:0DB8:85A3:0000:text:8A2E:0370:7334"} | ${"2001:0DB8:85A3:0000:text:8A2E:0370:7334"}
|
|
79
|
+
${"2001:0db8::12001:0db8::1"} | ${"2001:0db8::12001:0db8::1"}
|
|
80
|
+
${"2001:0db8::1,2001:0db8::1"} | ${"[REDACTED IPV6],[REDACTED IPV6]"}
|
|
81
|
+
`(
|
|
82
|
+
"returns $expectedRedactedValue for value '$value'",
|
|
83
|
+
async ({ value, expectedRedactedValue }: Record<string, string>) => {
|
|
84
|
+
const result = ipRedactor(value, "log", "string");
|
|
85
|
+
expect(result).toBe(expectedRedactedValue);
|
|
86
|
+
},
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
import * as piiDetection from "../../lib/internals/pii-detection.js";
|
|
2
|
+
import * as piiDetection from "../../lib/internals/redaction/pii-detection.js";
|
|
3
3
|
import { getActiveSpan } from "../../lib/traces.js";
|
|
4
4
|
import { MockSpan } from "../utils/mock-signals.js";
|
|
5
5
|
import { setNodeSdkConfig } from "../../lib/config-manager.js";
|
package/vitest.config.ts
CHANGED
|
@@ -27,10 +27,10 @@ export default defineConfig({
|
|
|
27
27
|
test: {
|
|
28
28
|
include: [
|
|
29
29
|
"**/test/*.test.ts",
|
|
30
|
-
"**/test/processor
|
|
31
|
-
"**/test/traces
|
|
32
|
-
"**/test/internals
|
|
33
|
-
"**/test/exporter
|
|
30
|
+
"**/test/processor/**/*.test.ts",
|
|
31
|
+
"**/test/traces/**/*.test.ts",
|
|
32
|
+
"**/test/internals/**/*.test.ts",
|
|
33
|
+
"**/test/exporter/**/*.test.ts",
|
|
34
34
|
],
|
|
35
35
|
name: "unit",
|
|
36
36
|
},
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import type { AnyValue } from "@opentelemetry/api-logs";
|
|
2
|
-
export type PIISource = "trace" | "log" | "metric";
|
|
3
|
-
/**
|
|
4
|
-
* Cleans a string by redacting email addresses and emitting metrics for PII.
|
|
5
|
-
*
|
|
6
|
-
* If the string is URL-encoded, it will be decoded before redaction.
|
|
7
|
-
* Metrics are emitted for each domain found in redacted email addresses.
|
|
8
|
-
*
|
|
9
|
-
* @param {string} value - The input string to sanitize.
|
|
10
|
-
* @param {"trace" | "log"} source - The source context of the input, used in metrics.
|
|
11
|
-
* @returns {string} The cleaned string with any email addresses replaced by `[REDACTED EMAIL]`.
|
|
12
|
-
*/
|
|
13
|
-
export declare function _cleanStringPII(value: AnyValue, source: PIISource): AnyValue;
|
|
14
|
-
export declare function _cleanObjectPII(entry: object, source: PIISource): {
|
|
15
|
-
[k: string]: AnyValue;
|
|
16
|
-
};
|
|
17
|
-
export declare function _cleanLogBodyPII(value: AnyValue): AnyValue;
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
import { _getPIICounterRedactionMetric } from "./shared-metrics.js";
|
|
2
|
-
const EMAIL_REGEX = /[a-zA-Z0-9._%+-]+@([a-zA-Z0-9.-]+\.[a-z]{2,})/gi;
|
|
3
|
-
const decoder = new TextDecoder();
|
|
4
|
-
const encoder = new TextEncoder();
|
|
5
|
-
/**
|
|
6
|
-
* Redacts all email addresses in the input string and collects metadata.
|
|
7
|
-
*
|
|
8
|
-
* @param {string} value The input string potentially containing email addresses.
|
|
9
|
-
* @returns {{
|
|
10
|
-
* redacted: string,
|
|
11
|
-
* count: number,
|
|
12
|
-
* domains: Record<string, number>
|
|
13
|
-
* }}
|
|
14
|
-
*
|
|
15
|
-
* An object containing:
|
|
16
|
-
* - `redacted`: the string with email addresses replaced by `[REDACTED EMAIL]`
|
|
17
|
-
* - `count`: total number of email addresses redacted
|
|
18
|
-
* - `domains`: a map of domain names to the number of times they were redacted
|
|
19
|
-
*/
|
|
20
|
-
function _redactEmails(value) {
|
|
21
|
-
let count = 0;
|
|
22
|
-
const domains = {};
|
|
23
|
-
const redacted = value.replace(EMAIL_REGEX, (_, domain) => {
|
|
24
|
-
count++;
|
|
25
|
-
domains[domain] = (domains[domain] || 0) + 1;
|
|
26
|
-
return "[REDACTED EMAIL]";
|
|
27
|
-
});
|
|
28
|
-
return { redacted, count, domains };
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Checks whether a string contains URI-encoded components.
|
|
32
|
-
*
|
|
33
|
-
* @param {string} value - The string to inspect.
|
|
34
|
-
* @returns {boolean} `true` if the string is encoded, `false` otherwise.
|
|
35
|
-
*/
|
|
36
|
-
function _containsEncodedComponents(value) {
|
|
37
|
-
try {
|
|
38
|
-
return decodeURI(value) !== decodeURIComponent(value);
|
|
39
|
-
}
|
|
40
|
-
catch {
|
|
41
|
-
return false;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* Cleans a string by redacting email addresses and emitting metrics for PII.
|
|
46
|
-
*
|
|
47
|
-
* If the string is URL-encoded, it will be decoded before redaction.
|
|
48
|
-
* Metrics are emitted for each domain found in redacted email addresses.
|
|
49
|
-
*
|
|
50
|
-
* @param {string} value - The input string to sanitize.
|
|
51
|
-
* @param {"trace" | "log"} source - The source context of the input, used in metrics.
|
|
52
|
-
* @returns {string} The cleaned string with any email addresses replaced by `[REDACTED EMAIL]`.
|
|
53
|
-
*/
|
|
54
|
-
export function _cleanStringPII(value, source) {
|
|
55
|
-
if (Array.isArray(value)) {
|
|
56
|
-
return value.map((v) => _cleanStringPII(v, source));
|
|
57
|
-
}
|
|
58
|
-
if (typeof value !== "string") {
|
|
59
|
-
return value;
|
|
60
|
-
}
|
|
61
|
-
let kind = "string";
|
|
62
|
-
let decodedValue = value;
|
|
63
|
-
if (_containsEncodedComponents(value)) {
|
|
64
|
-
decodedValue = decodeURIComponent(value);
|
|
65
|
-
kind = "url";
|
|
66
|
-
}
|
|
67
|
-
const { redacted, count, domains } = _redactEmails(decodedValue);
|
|
68
|
-
if (count > 0) {
|
|
69
|
-
for (const [domain, domainCount] of Object.entries(domains)) {
|
|
70
|
-
_getPIICounterRedactionMetric().add(domainCount, {
|
|
71
|
-
pii_type: "email",
|
|
72
|
-
redaction_source: source,
|
|
73
|
-
pii_email_domain: domain,
|
|
74
|
-
pii_format: kind,
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
return redacted;
|
|
79
|
-
}
|
|
80
|
-
export function _cleanObjectPII(entry, source) {
|
|
81
|
-
if (!entry) {
|
|
82
|
-
return entry;
|
|
83
|
-
}
|
|
84
|
-
return Object.fromEntries(Object.entries(entry).map(([k, v]) => [k, _cleanStringPII(v, source)]));
|
|
85
|
-
}
|
|
86
|
-
export function _cleanLogBodyPII(value) {
|
|
87
|
-
if (typeof value === "string") {
|
|
88
|
-
return _cleanStringPII(value, "log");
|
|
89
|
-
}
|
|
90
|
-
if (typeof value === "number" ||
|
|
91
|
-
typeof value === "boolean" ||
|
|
92
|
-
value == null) {
|
|
93
|
-
return value;
|
|
94
|
-
}
|
|
95
|
-
if (value instanceof Uint8Array) {
|
|
96
|
-
try {
|
|
97
|
-
const decoded = decoder.decode(value);
|
|
98
|
-
const sanitized = _cleanStringPII(decoded, "log");
|
|
99
|
-
return encoder.encode(sanitized);
|
|
100
|
-
}
|
|
101
|
-
catch {
|
|
102
|
-
return value;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
if (Array.isArray(value)) {
|
|
106
|
-
return value.map(_cleanLogBodyPII);
|
|
107
|
-
}
|
|
108
|
-
if (typeof value === "object") {
|
|
109
|
-
const sanitized = {};
|
|
110
|
-
for (const [key, val] of Object.entries(value)) {
|
|
111
|
-
sanitized[key] = _cleanLogBodyPII(val);
|
|
112
|
-
}
|
|
113
|
-
return sanitized;
|
|
114
|
-
}
|
|
115
|
-
return value;
|
|
116
|
-
}
|