@privacyscrubber/sdk 2.4.5 → 2.4.6
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/README.md +50 -1
- package/browser.js +399 -19
- package/cli.js +119 -11
- package/examples/python-bridge/README.md +3 -3
- package/examples/python-bridge/fastapi_example.py +1 -1
- package/examples/python-bridge/langchain_example.py +1 -1
- package/examples/python-bridge/privacyscrubber/__init__.py +1 -1
- package/examples/python-bridge/privacyscrubber/bridge_daemon.cjs +2 -2
- package/examples/python-bridge/privacyscrubber/cli.py +1 -1
- package/examples/python-bridge/pyproject.toml +1 -1
- package/examples/python-bridge/setup.py +1 -1
- package/index.cjs +1153 -81
- package/index.d.ts +147 -2
- package/index.js +1153 -81
- package/package.json +13 -3
- package/ps-license-manager.js +1 -1
- package/ps-pii-engine.cjs +1 -1
- package/sarif.cjs +14 -2
- package/scrubber-core.cjs +1 -1
- package/vercel-ai.cjs +123 -34
- package/vercel-ai.d.ts +7 -0
- package/benchmark.js +0 -355
- package/examples/python-bridge/bridge_daemon.cjs +0 -260
- package/examples/python-bridge/langchain_adapter.py +0 -20
- package/examples/python-bridge/privacyscrubber.py +0 -36
- package/examples/python-bridge/test_python_bridge.py +0 -320
|
@@ -1,260 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* PrivacyScrubber ZTDS Python Bridge Daemon
|
|
4
|
-
*
|
|
5
|
-
* Provides a low-latency, zero-network-egress stdio NDJSON RPC daemon
|
|
6
|
-
* for Python, FastAPI, LangChain, and LlamaIndex environments.
|
|
7
|
-
*
|
|
8
|
-
* Communication protocol:
|
|
9
|
-
* - Request (stdin): Single-line JSON {"id": number|string, "action": string, ...}\n
|
|
10
|
-
* - Response (stdout): Single-line JSON {"id": number|string, "result": {...}}\n
|
|
11
|
-
* or {"id": number|string, "error": {"message": string, ...}}\n
|
|
12
|
-
*
|
|
13
|
-
* Zero external network egress. 100% in-memory RAM execution.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
'use strict';
|
|
17
|
-
|
|
18
|
-
const readline = require('readline');
|
|
19
|
-
const path = require('path');
|
|
20
|
-
|
|
21
|
-
// Resolve @privacyscrubber/sdk across local monorepo, parent dirs, cwd, and node_modules
|
|
22
|
-
let sdk;
|
|
23
|
-
const candidatePaths = [
|
|
24
|
-
process.env.PRIVACYSCRUBBER_SDK_PATH,
|
|
25
|
-
path.resolve(process.cwd(), 'npm-sdk/index.cjs'),
|
|
26
|
-
path.resolve(process.cwd(), 'node_modules/@privacyscrubber/sdk'),
|
|
27
|
-
path.resolve(__dirname, '../../../index.cjs'),
|
|
28
|
-
path.resolve(__dirname, '../../index.cjs'),
|
|
29
|
-
path.resolve(__dirname, '../index.cjs'),
|
|
30
|
-
path.resolve(__dirname, './index.cjs'),
|
|
31
|
-
'@privacyscrubber/sdk'
|
|
32
|
-
];
|
|
33
|
-
|
|
34
|
-
for (const p of candidatePaths) {
|
|
35
|
-
if (!p) continue;
|
|
36
|
-
try {
|
|
37
|
-
sdk = require(p);
|
|
38
|
-
break;
|
|
39
|
-
} catch (e) {
|
|
40
|
-
// try next candidate
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
if (!sdk) {
|
|
45
|
-
try {
|
|
46
|
-
const resolved = require.resolve('@privacyscrubber/sdk', { paths: [process.cwd(), __dirname] });
|
|
47
|
-
sdk = require(resolved);
|
|
48
|
-
} catch (e2) {}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (!sdk) {
|
|
52
|
-
process.stderr.write('Fatal: Could not load @privacyscrubber/sdk or index.cjs\n');
|
|
53
|
-
process.exit(1);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Parse initial CLI options
|
|
57
|
-
const args = process.argv.slice(2);
|
|
58
|
-
let initialProfile = process.env.PRIVACYSCRUBBER_PROFILE || 'General';
|
|
59
|
-
let licenseKey = process.env.PRIVACYSCRUBBER_KEY || process.env.PRIVACYSCRUBBER_LICENSE_KEY || '';
|
|
60
|
-
|
|
61
|
-
for (let i = 0; i < args.length; i++) {
|
|
62
|
-
if (args[i] === '--profile' && args[i + 1]) {
|
|
63
|
-
initialProfile = args[++i];
|
|
64
|
-
} else if (args[i] === '--license-key' && args[i + 1]) {
|
|
65
|
-
licenseKey = args[++i];
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Instantiate long-lived engine in local RAM
|
|
70
|
-
const engine = new sdk.PrivacyScrubberEngine({
|
|
71
|
-
licenseKey,
|
|
72
|
-
defaultProfile: initialProfile,
|
|
73
|
-
generateReceipt: true
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
function sendResponse(id, result, error = null) {
|
|
77
|
-
const payload = { id };
|
|
78
|
-
if (error) {
|
|
79
|
-
payload.error = {
|
|
80
|
-
message: error.message || String(error),
|
|
81
|
-
name: error.name || 'Error',
|
|
82
|
-
tier: error.tier,
|
|
83
|
-
limit: error.limit,
|
|
84
|
-
profile: error.profile
|
|
85
|
-
};
|
|
86
|
-
} else {
|
|
87
|
-
payload.result = result;
|
|
88
|
-
}
|
|
89
|
-
process.stdout.write(JSON.stringify(payload) + '\n');
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Setup readline interface on stdin
|
|
93
|
-
const rl = readline.createInterface({
|
|
94
|
-
input: process.stdin,
|
|
95
|
-
terminal: false
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
rl.on('line', (line) => {
|
|
99
|
-
const trimmed = line.trim();
|
|
100
|
-
if (!trimmed) return;
|
|
101
|
-
|
|
102
|
-
let req;
|
|
103
|
-
try {
|
|
104
|
-
req = JSON.parse(trimmed);
|
|
105
|
-
} catch (parseErr) {
|
|
106
|
-
sendResponse(null, null, new Error(`Invalid JSON request: ${parseErr.message}`));
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const { id, action } = req;
|
|
111
|
-
|
|
112
|
-
try {
|
|
113
|
-
switch (action) {
|
|
114
|
-
case 'ping': {
|
|
115
|
-
sendResponse(id, {
|
|
116
|
-
pong: true,
|
|
117
|
-
engine: 'PrivacyScrubber ZTDS',
|
|
118
|
-
version: '2.4.5',
|
|
119
|
-
uptime: process.uptime()
|
|
120
|
-
});
|
|
121
|
-
break;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
case 'sanitize': {
|
|
125
|
-
const text = req.text || '';
|
|
126
|
-
const options = {
|
|
127
|
-
profile: req.profile || engine.defaultProfile,
|
|
128
|
-
detectSecrets: req.detect_secrets ?? req.detectSecrets ?? false,
|
|
129
|
-
licenseKey: req.license_key || req.licenseKey || engine.licenseKey,
|
|
130
|
-
persistSession: req.persist_session ?? req.persistSession ?? true,
|
|
131
|
-
generateReceipt: req.generate_receipt ?? req.generateReceipt ?? true,
|
|
132
|
-
customRules: req.custom_rules || req.customRules || [],
|
|
133
|
-
ignoreList: req.ignore_list || req.ignoreList || null,
|
|
134
|
-
throwOnLimit: req.throw_on_limit ?? req.throwOnLimit ?? true
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
const res = engine.sanitize(text, options);
|
|
138
|
-
sendResponse(id, {
|
|
139
|
-
sanitized: res.scrubbedText,
|
|
140
|
-
scrubbed_text: res.scrubbedText,
|
|
141
|
-
token_map: res.tokenMap,
|
|
142
|
-
matches: res.matches,
|
|
143
|
-
count: res.count,
|
|
144
|
-
execution_ms: res.executionMs,
|
|
145
|
-
telemetry: res.telemetry,
|
|
146
|
-
audit_receipt: res.auditReceipt,
|
|
147
|
-
license: res.license
|
|
148
|
-
});
|
|
149
|
-
break;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
case 'restore': {
|
|
153
|
-
const text = req.text || '';
|
|
154
|
-
const tokenMap = req.token_map || req.tokenMap || null;
|
|
155
|
-
const res = engine.restore(text, tokenMap);
|
|
156
|
-
sendResponse(id, {
|
|
157
|
-
restored_text: res.restoredText,
|
|
158
|
-
restored_count: res.restoredCount
|
|
159
|
-
});
|
|
160
|
-
break;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
case 'sanitize_object': {
|
|
164
|
-
const data = req.data ?? {};
|
|
165
|
-
const persistSession = req.persist_session ?? req.persistSession ?? true;
|
|
166
|
-
let mergedIgnore = null;
|
|
167
|
-
const reqIgnore = req.ignore_list || req.ignoreList || null;
|
|
168
|
-
if (reqIgnore || engine.ignoreList.size > 0) {
|
|
169
|
-
mergedIgnore = new Set(engine.ignoreList);
|
|
170
|
-
if (reqIgnore) {
|
|
171
|
-
for (const item of reqIgnore) mergedIgnore.add(item);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
const options = {
|
|
175
|
-
profile: req.profile || engine.defaultProfile,
|
|
176
|
-
detectSecrets: req.detect_secrets ?? req.detectSecrets ?? false,
|
|
177
|
-
licenseKey: req.license_key || req.licenseKey || engine.licenseKey,
|
|
178
|
-
generateReceipt: req.generate_receipt ?? req.generateReceipt ?? true,
|
|
179
|
-
customRules: req.custom_rules || req.customRules || [],
|
|
180
|
-
ignoreList: mergedIgnore,
|
|
181
|
-
existingSessionMap: persistSession ? engine.sessionMap : (req.existing_session_map || req.existingSessionMap || {}),
|
|
182
|
-
throwOnLimit: req.throw_on_limit ?? req.throwOnLimit ?? true
|
|
183
|
-
};
|
|
184
|
-
|
|
185
|
-
const res = sdk.sanitizeObject(data, options);
|
|
186
|
-
if (persistSession && res.tokenMap) {
|
|
187
|
-
Object.assign(engine.sessionMap, res.tokenMap);
|
|
188
|
-
const keys = Object.keys(engine.sessionMap);
|
|
189
|
-
if (keys.length > engine.maxSessionTokens) {
|
|
190
|
-
const excess = keys.length - engine.maxSessionTokens;
|
|
191
|
-
for (let i = 0; i < excess; i++) {
|
|
192
|
-
delete engine.sessionMap[keys[i]];
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
sendResponse(id, {
|
|
197
|
-
sanitized: res.sanitized,
|
|
198
|
-
token_map: res.tokenMap,
|
|
199
|
-
count: res.count,
|
|
200
|
-
execution_ms: res.executionMs,
|
|
201
|
-
telemetry: res.telemetry,
|
|
202
|
-
audit_receipt: res.auditReceipt
|
|
203
|
-
});
|
|
204
|
-
break;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
case 'restore_object': {
|
|
208
|
-
const data = req.data ?? {};
|
|
209
|
-
const tokenMap = req.token_map || req.tokenMap || engine.sessionMap;
|
|
210
|
-
const restored = sdk.restoreObject(data, tokenMap);
|
|
211
|
-
sendResponse(id, {
|
|
212
|
-
restored
|
|
213
|
-
});
|
|
214
|
-
break;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
case 'reset_session': {
|
|
218
|
-
engine.resetSession();
|
|
219
|
-
sendResponse(id, { success: true });
|
|
220
|
-
break;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
case 'mark_false_positive': {
|
|
224
|
-
const token = req.token;
|
|
225
|
-
if (!token) {
|
|
226
|
-
throw new Error('Missing token argument for mark_false_positive');
|
|
227
|
-
}
|
|
228
|
-
const fpRes = engine.markFalsePositive(token);
|
|
229
|
-
sendResponse(id, fpRes);
|
|
230
|
-
break;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
case 'status': {
|
|
234
|
-
const tier = engine.license?.tier || 'FREE';
|
|
235
|
-
sendResponse(id, {
|
|
236
|
-
profile: engine.defaultProfile,
|
|
237
|
-
license_tier: tier,
|
|
238
|
-
is_pro: Boolean(engine.license?.valid && ['SDK', 'OEM', 'ENTERPRISE', 'PRO', 'TEAMS'].includes(tier)),
|
|
239
|
-
session_tokens_count: Object.keys(engine.sessionMap).length,
|
|
240
|
-
ignore_list_size: engine.ignoreList.size,
|
|
241
|
-
memory_usage: process.memoryUsage()
|
|
242
|
-
});
|
|
243
|
-
break;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
default:
|
|
247
|
-
sendResponse(id, null, new Error(`Unknown action: ${action}`));
|
|
248
|
-
break;
|
|
249
|
-
}
|
|
250
|
-
} catch (err) {
|
|
251
|
-
sendResponse(id, null, err);
|
|
252
|
-
}
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
rl.on('close', () => {
|
|
256
|
-
process.exit(0);
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
process.on('SIGINT', () => process.exit(0));
|
|
260
|
-
process.on('SIGTERM', () => process.exit(0));
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
PrivacyScrubber ZTDS Adapter for LangChain
|
|
3
|
-
Zero-Trust Data Sanitization & Local PII Shield for LangChain Pipelines.
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
from __future__ import annotations
|
|
7
|
-
|
|
8
|
-
from privacyscrubber.langchain import (
|
|
9
|
-
ZTDSCallbackHandler,
|
|
10
|
-
ZTDSDocumentTransformer,
|
|
11
|
-
create_ztds_lcel_runnables,
|
|
12
|
-
LANGCHAIN_INSTALLED,
|
|
13
|
-
)
|
|
14
|
-
|
|
15
|
-
__all__ = [
|
|
16
|
-
"ZTDSCallbackHandler",
|
|
17
|
-
"ZTDSDocumentTransformer",
|
|
18
|
-
"create_ztds_lcel_runnables",
|
|
19
|
-
"LANGCHAIN_INSTALLED",
|
|
20
|
-
]
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
PrivacyScrubber ZTDS Client for Python
|
|
3
|
-
Zero-Trust Data Sanitization & Local PII Shield for LLM / AI Pipelines.
|
|
4
|
-
|
|
5
|
-
100% in-memory client-side execution. Zero cloud egress. Sub-millisecond latency.
|
|
6
|
-
RFC v1.0 standard-compliant Zero-Trust Data Sanitization engine.
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
from __future__ import annotations
|
|
10
|
-
|
|
11
|
-
from privacyscrubber import (
|
|
12
|
-
PrivacyScrubberClient,
|
|
13
|
-
PrivacyScrubber,
|
|
14
|
-
AsyncPrivacyScrubberClient,
|
|
15
|
-
AsyncPrivacyScrubber,
|
|
16
|
-
SanitizeResult,
|
|
17
|
-
RestoreResult,
|
|
18
|
-
ObjectSanitizeResult,
|
|
19
|
-
PrivacyScrubberError,
|
|
20
|
-
PrivacyScrubberLicenseError,
|
|
21
|
-
__version__,
|
|
22
|
-
)
|
|
23
|
-
from privacyscrubber.client import _find_daemon_path
|
|
24
|
-
|
|
25
|
-
__all__ = [
|
|
26
|
-
"PrivacyScrubberClient",
|
|
27
|
-
"PrivacyScrubber",
|
|
28
|
-
"AsyncPrivacyScrubberClient",
|
|
29
|
-
"AsyncPrivacyScrubber",
|
|
30
|
-
"SanitizeResult",
|
|
31
|
-
"RestoreResult",
|
|
32
|
-
"ObjectSanitizeResult",
|
|
33
|
-
"PrivacyScrubberError",
|
|
34
|
-
"PrivacyScrubberLicenseError",
|
|
35
|
-
"__version__",
|
|
36
|
-
]
|
|
@@ -1,320 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
PrivacyScrubber ZTDS Python Bridge - Comprehensive Test Suite
|
|
4
|
-
Zero-Trust Data Sanitization & In-Memory PII Shield Verification.
|
|
5
|
-
|
|
6
|
-
Verifies:
|
|
7
|
-
1. Subprocess lifecycle, stdio NDJSON RPC, ping & status.
|
|
8
|
-
2. Synchronous and Asynchronous client execution parity.
|
|
9
|
-
3. Round-trip restoration fidelity (Zero Data Loss).
|
|
10
|
-
4. Specialized vertical profiles (Healthcare, Financial, Legal, HR).
|
|
11
|
-
5. Nested dictionary and list object sanitization / restoration.
|
|
12
|
-
6. False-positive ignore list management and session reset.
|
|
13
|
-
7. Sub-millisecond latency benchmark (<2ms over stdio IPC).
|
|
14
|
-
8. Thread-safe concurrent execution under load.
|
|
15
|
-
9. LangChain callback handler and LCEL runnable integration.
|
|
16
|
-
"""
|
|
17
|
-
|
|
18
|
-
from __future__ import annotations
|
|
19
|
-
|
|
20
|
-
import asyncio
|
|
21
|
-
import os
|
|
22
|
-
import sys
|
|
23
|
-
import threading
|
|
24
|
-
import time
|
|
25
|
-
import unittest
|
|
26
|
-
|
|
27
|
-
# Ensure current directory is in sys.path
|
|
28
|
-
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
29
|
-
|
|
30
|
-
from privacyscrubber import (
|
|
31
|
-
PrivacyScrubberClient,
|
|
32
|
-
AsyncPrivacyScrubberClient,
|
|
33
|
-
PrivacyScrubberError,
|
|
34
|
-
SanitizeResult,
|
|
35
|
-
RestoreResult
|
|
36
|
-
)
|
|
37
|
-
from langchain_adapter import (
|
|
38
|
-
ZTDSCallbackHandler,
|
|
39
|
-
ZTDSDocumentTransformer,
|
|
40
|
-
create_ztds_lcel_runnables
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
class TestPrivacyScrubberBridge(unittest.TestCase):
|
|
45
|
-
@classmethod
|
|
46
|
-
def setUpClass(cls):
|
|
47
|
-
cls.client = PrivacyScrubberClient(default_profile="General")
|
|
48
|
-
|
|
49
|
-
@classmethod
|
|
50
|
-
def tearDownClass(cls):
|
|
51
|
-
cls.client.close()
|
|
52
|
-
|
|
53
|
-
def setUp(self):
|
|
54
|
-
self.client.reset_session()
|
|
55
|
-
|
|
56
|
-
def test_01_handshake_and_ping(self):
|
|
57
|
-
"""Verifies sidecar daemon handshake, engine version, and uptime."""
|
|
58
|
-
res = self.client.ping()
|
|
59
|
-
self.assertTrue(res.get("pong"))
|
|
60
|
-
self.assertEqual(res.get("engine"), "PrivacyScrubber ZTDS")
|
|
61
|
-
self.assertIn("version", res)
|
|
62
|
-
self.assertGreater(res.get("uptime", 0), 0)
|
|
63
|
-
|
|
64
|
-
status = self.client.status()
|
|
65
|
-
self.assertEqual(status.get("profile"), "General")
|
|
66
|
-
self.assertIn("license_tier", status)
|
|
67
|
-
|
|
68
|
-
def test_02_sync_sanitize_and_restore(self):
|
|
69
|
-
"""Verifies text sanitization and 100% round-trip restoration fidelity."""
|
|
70
|
-
raw_text = "Contact agent Johnathan Archer at jarcher@starfleet.mil or +1-415-555-0199 for contract details."
|
|
71
|
-
san_res = self.client.sanitize(raw_text)
|
|
72
|
-
|
|
73
|
-
self.assertIsInstance(san_res, SanitizeResult)
|
|
74
|
-
self.assertNotIn("jarcher@starfleet.mil", san_res.sanitized)
|
|
75
|
-
self.assertNotIn("+1-415-555-0199", san_res.sanitized)
|
|
76
|
-
self.assertIn("[NAME_1]", san_res.sanitized)
|
|
77
|
-
self.assertIn("[EMAIL_1]", san_res.sanitized)
|
|
78
|
-
self.assertIn("[PHONE_1]", san_res.sanitized)
|
|
79
|
-
self.assertGreaterEqual(san_res.count, 3)
|
|
80
|
-
|
|
81
|
-
# In-memory restoration using engine session
|
|
82
|
-
rest_res = self.client.restore(san_res.sanitized)
|
|
83
|
-
self.assertIsInstance(rest_res, RestoreResult)
|
|
84
|
-
self.assertEqual(rest_res.restored_text, raw_text)
|
|
85
|
-
self.assertEqual(rest_res.restored_count, san_res.count)
|
|
86
|
-
|
|
87
|
-
def test_03_async_client(self):
|
|
88
|
-
"""Verifies asynchronous client execution in asyncio event loop."""
|
|
89
|
-
async def _run_async_test():
|
|
90
|
-
async with AsyncPrivacyScrubberClient(default_profile="General") as async_client:
|
|
91
|
-
ping = await async_client.ping()
|
|
92
|
-
self.assertTrue(ping.get("pong"))
|
|
93
|
-
|
|
94
|
-
raw = "Executive Director Clark Kent (ckent@dailyplanet.com) approved payment."
|
|
95
|
-
san = await async_client.sanitize(raw)
|
|
96
|
-
self.assertNotIn("ckent@dailyplanet.com", san.sanitized)
|
|
97
|
-
self.assertIn("[EMAIL_1]", san.sanitized)
|
|
98
|
-
|
|
99
|
-
rest = await async_client.restore(san.sanitized)
|
|
100
|
-
self.assertEqual(rest.restored_text, raw)
|
|
101
|
-
|
|
102
|
-
asyncio.run(_run_async_test())
|
|
103
|
-
|
|
104
|
-
def test_04_industry_profiles(self):
|
|
105
|
-
"""Verifies specialized industry profile tokenization."""
|
|
106
|
-
# Healthcare Profile
|
|
107
|
-
med_text = "Patient Bruce Wayne (DOB: 1972-02-19, SSN: 000-44-5555) admitted to ICU."
|
|
108
|
-
med_res = self.client.sanitize(med_text, profile="Healthcare")
|
|
109
|
-
self.assertNotIn("Bruce Wayne", med_res.sanitized)
|
|
110
|
-
self.assertNotIn("000-44-5555", med_res.sanitized)
|
|
111
|
-
self.assertIn("[DATE_1]", med_res.sanitized)
|
|
112
|
-
|
|
113
|
-
# Financial Profile
|
|
114
|
-
fin_text = "Wire $1,500,000 to IBAN GB29XAAA14151617181920 for client Bruce Wayne."
|
|
115
|
-
fin_res = self.client.sanitize(fin_text, profile="Financial")
|
|
116
|
-
self.assertNotIn("GB29XAAA14151617181920", fin_res.sanitized)
|
|
117
|
-
|
|
118
|
-
# Clean restoration check
|
|
119
|
-
med_restored = self.client.restore(med_res.sanitized, token_map=med_res.token_map)
|
|
120
|
-
self.assertEqual(med_restored.restored_text, med_text)
|
|
121
|
-
|
|
122
|
-
def test_05_dict_and_list_sanitization(self):
|
|
123
|
-
"""Verifies structured dictionary/list sanitization and restoration."""
|
|
124
|
-
payload = {
|
|
125
|
-
"user_id": 1042,
|
|
126
|
-
"profile": {
|
|
127
|
-
"full_name": "Tony Stark",
|
|
128
|
-
"email": "tony@starkindustries.com",
|
|
129
|
-
"phone": "+1-212-555-0100"
|
|
130
|
-
},
|
|
131
|
-
"tags": ["vip", "avenger"],
|
|
132
|
-
"notes": "Payment from tony@starkindustries.com approved."
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
san_obj = self.client.sanitize_object(payload)
|
|
136
|
-
self.assertEqual(san_obj.sanitized["profile"]["full_name"], "[NAME_1]")
|
|
137
|
-
self.assertEqual(san_obj.sanitized["profile"]["email"], "[EMAIL_1]")
|
|
138
|
-
self.assertEqual(san_obj.sanitized["user_id"], 1042)
|
|
139
|
-
self.assertEqual(san_obj.sanitized["tags"], ["vip", "avenger"])
|
|
140
|
-
|
|
141
|
-
rest_obj = self.client.restore_object(san_obj.sanitized, token_map=san_obj.token_map)
|
|
142
|
-
self.assertEqual(rest_obj, payload)
|
|
143
|
-
|
|
144
|
-
def test_06_false_positive_and_reset(self):
|
|
145
|
-
"""Verifies false positive marking and session clearing."""
|
|
146
|
-
text = "Contact Jane Doe at jane@example.com."
|
|
147
|
-
res1 = self.client.sanitize(text)
|
|
148
|
-
self.assertIn("[NAME_1]", res1.sanitized)
|
|
149
|
-
|
|
150
|
-
# Mark Jane Doe as false positive
|
|
151
|
-
fp_res = self.client.mark_false_positive("[NAME_1]")
|
|
152
|
-
self.assertTrue(fp_res.get("success"))
|
|
153
|
-
self.assertEqual(fp_res.get("original"), "Jane Doe")
|
|
154
|
-
|
|
155
|
-
# Next sanitize should not mask Jane Doe
|
|
156
|
-
res2 = self.client.sanitize(text)
|
|
157
|
-
self.assertIn("Jane Doe", res2.sanitized)
|
|
158
|
-
self.assertIn("[EMAIL_1]", res2.sanitized)
|
|
159
|
-
|
|
160
|
-
# Reset session
|
|
161
|
-
self.assertTrue(self.client.reset_session())
|
|
162
|
-
status = self.client.status()
|
|
163
|
-
self.assertEqual(status.get("session_tokens_count"), 0)
|
|
164
|
-
|
|
165
|
-
def test_07_sub_millisecond_benchmark(self):
|
|
166
|
-
"""Measures stdio IPC latency over 50 iterations; asserts average < 3ms."""
|
|
167
|
-
test_string = "Customer Arthur Dent (adent@hitchhiker.org, phone 44-20-7946-0919) inquiry."
|
|
168
|
-
latencies = []
|
|
169
|
-
|
|
170
|
-
# Warm up
|
|
171
|
-
self.client.sanitize(test_string)
|
|
172
|
-
|
|
173
|
-
for _ in range(50):
|
|
174
|
-
t0 = time.perf_counter()
|
|
175
|
-
self.client.sanitize(test_string, persist_session=False)
|
|
176
|
-
t1 = time.perf_counter()
|
|
177
|
-
latencies.append((t1 - t0) * 1000.0)
|
|
178
|
-
|
|
179
|
-
avg_latency = sum(latencies) / len(latencies)
|
|
180
|
-
print(f"\n[Benchmark] 50 Stdio IPC Iterations: Avg Latency = {avg_latency:.2f}ms | Min = {min(latencies):.2f}ms | Max = {max(latencies):.2f}ms")
|
|
181
|
-
self.assertLess(avg_latency, 5.0, f"Average latency {avg_latency:.2f}ms exceeded 5.0ms threshold")
|
|
182
|
-
|
|
183
|
-
def test_08_multithreaded_concurrency(self):
|
|
184
|
-
"""Verifies thread-safety across 10 concurrent threads."""
|
|
185
|
-
errors = []
|
|
186
|
-
|
|
187
|
-
def worker(thread_idx: int):
|
|
188
|
-
try:
|
|
189
|
-
name = f"Tester {thread_idx}"
|
|
190
|
-
email = f"user_{thread_idx}@concurrent.io"
|
|
191
|
-
sample = f"Hello from {name}, reach me at {email}."
|
|
192
|
-
res = self.client.sanitize(sample, persist_session=False)
|
|
193
|
-
if f"[EMAIL_1]" not in res.sanitized:
|
|
194
|
-
errors.append(f"Thread {thread_idx} missing email token: {res.sanitized}")
|
|
195
|
-
rest = self.client.restore(res.sanitized, token_map=res.token_map)
|
|
196
|
-
if rest.restored_text != sample:
|
|
197
|
-
errors.append(f"Thread {thread_idx} restoration mismatch: {rest.restored_text} != {sample}")
|
|
198
|
-
except Exception as e:
|
|
199
|
-
errors.append(f"Thread {thread_idx} failed: {e}")
|
|
200
|
-
|
|
201
|
-
threads = [threading.Thread(target=worker, args=(i,)) for i in range(10)]
|
|
202
|
-
for t in threads:
|
|
203
|
-
t.start()
|
|
204
|
-
for t in threads:
|
|
205
|
-
t.join()
|
|
206
|
-
|
|
207
|
-
self.assertEqual(len(errors), 0, f"Concurrent thread errors: {errors}")
|
|
208
|
-
|
|
209
|
-
def test_09_langchain_callback_flow(self):
|
|
210
|
-
"""Verifies LangChain ZTDSCallbackHandler interception and restoration."""
|
|
211
|
-
handler = ZTDSCallbackHandler(scrubber=self.client, profile="General")
|
|
212
|
-
|
|
213
|
-
prompt = ["Review case file for John Watson (SSN 111-22-3333, email jwatson@bakerst.co.uk)."]
|
|
214
|
-
handler.on_llm_start(serialized={}, prompts=prompt, run_id="unit_run_1")
|
|
215
|
-
|
|
216
|
-
self.assertNotIn("jwatson@bakerst.co.uk", prompt[0])
|
|
217
|
-
self.assertIn("[EMAIL_1]", prompt[0])
|
|
218
|
-
|
|
219
|
-
class MockGen:
|
|
220
|
-
def __init__(self, text):
|
|
221
|
-
self.text = text
|
|
222
|
-
|
|
223
|
-
class MockRes:
|
|
224
|
-
def __init__(self, text):
|
|
225
|
-
self.generations = [[MockGen(text)]]
|
|
226
|
-
|
|
227
|
-
ai_out = MockRes("Case file approved for [NAME_1] ([EMAIL_1]).")
|
|
228
|
-
handler.on_llm_end(ai_out, run_id="unit_run_1")
|
|
229
|
-
|
|
230
|
-
final_text = ai_out.generations[0][0].text
|
|
231
|
-
self.assertIn("John Watson", final_text)
|
|
232
|
-
self.assertIn("jwatson@bakerst.co.uk", final_text)
|
|
233
|
-
|
|
234
|
-
def test_10_lcel_runnables(self):
|
|
235
|
-
"""Verifies create_ztds_lcel_runnables() pre and post functions."""
|
|
236
|
-
scrub_fn, restore_fn = create_ztds_lcel_runnables(self.client, profile="General")
|
|
237
|
-
|
|
238
|
-
raw_input = "Send memo to Peter Parker (pparker@dailybugle.com)."
|
|
239
|
-
sanitized = scrub_fn(raw_input)
|
|
240
|
-
self.assertIn("[NAME_1]", sanitized)
|
|
241
|
-
self.assertIn("[EMAIL_1]", sanitized)
|
|
242
|
-
|
|
243
|
-
ai_reply = "Memo dispatched to [NAME_1] ([EMAIL_1])."
|
|
244
|
-
restored = restore_fn(ai_reply)
|
|
245
|
-
self.assertEqual(restored, "Memo dispatched to Peter Parker (pparker@dailybugle.com).")
|
|
246
|
-
|
|
247
|
-
def test_11_cli_commands(self):
|
|
248
|
-
"""Verifies privacyscrubber.cli commands (ping, status, sanitize, restore, version)."""
|
|
249
|
-
import subprocess
|
|
250
|
-
python_bin = sys.executable
|
|
251
|
-
cli_module = "privacyscrubber.cli"
|
|
252
|
-
bridge_dir = os.path.dirname(os.path.abspath(__file__))
|
|
253
|
-
sub_env = {**os.environ, "PYTHONPATH": bridge_dir}
|
|
254
|
-
|
|
255
|
-
# 1. Ping
|
|
256
|
-
ping_out = subprocess.check_output(
|
|
257
|
-
[python_bin, "-m", cli_module, "ping"],
|
|
258
|
-
text=True,
|
|
259
|
-
cwd=bridge_dir,
|
|
260
|
-
env=sub_env
|
|
261
|
-
)
|
|
262
|
-
self.assertIn("pong", ping_out)
|
|
263
|
-
self.assertIn("PrivacyScrubber ZTDS", ping_out)
|
|
264
|
-
|
|
265
|
-
# 2. Status
|
|
266
|
-
status_out = subprocess.check_output(
|
|
267
|
-
[python_bin, "-m", cli_module, "status"],
|
|
268
|
-
text=True,
|
|
269
|
-
cwd=bridge_dir,
|
|
270
|
-
env=sub_env
|
|
271
|
-
)
|
|
272
|
-
self.assertIn("license_tier", status_out)
|
|
273
|
-
|
|
274
|
-
# 3. Sanitize argument
|
|
275
|
-
san_out = subprocess.check_output(
|
|
276
|
-
[python_bin, "-m", cli_module, "sanitize", "Send to Alice Cooper alice@corp.com"],
|
|
277
|
-
text=True,
|
|
278
|
-
cwd=bridge_dir,
|
|
279
|
-
env=sub_env
|
|
280
|
-
)
|
|
281
|
-
self.assertIn("[NAME_1]", san_out)
|
|
282
|
-
self.assertIn("[EMAIL_1]", san_out)
|
|
283
|
-
self.assertNotIn("alice@corp.com", san_out)
|
|
284
|
-
|
|
285
|
-
# 4. Sanitize stdin
|
|
286
|
-
pipe_p = subprocess.Popen(
|
|
287
|
-
[python_bin, "-m", cli_module, "sanitize"],
|
|
288
|
-
stdin=subprocess.PIPE,
|
|
289
|
-
stdout=subprocess.PIPE,
|
|
290
|
-
text=True,
|
|
291
|
-
cwd=bridge_dir,
|
|
292
|
-
env=sub_env
|
|
293
|
-
)
|
|
294
|
-
stdout, _ = pipe_p.communicate(input="Confidential email: test@domain.org")
|
|
295
|
-
self.assertIn("[EMAIL_1]", stdout)
|
|
296
|
-
self.assertNotIn("test@domain.org", stdout)
|
|
297
|
-
|
|
298
|
-
# 5. Restore with token map
|
|
299
|
-
token_map_json = '{"[NAME_1]": "Bruce Wayne"}'
|
|
300
|
-
restore_out = subprocess.check_output(
|
|
301
|
-
[python_bin, "-m", cli_module, "restore", "Hello [NAME_1]", "--token-map", token_map_json],
|
|
302
|
-
text=True,
|
|
303
|
-
cwd=bridge_dir,
|
|
304
|
-
env=sub_env
|
|
305
|
-
)
|
|
306
|
-
self.assertIn("Hello Bruce Wayne", restore_out)
|
|
307
|
-
|
|
308
|
-
# 6. Version
|
|
309
|
-
ver_out = subprocess.check_output(
|
|
310
|
-
[python_bin, "-m", cli_module, "--version"],
|
|
311
|
-
text=True,
|
|
312
|
-
cwd=bridge_dir,
|
|
313
|
-
env=sub_env
|
|
314
|
-
)
|
|
315
|
-
self.assertIn("2.4.5", ver_out)
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
if __name__ == "__main__":
|
|
319
|
-
unittest.main()
|
|
320
|
-
|