@privacyscrubber/sdk 2.4.4 → 2.4.5

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/cli.js CHANGED
@@ -32,7 +32,7 @@ for (const a of rawArgs) {
32
32
  }
33
33
  }
34
34
 
35
- let sdkVersion = '2.4.4';
35
+ let sdkVersion = '2.4.5';
36
36
  try {
37
37
  const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8'));
38
38
  sdkVersion = pkg.version || sdkVersion;
@@ -115,7 +115,7 @@ rl.on('line', (line) => {
115
115
  sendResponse(id, {
116
116
  pong: true,
117
117
  engine: 'PrivacyScrubber ZTDS',
118
- version: '2.4.4',
118
+ version: '2.4.5',
119
119
  uptime: process.uptime()
120
120
  });
121
121
  break;
@@ -59,7 +59,7 @@ if FASTAPI_AVAILABLE:
59
59
  app = FastAPI(
60
60
  title="PrivacyScrubber ZTDS Sidecar Proxy",
61
61
  description="Zero-Trust Data Sanitization & In-Memory PII Shield for LLM Pipelines",
62
- version="2.4.4"
62
+ version="2.4.5"
63
63
  )
64
64
 
65
65
  @app.on_event("startup")
@@ -24,7 +24,7 @@ from privacyscrubber.exceptions import (
24
24
  PrivacyScrubberLicenseError,
25
25
  )
26
26
 
27
- __version__ = "2.4.4"
27
+ __version__ = "2.4.5"
28
28
  __author__ = "Ilya Sibiryakov (BrandMeWeb)"
29
29
  __license__ = "Proprietary"
30
30
 
@@ -115,7 +115,7 @@ rl.on('line', (line) => {
115
115
  sendResponse(id, {
116
116
  pong: true,
117
117
  engine: 'PrivacyScrubber ZTDS',
118
- version: '2.4.4',
118
+ version: '2.4.5',
119
119
  uptime: process.uptime()
120
120
  });
121
121
  break;
@@ -50,7 +50,7 @@ def main() -> None:
50
50
  args = parser.parse_args()
51
51
 
52
52
  if args.version:
53
- print("PrivacyScrubber ZTDS Python SDK v2.4.4")
53
+ print("PrivacyScrubber ZTDS Python SDK v2.4.5")
54
54
  sys.exit(0)
55
55
 
56
56
  if not args.command:
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "privacyscrubber"
7
- version = "2.4.4"
7
+ version = "2.4.5"
8
8
  description = "Zero-Trust Data Sanitization & In-Memory PII Shield for Python AI Pipelines (ZTDS RFC v1.0)"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -13,7 +13,7 @@ with open(os.path.join(here, "README.md"), encoding="utf-8") as f:
13
13
 
14
14
  setup(
15
15
  name="privacyscrubber",
16
- version="2.4.4",
16
+ version="2.4.5",
17
17
  description="Zero-Trust Data Sanitization & In-Memory PII Shield for Python AI Pipelines (ZTDS RFC v1.0)",
18
18
  long_description=long_description,
19
19
  long_description_content_type="text/markdown",
@@ -312,7 +312,7 @@ class TestPrivacyScrubberBridge(unittest.TestCase):
312
312
  cwd=bridge_dir,
313
313
  env=sub_env
314
314
  )
315
- self.assertIn("2.4.4", ver_out)
315
+ self.assertIn("2.4.5", ver_out)
316
316
 
317
317
 
318
318
  if __name__ == "__main__":
package/index.cjs CHANGED
@@ -540,8 +540,17 @@ function sanitize(text, options = {}) {
540
540
  wasTruncated = true;
541
541
  }
542
542
 
543
+ // Filter user-supplied custom rules against catastrophic backtracking (PS-VDP-2026-10-002 parity)
544
+ const nestedQuantifierRegex = /\([^)]*(\+|\*|\{\d+,?\d*\})\)[+*]|\([^)]*(\+|\*|\{\d+,?\d*\})\)\{\d+,?\d*\}|\([^)]*\|[^)]*\)(?:[+*]|\{\d+,?\d*\})/;
545
+ const safeCustomRules = (Array.isArray(customRules) ? customRules : []).filter(r => {
546
+ const pat = typeof r === 'string' ? r : (r && (r.pattern || (r.regex && r.regex.source)));
547
+ if (!pat || typeof pat !== 'string' || pat.length > 500) return false;
548
+ if (nestedQuantifierRegex.test(pat)) return false;
549
+ return true;
550
+ });
551
+
543
552
  // Auto-inject DevOps Secrets rules when in Dev profile or when detectSecrets is requested
544
- const activeRules = [...customRules];
553
+ const activeRules = [...safeCustomRules];
545
554
  if (profile.toLowerCase() === 'dev' || detectSecrets === true) {
546
555
  DEVOPS_SECRETS_DETECTOR.forEach(rule => {
547
556
  activeRules.push({
@@ -1952,7 +1961,7 @@ function formatCefEvent(eventData = {}) {
1952
1961
  const {
1953
1962
  vendor = 'PrivacyScrubber',
1954
1963
  product = 'ZTDS',
1955
- version = '2.4.4',
1964
+ version = '2.4.5',
1956
1965
  eventClassId = 'PII_REDACTION',
1957
1966
  eventName = 'Zero-Trust PII Sanitization Executed',
1958
1967
  riskLevel = 'LOW EXPOSURE',
@@ -2021,7 +2030,7 @@ function formatSyslogEvent(eventData = {}) {
2021
2030
  function formatJsonlEvent(eventData = {}) {
2022
2031
  const {
2023
2032
  timestamp = new Date().toISOString(),
2024
- version = '2.4.4',
2033
+ version = '2.4.5',
2025
2034
  protocol = 'ZTDS',
2026
2035
  event = 'sanitization',
2027
2036
  riskLevel = 'LOW EXPOSURE',
@@ -2465,6 +2474,32 @@ function createGuardedTools(engineOrOptions = {}, toolConfig = {}) {
2465
2474
 
2466
2475
  const baseCwd = toolConfig.cwd || process.cwd();
2467
2476
  const defaultProfile = toolConfig.profile || engine.defaultProfile || 'Dev';
2477
+ const allowOutsideWorkspace = toolConfig.allowOutsideWorkspace === true;
2478
+
2479
+ function resolveSafeToolPath(inputPath, toolName, paramName = 'filePath') {
2480
+ const workspaceRoot = fs.existsSync(baseCwd) ? fs.realpathSync(baseCwd) : path.resolve(baseCwd);
2481
+ const resolvedPath = path.resolve(workspaceRoot, inputPath);
2482
+ if (allowOutsideWorkspace) return resolvedPath;
2483
+
2484
+ const relToRoot = path.relative(workspaceRoot, resolvedPath);
2485
+ if (relToRoot.startsWith('..') || path.isAbsolute(relToRoot)) {
2486
+ throw new Error(`[${toolName}] Security policy violation — target '${paramName}' resolves outside the active workspace directory (path traversal blocked).`);
2487
+ }
2488
+ let checkCursor = resolvedPath;
2489
+ while (!fs.existsSync(checkCursor)) {
2490
+ const parentCursor = path.dirname(checkCursor);
2491
+ if (parentCursor === checkCursor) break;
2492
+ checkCursor = parentCursor;
2493
+ }
2494
+ if (fs.existsSync(checkCursor)) {
2495
+ const realCheck = fs.realpathSync(checkCursor);
2496
+ const relReal = path.relative(workspaceRoot, realCheck);
2497
+ if (relReal.startsWith('..') || path.isAbsolute(relReal)) {
2498
+ throw new Error(`[${toolName}] Security policy violation — target path symlink escapes the active workspace directory.`);
2499
+ }
2500
+ }
2501
+ return resolvedPath;
2502
+ }
2468
2503
 
2469
2504
  const tools = {
2470
2505
  guardExec: {
@@ -2484,7 +2519,7 @@ function createGuardedTools(engineOrOptions = {}, toolConfig = {}) {
2484
2519
  if (!command || typeof command !== 'string') {
2485
2520
  throw new Error('[guard_exec] Missing or invalid parameter: command must be a string.');
2486
2521
  }
2487
- const execCwd = cwd ? path.resolve(baseCwd, cwd) : baseCwd;
2522
+ const execCwd = cwd ? resolveSafeToolPath(cwd, 'guard_exec', 'cwd') : baseCwd;
2488
2523
  let stdout = '';
2489
2524
  let stderr = '';
2490
2525
  let exitCode = 0;
@@ -2548,7 +2583,7 @@ function createGuardedTools(engineOrOptions = {}, toolConfig = {}) {
2548
2583
  if (!filePath || typeof filePath !== 'string') {
2549
2584
  throw new Error('[guard_read_file] Missing or invalid parameter: filePath must be a string.');
2550
2585
  }
2551
- const resolved = path.resolve(baseCwd, filePath);
2586
+ const resolved = resolveSafeToolPath(filePath, 'guard_read_file', 'filePath');
2552
2587
  if (!fs.existsSync(resolved)) {
2553
2588
  throw new Error(`[guard_read_file] File not found: ${resolved}`);
2554
2589
  }
@@ -2639,7 +2674,8 @@ function createGuardedTools(engineOrOptions = {}, toolConfig = {}) {
2639
2674
  required: ['filePath', 'content']
2640
2675
  },
2641
2676
  execute: async ({ filePath, content, createBackup = true } = {}) => {
2642
- const patchResult = applyPatch(filePath, content, {
2677
+ const safeFilePath = resolveSafeToolPath(filePath, 'guard_apply_patch', 'filePath');
2678
+ const patchResult = applyPatch(safeFilePath, content, {
2643
2679
  tokenMap: engine.sessionMap,
2644
2680
  createBackup
2645
2681
  });
@@ -2670,7 +2706,7 @@ function createGuardedTools(engineOrOptions = {}, toolConfig = {}) {
2670
2706
  required: []
2671
2707
  },
2672
2708
  execute: async ({ staged = false, cwd, profile = defaultProfile } = {}) => {
2673
- const repoCwd = cwd ? path.resolve(baseCwd, cwd) : baseCwd;
2709
+ const repoCwd = cwd ? resolveSafeToolPath(cwd, 'guard_git_diff', 'cwd') : baseCwd;
2674
2710
  const gitCmd = staged ? 'git diff --cached' : 'git diff';
2675
2711
  let diffOutput = '';
2676
2712
 
package/index.js CHANGED
@@ -542,8 +542,17 @@ export function sanitize(text, options = {}) {
542
542
  wasTruncated = true;
543
543
  }
544
544
 
545
+ // Filter user-supplied custom rules against catastrophic backtracking (PS-VDP-2026-10-002 parity)
546
+ const nestedQuantifierRegex = /\([^)]*(\+|\*|\{\d+,?\d*\})\)[+*]|\([^)]*(\+|\*|\{\d+,?\d*\})\)\{\d+,?\d*\}|\([^)]*\|[^)]*\)(?:[+*]|\{\d+,?\d*\})/;
547
+ const safeCustomRules = (Array.isArray(customRules) ? customRules : []).filter(r => {
548
+ const pat = typeof r === 'string' ? r : (r && (r.pattern || (r.regex && r.regex.source)));
549
+ if (!pat || typeof pat !== 'string' || pat.length > 500) return false;
550
+ if (nestedQuantifierRegex.test(pat)) return false;
551
+ return true;
552
+ });
553
+
545
554
  // Auto-inject DevOps Secrets rules when in Dev profile or when detectSecrets is requested
546
- const activeRules = [...customRules];
555
+ const activeRules = [...safeCustomRules];
547
556
  if (profile.toLowerCase() === 'dev' || detectSecrets === true) {
548
557
  DEVOPS_SECRETS_DETECTOR.forEach(rule => {
549
558
  activeRules.push({
@@ -1954,7 +1963,7 @@ export function formatCefEvent(eventData = {}) {
1954
1963
  const {
1955
1964
  vendor = 'PrivacyScrubber',
1956
1965
  product = 'ZTDS',
1957
- version = '2.4.4',
1966
+ version = '2.4.5',
1958
1967
  eventClassId = 'PII_REDACTION',
1959
1968
  eventName = 'Zero-Trust PII Sanitization Executed',
1960
1969
  riskLevel = 'LOW EXPOSURE',
@@ -2023,7 +2032,7 @@ export function formatSyslogEvent(eventData = {}) {
2023
2032
  export function formatJsonlEvent(eventData = {}) {
2024
2033
  const {
2025
2034
  timestamp = new Date().toISOString(),
2026
- version = '2.4.4',
2035
+ version = '2.4.5',
2027
2036
  protocol = 'ZTDS',
2028
2037
  event = 'sanitization',
2029
2038
  riskLevel = 'LOW EXPOSURE',
@@ -2467,6 +2476,32 @@ export function createGuardedTools(engineOrOptions = {}, toolConfig = {}) {
2467
2476
 
2468
2477
  const baseCwd = toolConfig.cwd || process.cwd();
2469
2478
  const defaultProfile = toolConfig.profile || engine.defaultProfile || 'Dev';
2479
+ const allowOutsideWorkspace = toolConfig.allowOutsideWorkspace === true;
2480
+
2481
+ function resolveSafeToolPath(inputPath, toolName, paramName = 'filePath') {
2482
+ const workspaceRoot = fs.existsSync(baseCwd) ? fs.realpathSync(baseCwd) : path.resolve(baseCwd);
2483
+ const resolvedPath = path.resolve(workspaceRoot, inputPath);
2484
+ if (allowOutsideWorkspace) return resolvedPath;
2485
+
2486
+ const relToRoot = path.relative(workspaceRoot, resolvedPath);
2487
+ if (relToRoot.startsWith('..') || path.isAbsolute(relToRoot)) {
2488
+ throw new Error(`[${toolName}] Security policy violation — target '${paramName}' resolves outside the active workspace directory (path traversal blocked).`);
2489
+ }
2490
+ let checkCursor = resolvedPath;
2491
+ while (!fs.existsSync(checkCursor)) {
2492
+ const parentCursor = path.dirname(checkCursor);
2493
+ if (parentCursor === checkCursor) break;
2494
+ checkCursor = parentCursor;
2495
+ }
2496
+ if (fs.existsSync(checkCursor)) {
2497
+ const realCheck = fs.realpathSync(checkCursor);
2498
+ const relReal = path.relative(workspaceRoot, realCheck);
2499
+ if (relReal.startsWith('..') || path.isAbsolute(relReal)) {
2500
+ throw new Error(`[${toolName}] Security policy violation — target path symlink escapes the active workspace directory.`);
2501
+ }
2502
+ }
2503
+ return resolvedPath;
2504
+ }
2470
2505
 
2471
2506
  const tools = {
2472
2507
  guardExec: {
@@ -2486,7 +2521,7 @@ export function createGuardedTools(engineOrOptions = {}, toolConfig = {}) {
2486
2521
  if (!command || typeof command !== 'string') {
2487
2522
  throw new Error('[guard_exec] Missing or invalid parameter: command must be a string.');
2488
2523
  }
2489
- const execCwd = cwd ? path.resolve(baseCwd, cwd) : baseCwd;
2524
+ const execCwd = cwd ? resolveSafeToolPath(cwd, 'guard_exec', 'cwd') : baseCwd;
2490
2525
  let stdout = '';
2491
2526
  let stderr = '';
2492
2527
  let exitCode = 0;
@@ -2550,7 +2585,7 @@ export function createGuardedTools(engineOrOptions = {}, toolConfig = {}) {
2550
2585
  if (!filePath || typeof filePath !== 'string') {
2551
2586
  throw new Error('[guard_read_file] Missing or invalid parameter: filePath must be a string.');
2552
2587
  }
2553
- const resolved = path.resolve(baseCwd, filePath);
2588
+ const resolved = resolveSafeToolPath(filePath, 'guard_read_file', 'filePath');
2554
2589
  if (!fs.existsSync(resolved)) {
2555
2590
  throw new Error(`[guard_read_file] File not found: ${resolved}`);
2556
2591
  }
@@ -2641,7 +2676,8 @@ export function createGuardedTools(engineOrOptions = {}, toolConfig = {}) {
2641
2676
  required: ['filePath', 'content']
2642
2677
  },
2643
2678
  execute: async ({ filePath, content, createBackup = true } = {}) => {
2644
- const patchResult = applyPatch(filePath, content, {
2679
+ const safeFilePath = resolveSafeToolPath(filePath, 'guard_apply_patch', 'filePath');
2680
+ const patchResult = applyPatch(safeFilePath, content, {
2645
2681
  tokenMap: engine.sessionMap,
2646
2682
  createBackup
2647
2683
  });
@@ -2672,7 +2708,7 @@ export function createGuardedTools(engineOrOptions = {}, toolConfig = {}) {
2672
2708
  required: []
2673
2709
  },
2674
2710
  execute: async ({ staged = false, cwd, profile = defaultProfile } = {}) => {
2675
- const repoCwd = cwd ? path.resolve(baseCwd, cwd) : baseCwd;
2711
+ const repoCwd = cwd ? resolveSafeToolPath(cwd, 'guard_git_diff', 'cwd') : baseCwd;
2676
2712
  const gitCmd = staged ? 'git diff --cached' : 'git diff';
2677
2713
  let diffOutput = '';
2678
2714
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@privacyscrubber/sdk",
3
- "version": "2.4.4",
3
+ "version": "2.4.5",
4
4
  "description": "In-process consumer privacy defense and pre-emptive PII interception SDK for Node.js, Next.js, and AI pipelines. Zero data loss, zero third parties, sub-millisecond in-memory execution.",
5
5
  "main": "index.cjs",
6
6
  "module": "index.js",