mask-privacy 1.0.2
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/.test_audit.db +0 -0
- package/README.md +250 -0
- package/dist/index.d.mts +257 -0
- package/dist/index.d.ts +257 -0
- package/dist/index.js +58820 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +58777 -0
- package/dist/index.mjs.map +1 -0
- package/examples/secure_vault/email_tool.ts +13 -0
- package/examples/test_agent.ts +50 -0
- package/jest.config.js +10 -0
- package/package.json +37 -0
- package/src/client.ts +135 -0
- package/src/core/crypto.ts +100 -0
- package/src/core/exceptions.ts +23 -0
- package/src/core/fpe.ts +185 -0
- package/src/core/key_provider.ts +158 -0
- package/src/core/scanner.ts +308 -0
- package/src/core/utils.ts +76 -0
- package/src/core/vault.ts +540 -0
- package/src/index.ts +85 -0
- package/src/integrations/adk_hooks.ts +56 -0
- package/src/integrations/langchain_hooks.ts +87 -0
- package/src/integrations/llamaindex_hooks.ts +80 -0
- package/src/telemetry/audit_logger.ts +168 -0
- package/tests/async.test.ts +47 -0
- package/tests/audit_logger.test.ts +55 -0
- package/tests/exceptions.test.ts +75 -0
- package/tests/fail_strategy.test.ts +84 -0
- package/tests/fpe.test.ts +126 -0
- package/tests/hooks.test.ts +107 -0
- package/tests/key_provider.test.ts +68 -0
- package/tests/langchain.test.ts +101 -0
- package/tests/llamaindex.test.ts +75 -0
- package/tests/scanner.test.ts +107 -0
- package/tests/smoke.test.ts +6 -0
- package/tests/substring.test.ts +59 -0
- package/tests/vault.test.ts +101 -0
- package/tests/vault_backends.test.ts +124 -0
- package/tsconfig.json +22 -0
- package/tsup.config.ts +11 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach } from '@jest/globals';
|
|
2
|
+
import { generateFPEToken, looksLikeToken, resetMasterKey } from '../src/core/fpe';
|
|
3
|
+
|
|
4
|
+
describe('TestFPETokenGeneration', () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
resetMasterKey();
|
|
7
|
+
process.env.MASK_MASTER_KEY = "test-key-for-deterministic-fpe";
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
resetMasterKey();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('test_email_format', () => {
|
|
15
|
+
const token = generateFPEToken("user@company.io");
|
|
16
|
+
expect(token.endsWith("@email.com")).toBe(true);
|
|
17
|
+
expect(token.startsWith("tkn-")).toBe(true);
|
|
18
|
+
expect(token).toMatch(/^[^@]+@[^@]+\.[^@]+$/);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('test_phone_format', () => {
|
|
22
|
+
const token = generateFPEToken("+1-212-555-1234");
|
|
23
|
+
expect(token.startsWith("+1-555-")).toBe(true);
|
|
24
|
+
expect(token.length).toBe(14);
|
|
25
|
+
expect(token).toMatch(/^\+1-555-\d{7}$/);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('test_seven_digit_phone_format', () => {
|
|
29
|
+
const token = generateFPEToken("555-1234");
|
|
30
|
+
expect(token.startsWith("+1-555-")).toBe(true);
|
|
31
|
+
expect(token.length).toBe(14);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('test_ssn_format', () => {
|
|
35
|
+
const token = generateFPEToken("123-45-6789");
|
|
36
|
+
expect(token.startsWith("000-00-")).toBe(true);
|
|
37
|
+
expect(token.length).toBe(11);
|
|
38
|
+
expect(token).toMatch(/^\d{3}-\d{2}-\d{4}$/);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('test_cc_format', () => {
|
|
42
|
+
const token = generateFPEToken("4111-1111-1111-1111");
|
|
43
|
+
expect(token.startsWith("4000-0000-0000-")).toBe(true);
|
|
44
|
+
expect(token.length).toBe(19);
|
|
45
|
+
expect(token).toMatch(/^(?:\d{4}[ \-]?){3}\d{4}$/);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('test_routing_format', () => {
|
|
49
|
+
const token = generateFPEToken("122000661");
|
|
50
|
+
expect(token.startsWith("000000")).toBe(true);
|
|
51
|
+
expect(token.length).toBe(9);
|
|
52
|
+
expect(token).toMatch(/^\d{9}$/);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('test_opaque_fallback', () => {
|
|
56
|
+
const token = generateFPEToken("just some random string");
|
|
57
|
+
expect(token.startsWith("[TKN-")).toBe(true);
|
|
58
|
+
expect(token.endsWith("]")).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('test_deterministic_same_input_same_output', () => {
|
|
62
|
+
const t1 = generateFPEToken("a@b.com");
|
|
63
|
+
const t2 = generateFPEToken("a@b.com");
|
|
64
|
+
expect(t1).toBe(t2);
|
|
65
|
+
expect(t1.endsWith("@email.com")).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('test_different_inputs_different_tokens', () => {
|
|
69
|
+
const t1 = generateFPEToken("alice@example.com");
|
|
70
|
+
const t2 = generateFPEToken("bob@example.com");
|
|
71
|
+
expect(t1).not.toBe(t2);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('test_determinism_across_all_types', () => {
|
|
75
|
+
const values = [
|
|
76
|
+
"user@test.com",
|
|
77
|
+
"+1-212-555-1234",
|
|
78
|
+
"555-1234",
|
|
79
|
+
"123-45-6789",
|
|
80
|
+
"4111-1111-1111-1111",
|
|
81
|
+
"122000661",
|
|
82
|
+
"John Doe",
|
|
83
|
+
];
|
|
84
|
+
for (const value of values) {
|
|
85
|
+
expect(generateFPEToken(value)).toBe(generateFPEToken(value));
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('test_whitespace_stripped_determinism', () => {
|
|
90
|
+
expect(generateFPEToken(" someone@email.com ")).toBe(generateFPEToken("someone@email.com"));
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('TestLooksLikeToken', () => {
|
|
95
|
+
test('test_email_token', () => {
|
|
96
|
+
expect(looksLikeToken("tkn-abcd1234@email.com")).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('test_phone_token', () => {
|
|
100
|
+
expect(looksLikeToken("+1-555-1234567")).toBe(true);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('test_ssn_token', () => {
|
|
104
|
+
expect(looksLikeToken("000-00-1234")).toBe(true);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('test_cc_token', () => {
|
|
108
|
+
expect(looksLikeToken("4000-0000-0000-1234")).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('test_routing_token', () => {
|
|
112
|
+
expect(looksLikeToken("000000123")).toBe(true);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('test_opaque_token', () => {
|
|
116
|
+
expect(looksLikeToken("[TKN-abcd1234]")).toBe(true);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('test_real_email_is_not_token', () => {
|
|
120
|
+
expect(looksLikeToken("real@company.com")).toBe(false);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('test_random_string_is_not_token', () => {
|
|
124
|
+
expect(looksLikeToken("hello world")).toBe(false);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach, jest } from '@jest/globals';
|
|
2
|
+
import { encode, resetVault } from '../src/core/vault';
|
|
3
|
+
import { resetMasterKey } from '../src/core/fpe';
|
|
4
|
+
import { decryptBeforeTool, encryptAfterTool } from '../src/integrations/adk_hooks';
|
|
5
|
+
import { deepDecode, deepEncodePII } from '../src/core/utils';
|
|
6
|
+
import { looksLikeToken } from '../src/core/fpe';
|
|
7
|
+
import * as process from 'process';
|
|
8
|
+
|
|
9
|
+
// Minimal stubs matching ADK protocol
|
|
10
|
+
class FakeTool {
|
|
11
|
+
name = "test_tool";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
class FakeCtx {
|
|
15
|
+
agentName = "test_agent";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('TestHooks', () => {
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
process.env.MASK_VAULT_TYPE = "memory";
|
|
21
|
+
resetVault();
|
|
22
|
+
resetMasterKey();
|
|
23
|
+
process.env.MASK_MASTER_KEY = "test-hooks-key";
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
resetVault();
|
|
28
|
+
resetMasterKey();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('TestDeepDecode', () => {
|
|
32
|
+
test('test_flat_dict', async () => {
|
|
33
|
+
const token = await encode("alice@corp.io");
|
|
34
|
+
const result = await deepDecode({"email": token, "msg": "hi"});
|
|
35
|
+
expect(result.email).toBe("alice@corp.io");
|
|
36
|
+
expect(result.msg).toBe("hi");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('test_nested_dict', async () => {
|
|
40
|
+
const token = await encode("bob@bank.com");
|
|
41
|
+
const data = {"user": {"contact": {"email": token}}};
|
|
42
|
+
const result = await deepDecode(data);
|
|
43
|
+
expect(result.user.contact.email).toBe("bob@bank.com");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('test_list_values', async () => {
|
|
47
|
+
const t1 = await encode("a@b.com");
|
|
48
|
+
const t2 = await encode("c@d.com");
|
|
49
|
+
const result = await deepDecode({"recipients": [t1, t2]});
|
|
50
|
+
expect(result.recipients).toEqual(["a@b.com", "c@d.com"]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('test_non_token_strings_unchanged', async () => {
|
|
54
|
+
const result = await deepDecode({"name": "Alice", "age": 30});
|
|
55
|
+
expect(result).toEqual({"name": "Alice", "age": 30});
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('TestDeepEncodeEmails', () => {
|
|
60
|
+
test('test_encodes_raw_email', async () => {
|
|
61
|
+
const result = await deepEncodePII({"email": "test@example.com"});
|
|
62
|
+
expect(looksLikeToken(result.email)).toBe(true);
|
|
63
|
+
expect(result.email).toMatch(/@email\.com$/);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('test_does_not_double_encode_token', async () => {
|
|
67
|
+
const token = await encode("original@test.com");
|
|
68
|
+
const result = await deepEncodePII({"email": token});
|
|
69
|
+
expect(result.email).toBe(token);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('TestDecryptBeforeTool', () => {
|
|
74
|
+
test('test_mutates_args_in_place', async () => {
|
|
75
|
+
const token = await encode("admin@secure.io");
|
|
76
|
+
const args = {"email": token, "action": "send"};
|
|
77
|
+
await decryptBeforeTool(new FakeTool(), args, new FakeCtx());
|
|
78
|
+
expect(args.email).toBe("admin@secure.io");
|
|
79
|
+
expect(args.action).toBe("send");
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('TestEncryptAfterTool', () => {
|
|
84
|
+
test('test_encodes_leaked_emails_in_args', async () => {
|
|
85
|
+
const args = {"email": "leaked@plain.com"};
|
|
86
|
+
await encryptAfterTool(new FakeTool() as any, args, new FakeCtx() as any, {});
|
|
87
|
+
expect(looksLikeToken(args.email)).toBe(true);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('test_encodes_leaked_emails_in_string_response', async () => {
|
|
91
|
+
const args = "Contact us at support@example.com for help.";
|
|
92
|
+
const result = await deepEncodePII(args);
|
|
93
|
+
expect(typeof result).toBe('string');
|
|
94
|
+
expect(result).toContain("@email.com");
|
|
95
|
+
expect(result).not.toContain("support@example.com");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('test_encodes_leaked_emails_but_skips_tokens_in_nested_dict', async () => {
|
|
99
|
+
const token = await encode("admin@secure.io");
|
|
100
|
+
const args = {"response": {"leaked": "bad@plain.com", "safe": token}};
|
|
101
|
+
const result = await deepEncodePII(args);
|
|
102
|
+
|
|
103
|
+
expect(looksLikeToken(result.response.leaked)).toBe(true);
|
|
104
|
+
expect(result.response.safe).toBe(token);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach } from '@jest/globals';
|
|
2
|
+
import {
|
|
3
|
+
EnvKeyProvider,
|
|
4
|
+
AwsKmsKeyProvider,
|
|
5
|
+
AzureKeyVaultProvider,
|
|
6
|
+
HashiCorpVaultProvider,
|
|
7
|
+
getKeyProvider,
|
|
8
|
+
setKeyProvider,
|
|
9
|
+
resetKeyProvider
|
|
10
|
+
} from '../src/core/key_provider';
|
|
11
|
+
|
|
12
|
+
describe('TestKeyProvider', () => {
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
resetKeyProvider();
|
|
15
|
+
delete process.env.MASK_ENCRYPTION_KEY;
|
|
16
|
+
delete process.env.MASK_MASTER_KEY;
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
resetKeyProvider();
|
|
21
|
+
delete process.env.MASK_ENCRYPTION_KEY;
|
|
22
|
+
delete process.env.MASK_MASTER_KEY;
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('test_default_is_env_provider', () => {
|
|
26
|
+
const provider = getKeyProvider();
|
|
27
|
+
expect(provider).toBeInstanceOf(EnvKeyProvider);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('test_set_custom_provider', () => {
|
|
31
|
+
class DummyProvider extends EnvKeyProvider {}
|
|
32
|
+
const dummy = new DummyProvider();
|
|
33
|
+
setKeyProvider(dummy);
|
|
34
|
+
expect(getKeyProvider()).toBe(dummy);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('test_env_provider_reads_from_environ', () => {
|
|
38
|
+
process.env.MASK_ENCRYPTION_KEY = "test-enc-key";
|
|
39
|
+
process.env.MASK_MASTER_KEY = "test-master-key";
|
|
40
|
+
|
|
41
|
+
const provider = new EnvKeyProvider();
|
|
42
|
+
expect(provider.getEncryptionKey()).toBe("test-enc-key");
|
|
43
|
+
expect(provider.getMasterKey()).toBe("test-master-key");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('test_env_provider_falls_back_master_to_encryption', () => {
|
|
47
|
+
process.env.MASK_ENCRYPTION_KEY = "fallback-key";
|
|
48
|
+
delete process.env.MASK_MASTER_KEY;
|
|
49
|
+
|
|
50
|
+
const provider = new EnvKeyProvider();
|
|
51
|
+
expect(provider.getEncryptionKey()).toBe("fallback-key");
|
|
52
|
+
expect(provider.getMasterKey()).toBe("fallback-key");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('test_stub_providers_raise_not_implemented', () => {
|
|
56
|
+
const aws = new AwsKmsKeyProvider("alias/key");
|
|
57
|
+
expect(() => aws.getEncryptionKey()).toThrow();
|
|
58
|
+
expect(() => aws.getMasterKey()).toThrow();
|
|
59
|
+
|
|
60
|
+
const azure = new AzureKeyVaultProvider("https://vault");
|
|
61
|
+
expect(() => azure.getEncryptionKey()).toThrow();
|
|
62
|
+
expect(() => azure.getMasterKey()).toThrow();
|
|
63
|
+
|
|
64
|
+
const hashi = new HashiCorpVaultProvider("https://vault:8200");
|
|
65
|
+
expect(() => hashi.getEncryptionKey()).toThrow();
|
|
66
|
+
expect(() => hashi.getMasterKey()).toThrow();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach, jest } from '@jest/globals';
|
|
2
|
+
import { encode, resetVault } from '../src/core/vault';
|
|
3
|
+
import { resetMasterKey } from '../src/core/fpe';
|
|
4
|
+
import { MaskToolWrapper, secureTool, getMaskCallbackHandler } from '../src/integrations/langchain_hooks';
|
|
5
|
+
import * as process from 'process';
|
|
6
|
+
|
|
7
|
+
describe('TestLangchainHooks', () => {
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
process.env.MASK_VAULT_TYPE = "memory";
|
|
10
|
+
resetVault();
|
|
11
|
+
resetMasterKey();
|
|
12
|
+
process.env.MASK_MASTER_KEY = "test-langchain-key";
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
resetVault();
|
|
17
|
+
resetMasterKey();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('TestLangchainMaskToolWrapper', () => {
|
|
21
|
+
test('test_wrapper_detokenizes_inputs_and_tokenizes_outputs', async () => {
|
|
22
|
+
const token = await encode("user@example.com");
|
|
23
|
+
|
|
24
|
+
const mockTool = jest.fn<any>().mockImplementation(async (email: string, subject: string) => {
|
|
25
|
+
expect(email).toBe("user@example.com");
|
|
26
|
+
expect(subject).toBe("Welcome");
|
|
27
|
+
return {"target": email, "subject": subject};
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const secure = new MaskToolWrapper(mockTool);
|
|
31
|
+
const result = await secure.run(token, "Welcome");
|
|
32
|
+
|
|
33
|
+
expect(result.target).not.toBe("user@example.com");
|
|
34
|
+
expect(result.target).toMatch(/@email\.com$/);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('TestLangchainMaskCallbackHandler', () => {
|
|
39
|
+
test('test_on_tool_start_mutates_inputs', async () => {
|
|
40
|
+
// Mock BaseCallbackHandler since we don't want to install @langchain/core in this environment if not present
|
|
41
|
+
const MockHandlerClass = await getMaskCallbackHandler();
|
|
42
|
+
const handler = new MockHandlerClass();
|
|
43
|
+
|
|
44
|
+
const token1 = await encode("alice@corp.io");
|
|
45
|
+
const token2 = await encode("bob@corp.io");
|
|
46
|
+
|
|
47
|
+
const inputsDict = {
|
|
48
|
+
"primary": token1,
|
|
49
|
+
"cc": [token2, "charlie@corp.io"],
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
if ((handler as any).handleToolStart) {
|
|
53
|
+
await (handler as any).handleToolStart(
|
|
54
|
+
{"name": "send_email"},
|
|
55
|
+
"...",
|
|
56
|
+
"run-id",
|
|
57
|
+
undefined,
|
|
58
|
+
[],
|
|
59
|
+
{},
|
|
60
|
+
inputsDict
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
expect(inputsDict.primary).toBe("alice@corp.io");
|
|
64
|
+
expect(inputsDict.cc[0]).toBe("bob@corp.io");
|
|
65
|
+
expect(inputsDict.cc[1]).toBe("charlie@corp.io");
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('TestLangchainSecureTool', () => {
|
|
71
|
+
test('test_secure_tool_decorator_detokenizes_and_retokenizes', async () => {
|
|
72
|
+
const token = await encode("dev@mask.ai");
|
|
73
|
+
|
|
74
|
+
const sendEmail = secureTool(async (email: string, body: string) => {
|
|
75
|
+
expect(email).toBe("dev@mask.ai");
|
|
76
|
+
return `Sent to ${email}`;
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const result = await sendEmail(token, "Hello");
|
|
80
|
+
expect(result).not.toContain("dev@mask.ai");
|
|
81
|
+
expect(result).toContain("@email.com");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('test_secure_tool_preserves_non_pii', async () => {
|
|
85
|
+
const greet = secureTool(async (name: string) => {
|
|
86
|
+
return `Hello, ${name}!`;
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const result = await greet("World");
|
|
90
|
+
expect(result).toBe("Hello, World!");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('test_secure_tool_with_custom_name', () => {
|
|
94
|
+
const lookup = secureTool(async (userId: string) => {
|
|
95
|
+
return {"id": userId};
|
|
96
|
+
}, { name: "custom_lookup" });
|
|
97
|
+
|
|
98
|
+
expect(lookup.name).toBe("custom_lookup");
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach, jest } from '@jest/globals';
|
|
2
|
+
import { encode, resetVault } from '../src/core/vault';
|
|
3
|
+
import { resetMasterKey } from '../src/core/fpe';
|
|
4
|
+
import { MaskToolWrapper, maskLlamaIndexHooks } from '../src/integrations/llamaindex_hooks';
|
|
5
|
+
import * as process from 'process';
|
|
6
|
+
|
|
7
|
+
describe('TestLlamaindexHooks', () => {
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
process.env.MASK_VAULT_TYPE = "memory";
|
|
10
|
+
resetVault();
|
|
11
|
+
resetMasterKey();
|
|
12
|
+
process.env.MASK_MASTER_KEY = "test-llamaindex-key";
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
resetVault();
|
|
17
|
+
resetMasterKey();
|
|
18
|
+
jest.restoreAllMocks();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe('TestLlamaindexMaskToolWrapper', () => {
|
|
22
|
+
test('test_wrapper_detokenizes_inputs_and_tokenizes_outputs', async () => {
|
|
23
|
+
const token = await encode("admin@hospital.com");
|
|
24
|
+
|
|
25
|
+
const mockTool = jest.fn<any>().mockImplementation(async (email: string, prompt: string) => {
|
|
26
|
+
expect(email).toBe("admin@hospital.com");
|
|
27
|
+
expect(prompt).toBe("Give me the records");
|
|
28
|
+
return {"target": email, "status": "success"};
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const secureTool = new MaskToolWrapper(mockTool);
|
|
32
|
+
const result = await secureTool.run(token, "Give me the records");
|
|
33
|
+
|
|
34
|
+
expect(result.target).not.toBe("admin@hospital.com");
|
|
35
|
+
expect(result.target).toMatch(/@email\.com$/);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('TestLlamaindexMagicHooks', () => {
|
|
40
|
+
test('test_mask_llamaindex_hooks_patches_basetool', async () => {
|
|
41
|
+
// Minimal stub for LlamaIndex BaseTool
|
|
42
|
+
class BaseTool {
|
|
43
|
+
async call(...args: any[]) {
|
|
44
|
+
return `Secret: ${args[0]}`;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Mock 'llamaindex' module discovery
|
|
49
|
+
// In a real environment, we'd use jest.mock('llamaindex', ...)
|
|
50
|
+
// Here we'll manually apply the logic to our stub to verify the "magic" patch logic
|
|
51
|
+
|
|
52
|
+
const originalCall = BaseTool.prototype.call;
|
|
53
|
+
const deepDecode = require('../src/core/utils').deepDecode;
|
|
54
|
+
const deepEncodePII = require('../src/core/utils').deepEncodePII;
|
|
55
|
+
|
|
56
|
+
BaseTool.prototype.call = async function(this: any, ...args: any[]) {
|
|
57
|
+
const decodedArgs = await Promise.all(args.map(a => deepDecode(a)));
|
|
58
|
+
const result = await originalCall.apply(this, decodedArgs);
|
|
59
|
+
if (typeof result === 'string' || typeof result === 'object') {
|
|
60
|
+
return await deepEncodePII(result);
|
|
61
|
+
}
|
|
62
|
+
return result;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const tool = new BaseTool();
|
|
66
|
+
const token = await encode("llamaindex@mask.ai");
|
|
67
|
+
|
|
68
|
+
const result = await tool.call(token);
|
|
69
|
+
|
|
70
|
+
expect(result).not.toContain("llamaindex@mask.ai");
|
|
71
|
+
expect(result).toContain("Secret:");
|
|
72
|
+
expect(result).toContain("tkn-");
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { describe, test, expect } from '@jest/globals';
|
|
2
|
+
import { REGEX_PATTERNS, PresidioScanner } from '../src/core/scanner';
|
|
3
|
+
|
|
4
|
+
describe('TestInternationalPhonePatterns', () => {
|
|
5
|
+
const pattern = new RegExp(REGEX_PATTERNS["PHONE_NUMBER_INTL"].source, 'g');
|
|
6
|
+
|
|
7
|
+
test.each([
|
|
8
|
+
"+44 20 7946 0958",
|
|
9
|
+
"+44 7911 123456",
|
|
10
|
+
"+442079460958",
|
|
11
|
+
"+33 1 23 45 67 89",
|
|
12
|
+
"+33 6 12 34 56 78",
|
|
13
|
+
"+49 30 1234 5678",
|
|
14
|
+
"+49 170 1234567",
|
|
15
|
+
])('test_intl_phone_match: %s', (number) => {
|
|
16
|
+
pattern.lastIndex = 0;
|
|
17
|
+
expect(pattern.test(number)).toBe(true);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test.each([
|
|
21
|
+
"+1 555 123 4567",
|
|
22
|
+
"020 7946 0958",
|
|
23
|
+
"just some text",
|
|
24
|
+
])('test_intl_phone_no_match: %s', (nonMatch) => {
|
|
25
|
+
pattern.lastIndex = 0;
|
|
26
|
+
expect(pattern.test(nonMatch)).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('TestUSRoutingNumber', () => {
|
|
31
|
+
const pattern = new RegExp(REGEX_PATTERNS["US_ROUTING_NUMBER"].source, 'g');
|
|
32
|
+
|
|
33
|
+
test('test_regex_matches_9_digit_number', () => {
|
|
34
|
+
pattern.lastIndex = 0;
|
|
35
|
+
expect(pattern.test("021000021")).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('test_regex_does_not_match_8_digits', () => {
|
|
39
|
+
pattern.lastIndex = 0;
|
|
40
|
+
expect(pattern.test("word 12345678 word")).toBe(false);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('test_aba_checksum_valid', () => {
|
|
44
|
+
// @ts-ignore - accessing protected static for test
|
|
45
|
+
expect(PresidioScanner._abaChecksum("021000021")).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('test_aba_checksum_invalid', () => {
|
|
49
|
+
// @ts-ignore
|
|
50
|
+
expect(PresidioScanner._abaChecksum("123456789")).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('test_aba_checksum_wrong_length', () => {
|
|
54
|
+
// @ts-ignore
|
|
55
|
+
expect(PresidioScanner._abaChecksum("12345")).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('TestUSPassport', () => {
|
|
60
|
+
const pattern = new RegExp(REGEX_PATTERNS["US_PASSPORT"].source, 'g');
|
|
61
|
+
|
|
62
|
+
test.each([
|
|
63
|
+
"C12345678",
|
|
64
|
+
"A00000001",
|
|
65
|
+
"Z99999999",
|
|
66
|
+
])('test_passport_match: %s', (passport) => {
|
|
67
|
+
pattern.lastIndex = 0;
|
|
68
|
+
expect(pattern.test(passport)).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test.each([
|
|
72
|
+
"c12345678",
|
|
73
|
+
"12345678A",
|
|
74
|
+
"AB12345678",
|
|
75
|
+
"A1234567",
|
|
76
|
+
])('test_passport_no_match: %s', (nonMatch) => {
|
|
77
|
+
pattern.lastIndex = 0;
|
|
78
|
+
expect(pattern.test(nonMatch)).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('TestDateOfBirth', () => {
|
|
83
|
+
const pattern = new RegExp(REGEX_PATTERNS["DATE_OF_BIRTH"].source, 'g');
|
|
84
|
+
|
|
85
|
+
test.each([
|
|
86
|
+
"01/15/1990",
|
|
87
|
+
"12/31/2000",
|
|
88
|
+
"06/01/1985",
|
|
89
|
+
"1990-01-15",
|
|
90
|
+
"2000-12-31",
|
|
91
|
+
"1985-06-01",
|
|
92
|
+
])('test_dob_match: %s', (dob) => {
|
|
93
|
+
pattern.lastIndex = 0;
|
|
94
|
+
expect(pattern.test(dob)).toBe(true);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test.each([
|
|
98
|
+
"13/01/1990",
|
|
99
|
+
"00/15/1990",
|
|
100
|
+
"01/32/1990",
|
|
101
|
+
"1890-01-01",
|
|
102
|
+
"2100-01-01",
|
|
103
|
+
])('test_dob_no_match: %s', (nonMatch) => {
|
|
104
|
+
pattern.lastIndex = 0;
|
|
105
|
+
expect(pattern.test(nonMatch)).toBe(false);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach } from '@jest/globals';
|
|
2
|
+
import { encode, resetVault, detokenizeText } from '../src/core/vault';
|
|
3
|
+
import { resetMasterKey } from '../src/core/fpe';
|
|
4
|
+
import { deepDecode } from '../src/core/utils';
|
|
5
|
+
import * as process from 'process';
|
|
6
|
+
|
|
7
|
+
describe('TestSubstringDetokenization', () => {
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
process.env.MASK_VAULT_TYPE = "memory";
|
|
10
|
+
resetVault();
|
|
11
|
+
resetMasterKey();
|
|
12
|
+
process.env.MASK_MASTER_KEY = "test-substring-key";
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
resetVault();
|
|
17
|
+
resetMasterKey();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('test_detokenize_text_with_embedded_tokens', async () => {
|
|
21
|
+
const email = "alice@example.com";
|
|
22
|
+
const phone = "+1-555-123-4567";
|
|
23
|
+
|
|
24
|
+
const tEmail = await encode(email);
|
|
25
|
+
const tPhone = await encode(phone);
|
|
26
|
+
|
|
27
|
+
const paragraph = `Contact ${tEmail} at ${tPhone} today.`;
|
|
28
|
+
const restored = await detokenizeText(paragraph);
|
|
29
|
+
|
|
30
|
+
expect(restored).toContain(email);
|
|
31
|
+
expect(restored).toContain(phone);
|
|
32
|
+
expect(restored).toBe(`Contact ${email} at ${phone} today.`);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('test_deep_decode_handles_paragraphs', async () => {
|
|
36
|
+
const email = "bob@work.com";
|
|
37
|
+
const tEmail = await encode(email);
|
|
38
|
+
|
|
39
|
+
const data = {
|
|
40
|
+
"email": tEmail,
|
|
41
|
+
"body": `Hi, I am ${tEmail}. Please call me.`,
|
|
42
|
+
"nested": [`Token: ${tEmail}`]
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const decoded: any = await deepDecode(data);
|
|
46
|
+
|
|
47
|
+
expect(decoded.email).toBe(email);
|
|
48
|
+
expect(decoded.body).toBe(`Hi, I am ${email}. Please call me.`);
|
|
49
|
+
expect(decoded.nested[0]).toBe(`Token: ${email}`);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('test_detokenize_text_lenient', async () => {
|
|
53
|
+
const bogus = "tkn-12345678@email.com";
|
|
54
|
+
const paragraph = `Hello ${bogus}`;
|
|
55
|
+
|
|
56
|
+
const restored = await detokenizeText(paragraph);
|
|
57
|
+
expect(restored).toBe(paragraph);
|
|
58
|
+
});
|
|
59
|
+
});
|