@privacyscrubber/mcp-server 2.4.4 → 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 +14 -7
- package/index.js +653 -151
- package/install.js +93 -2
- package/manifest.json +8 -10
- package/package.json +1 -3
- package/pii-masking-run.js +202 -39
- package/ps-license-manager.cjs +1 -1
- package/ps-pii-engine.cjs +1 -1
- package/scrubber-core.cjs +1 -1
- package/server.json +3 -3
- package/.well-known/mcp/server-card.json +0 -78
- package/LICENSE +0 -23
- package/ps-license-manager.js +0 -11
- package/ps-pii-engine.js +0 -11
package/index.js
CHANGED
|
@@ -82,6 +82,9 @@ if (rawCliArgs.includes('--memo') || rawCliArgs.includes('memo')) {
|
|
|
82
82
|
console.log(generateProcurementMemo({ targetTier: tierArg, format: 'text' }));
|
|
83
83
|
process.exit(0);
|
|
84
84
|
}
|
|
85
|
+
if (rawCliArgs.includes('--hook')) {
|
|
86
|
+
await import('./pii-masking-run.js');
|
|
87
|
+
}
|
|
85
88
|
if (rawCliArgs.some(arg => ['init', 'install', '--install', 'setup', '--setup', 'uninstall', '--uninstall', '--remove'].includes(arg))) {
|
|
86
89
|
const { runInstaller } = await import('./install.js');
|
|
87
90
|
const isRemove = rawCliArgs.some(arg => ['uninstall', '--uninstall', '--remove'].includes(arg));
|
|
@@ -1364,6 +1367,15 @@ We request approval to acquire a **PrivacyScrubber TEAMS License** (\$99/month f
|
|
|
1364
1367
|
`;
|
|
1365
1368
|
}
|
|
1366
1369
|
|
|
1370
|
+
const MCP_SYSTEM_INSTRUCTIONS = [
|
|
1371
|
+
"Zero-Trust Data Sanitization (ZTDS) Protocol Active (100% local RAM, 0 network egress).",
|
|
1372
|
+
"AUTOMATIC INTERCEPTION RULES FOR AI AGENTS:",
|
|
1373
|
+
"1. FILE READS: Before reading .env, configs, logs, database dumps, or documents (DOCX/PDF/XLSX/CSV/JSON), call 'guard_read_file' (or 'sanitize_file') instead of native file read tools.",
|
|
1374
|
+
"2. SHELL & GIT: Route terminal commands that may output secrets or PII through 'guard_exec', and inspect diffs via 'guard_git_diff'.",
|
|
1375
|
+
"3. RAW TEXT & RAG: Sanitize raw user PII/secrets in prompts via 'sanitize_text' (or 'guard_rag_chunk' for vector chunks) before external reasoning.",
|
|
1376
|
+
"4. LOCAL RESTORATION: Keep placeholders like [EMAIL_1], [API_KEY_1], [NAME_1] intact during reasoning. Call 'reveal_text' to restore final answers, 'guard_apply_patch' to write unmasked code/configs to disk, or 'guard_unmask_args' for local tool calls."
|
|
1377
|
+
].join("\n");
|
|
1378
|
+
|
|
1367
1379
|
// Create the MCP server
|
|
1368
1380
|
const server = new Server(
|
|
1369
1381
|
{
|
|
@@ -1377,6 +1389,7 @@ const server = new Server(
|
|
|
1377
1389
|
logging: {},
|
|
1378
1390
|
resources: {},
|
|
1379
1391
|
},
|
|
1392
|
+
instructions: MCP_SYSTEM_INSTRUCTIONS,
|
|
1380
1393
|
}
|
|
1381
1394
|
);
|
|
1382
1395
|
|
|
@@ -1387,6 +1400,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1387
1400
|
{
|
|
1388
1401
|
name: "audit_context",
|
|
1389
1402
|
description: "STEP 0 (Pre-Flight): Non-destructive security inspection of raw prompts or document chunks before sending to LLMs. Evaluates PII, secrets, risk severity (CLEAN, LOW, MODERATE, CRITICAL), and triggered regulatory frameworks (GDPR, HIPAA, SOC 2, PCI DSS) with zero text mutation.",
|
|
1403
|
+
annotations: {
|
|
1404
|
+
title: "Audit Context for PII & Secrets",
|
|
1405
|
+
readOnlyHint: true,
|
|
1406
|
+
destructiveHint: false,
|
|
1407
|
+
idempotentHint: true,
|
|
1408
|
+
openWorldHint: false
|
|
1409
|
+
},
|
|
1390
1410
|
inputSchema: {
|
|
1391
1411
|
type: "object",
|
|
1392
1412
|
properties: {
|
|
@@ -1414,6 +1434,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1414
1434
|
{
|
|
1415
1435
|
name: "sanitize_text",
|
|
1416
1436
|
description: "STEP 1: Call this first. You MUST NOT process raw user data before calling this. Locally scrubs PII, secrets, and credentials (like API keys, passwords, emails, phones, names) from code, logs, or text. Replaces them with safe placeholders (e.g., [EMAIL_1], [API_KEY_1]). Keep your data secure before passing it to any LLM. Supports General and all 30 specialized industry profiles in RAM session map. (For in-code backend services or RAG vector pipelines outside of MCP, use '@privacyscrubber/sdk': npm i @privacyscrubber/sdk)",
|
|
1437
|
+
annotations: {
|
|
1438
|
+
title: "Sanitize Text in Local RAM",
|
|
1439
|
+
readOnlyHint: true,
|
|
1440
|
+
destructiveHint: false,
|
|
1441
|
+
idempotentHint: true,
|
|
1442
|
+
openWorldHint: false
|
|
1443
|
+
},
|
|
1417
1444
|
inputSchema: {
|
|
1418
1445
|
type: "object",
|
|
1419
1446
|
properties: {
|
|
@@ -1446,44 +1473,16 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1446
1473
|
required: ["text"]
|
|
1447
1474
|
}
|
|
1448
1475
|
},
|
|
1449
|
-
{
|
|
1450
|
-
name: "scrub_text",
|
|
1451
|
-
description: "Alias for 'sanitize_text'. Locally scrubs PII, secrets, and credentials from prompts or text before LLM ingestion. Supports General and all 30 specialized industry profiles in RAM session map.",
|
|
1452
|
-
inputSchema: {
|
|
1453
|
-
type: "object",
|
|
1454
|
-
properties: {
|
|
1455
|
-
text: {
|
|
1456
|
-
type: "string",
|
|
1457
|
-
description: "The raw text, code, or logs to sanitize."
|
|
1458
|
-
},
|
|
1459
|
-
profile: {
|
|
1460
|
-
type: "string",
|
|
1461
|
-
description: PROFILE_PARAM_DESCRIPTION
|
|
1462
|
-
},
|
|
1463
|
-
sector: {
|
|
1464
|
-
type: "string",
|
|
1465
|
-
description: SECTOR_PARAM_DESCRIPTION
|
|
1466
|
-
},
|
|
1467
|
-
compact: {
|
|
1468
|
-
type: "boolean",
|
|
1469
|
-
description: "Optional. Compact 1-line audit summary."
|
|
1470
|
-
},
|
|
1471
|
-
ignore_list: {
|
|
1472
|
-
type: "array",
|
|
1473
|
-
items: { type: "string" },
|
|
1474
|
-
description: "Optional list of plaintext values to skip during detection."
|
|
1475
|
-
},
|
|
1476
|
-
session_id: {
|
|
1477
|
-
type: "string",
|
|
1478
|
-
description: "Optional session ID for multi-agent isolation. Defaults to 'default'."
|
|
1479
|
-
}
|
|
1480
|
-
},
|
|
1481
|
-
required: ["text"]
|
|
1482
|
-
}
|
|
1483
|
-
},
|
|
1484
1476
|
{
|
|
1485
1477
|
name: "reveal_text",
|
|
1486
1478
|
description: "STEP 3: Call this last. You MUST pass your final generated response through this tool to restore tokens (e.g., [EMAIL_1]) back with the original private data from the local volatile RAM-only session map before showing it to the user. Supports both standard batch mode and real-time streaming mode (stream: true).",
|
|
1479
|
+
annotations: {
|
|
1480
|
+
title: "Restore Tokens in Local RAM",
|
|
1481
|
+
readOnlyHint: true,
|
|
1482
|
+
destructiveHint: false,
|
|
1483
|
+
idempotentHint: true,
|
|
1484
|
+
openWorldHint: false
|
|
1485
|
+
},
|
|
1487
1486
|
inputSchema: {
|
|
1488
1487
|
type: "object",
|
|
1489
1488
|
properties: {
|
|
@@ -1510,6 +1509,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1510
1509
|
{
|
|
1511
1510
|
name: "stream_reveal_text",
|
|
1512
1511
|
description: "STEP 3 (Streaming): Real-time streaming token detokenizer for LLM Server-Sent Events (SSE) and stdio chunk pipelines. Buffers split token candidates across chunk boundaries (e.g. '[EM' + 'AIL_1]') so no characters or tokens are corrupted. Pass 'flush: true' on the final chunk.",
|
|
1512
|
+
annotations: {
|
|
1513
|
+
title: "Streaming Token Detokenizer",
|
|
1514
|
+
readOnlyHint: true,
|
|
1515
|
+
destructiveHint: false,
|
|
1516
|
+
idempotentHint: false,
|
|
1517
|
+
openWorldHint: false
|
|
1518
|
+
},
|
|
1513
1519
|
inputSchema: {
|
|
1514
1520
|
type: "object",
|
|
1515
1521
|
properties: {
|
|
@@ -1535,43 +1541,20 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1535
1541
|
},
|
|
1536
1542
|
{
|
|
1537
1543
|
name: "sanitize_file",
|
|
1538
|
-
description: "Reads a local file, sanitizes its contents using the selected profile, and outputs the safe version for AI analysis. Securely keeps original identifiers in memory.",
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
profile: {
|
|
1547
|
-
type: "string",
|
|
1548
|
-
description: PROFILE_PARAM_DESCRIPTION
|
|
1549
|
-
},
|
|
1550
|
-
sector: {
|
|
1551
|
-
type: "string",
|
|
1552
|
-
description: SECTOR_PARAM_DESCRIPTION
|
|
1553
|
-
},
|
|
1554
|
-
compact: {
|
|
1555
|
-
type: "boolean",
|
|
1556
|
-
description: "Optional. Compact 1-line audit summary."
|
|
1557
|
-
},
|
|
1558
|
-
session_id: {
|
|
1559
|
-
type: "string",
|
|
1560
|
-
description: "Optional session ID for multi-agent isolation. Defaults to 'default'."
|
|
1561
|
-
}
|
|
1562
|
-
},
|
|
1563
|
-
required: ["file_path"]
|
|
1564
|
-
}
|
|
1565
|
-
},
|
|
1566
|
-
{
|
|
1567
|
-
name: "scrub_file",
|
|
1568
|
-
description: "Alias for 'sanitize_file'. Reads and sanitizes a local file.",
|
|
1544
|
+
description: "Reads a local file (including plain text, source code, .eml, .rtf, .docx, .pdf, .xlsx), sanitizes its contents using the selected profile, and outputs the safe version for AI analysis. Securely keeps original identifiers in memory.",
|
|
1545
|
+
annotations: {
|
|
1546
|
+
title: "Sanitize Local File in RAM",
|
|
1547
|
+
readOnlyHint: true,
|
|
1548
|
+
destructiveHint: false,
|
|
1549
|
+
idempotentHint: true,
|
|
1550
|
+
openWorldHint: false
|
|
1551
|
+
},
|
|
1569
1552
|
inputSchema: {
|
|
1570
1553
|
type: "object",
|
|
1571
1554
|
properties: {
|
|
1572
1555
|
file_path: {
|
|
1573
1556
|
type: "string",
|
|
1574
|
-
description: "Absolute path to the file to sanitize."
|
|
1557
|
+
description: "Absolute or workspace-relative path to the file to sanitize."
|
|
1575
1558
|
},
|
|
1576
1559
|
profile: {
|
|
1577
1560
|
type: "string",
|
|
@@ -1596,6 +1579,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1596
1579
|
{
|
|
1597
1580
|
name: "audit_directory_for_pii",
|
|
1598
1581
|
description: "Scans a local directory for leaks of secrets, keys, and PII. Returns a summary report. Use this tool for Security Auditing.",
|
|
1582
|
+
annotations: {
|
|
1583
|
+
title: "Audit Directory for PII & Secrets",
|
|
1584
|
+
readOnlyHint: true,
|
|
1585
|
+
destructiveHint: false,
|
|
1586
|
+
idempotentHint: true,
|
|
1587
|
+
openWorldHint: false
|
|
1588
|
+
},
|
|
1599
1589
|
inputSchema: {
|
|
1600
1590
|
type: "object",
|
|
1601
1591
|
properties: {
|
|
@@ -1627,6 +1617,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1627
1617
|
{
|
|
1628
1618
|
name: "redact_file",
|
|
1629
1619
|
description: "Action/Redact: In-place redaction of a local file. Replaces PII and secrets with tokens and saves the file. By default, creates a .bak backup. Use dry_run=true to test without modifying.",
|
|
1620
|
+
annotations: {
|
|
1621
|
+
title: "In-Place File Redaction",
|
|
1622
|
+
readOnlyHint: false,
|
|
1623
|
+
destructiveHint: true,
|
|
1624
|
+
idempotentHint: false,
|
|
1625
|
+
openWorldHint: false
|
|
1626
|
+
},
|
|
1630
1627
|
inputSchema: {
|
|
1631
1628
|
type: "object",
|
|
1632
1629
|
properties: {
|
|
@@ -1657,6 +1654,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1657
1654
|
{
|
|
1658
1655
|
name: "create_default_config",
|
|
1659
1656
|
description: "Creates a default 'privacyscrubber.json' configuration file in the active workspace root directory if one does not exist. Includes template structures for custom regex rules and exclusion bypass patterns.",
|
|
1657
|
+
annotations: {
|
|
1658
|
+
title: "Create Default Workspace Config",
|
|
1659
|
+
readOnlyHint: false,
|
|
1660
|
+
destructiveHint: false,
|
|
1661
|
+
idempotentHint: true,
|
|
1662
|
+
openWorldHint: false
|
|
1663
|
+
},
|
|
1660
1664
|
inputSchema: {
|
|
1661
1665
|
type: "object",
|
|
1662
1666
|
properties: {},
|
|
@@ -1666,6 +1670,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1666
1670
|
{
|
|
1667
1671
|
name: "generate_compliance_report",
|
|
1668
1672
|
description: "Generates an official Zero-Trust Compliance Audit Certificate (GDPR, HIPAA, EU AI Act, SOC 2) for the current MCP session. Returns cryptographic session hash, masked entities breakdown, and compliance certification.",
|
|
1673
|
+
annotations: {
|
|
1674
|
+
title: "Generate Compliance Audit Certificate",
|
|
1675
|
+
readOnlyHint: true,
|
|
1676
|
+
destructiveHint: false,
|
|
1677
|
+
idempotentHint: true,
|
|
1678
|
+
openWorldHint: false
|
|
1679
|
+
},
|
|
1669
1680
|
inputSchema: {
|
|
1670
1681
|
type: "object",
|
|
1671
1682
|
properties: {
|
|
@@ -1693,6 +1704,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1693
1704
|
{
|
|
1694
1705
|
name: "mark_false_positive",
|
|
1695
1706
|
description: "Marks a previously detected token as a false positive. The original plaintext value will be excluded from all future sanitize_text calls in this session. Returns the restored original value and updated ignore list size.",
|
|
1707
|
+
annotations: {
|
|
1708
|
+
title: "Mark Token as False Positive",
|
|
1709
|
+
readOnlyHint: true,
|
|
1710
|
+
destructiveHint: false,
|
|
1711
|
+
idempotentHint: true,
|
|
1712
|
+
openWorldHint: false
|
|
1713
|
+
},
|
|
1696
1714
|
inputSchema: {
|
|
1697
1715
|
type: "object",
|
|
1698
1716
|
properties: {
|
|
@@ -1711,6 +1729,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1711
1729
|
{
|
|
1712
1730
|
name: "check_status",
|
|
1713
1731
|
description: "Returns the current PrivacyScrubber MCP tier, session usage, available profiles, and PRO upgrade instructions. Call this to see your license status or get setup help.",
|
|
1732
|
+
annotations: {
|
|
1733
|
+
title: "Check MCP License & Session Status",
|
|
1734
|
+
readOnlyHint: true,
|
|
1735
|
+
destructiveHint: false,
|
|
1736
|
+
idempotentHint: true,
|
|
1737
|
+
openWorldHint: false
|
|
1738
|
+
},
|
|
1714
1739
|
inputSchema: {
|
|
1715
1740
|
type: "object",
|
|
1716
1741
|
properties: {},
|
|
@@ -1720,6 +1745,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1720
1745
|
{
|
|
1721
1746
|
name: "guard_exec",
|
|
1722
1747
|
description: "Zero-Trust Agentic Guard: Safely executes a shell/terminal command locally in isolated process, sanitizes stdout and stderr in volatile RAM, tokenizes any API keys, credentials, database URIs, or PII before sending output to LLM. Maps original values in memory for safe local restoration.",
|
|
1748
|
+
annotations: {
|
|
1749
|
+
title: "Guard Shell Command Execution",
|
|
1750
|
+
readOnlyHint: false,
|
|
1751
|
+
destructiveHint: true,
|
|
1752
|
+
idempotentHint: false,
|
|
1753
|
+
openWorldHint: false
|
|
1754
|
+
},
|
|
1723
1755
|
inputSchema: {
|
|
1724
1756
|
type: "object",
|
|
1725
1757
|
properties: {
|
|
@@ -1742,6 +1774,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1742
1774
|
timeout_ms: {
|
|
1743
1775
|
type: "number",
|
|
1744
1776
|
description: "Execution timeout in milliseconds. Defaults to 15000 (15s)."
|
|
1777
|
+
},
|
|
1778
|
+
session_id: {
|
|
1779
|
+
type: "string",
|
|
1780
|
+
description: "Optional session ID for multi-agent isolation. Defaults to 'default'."
|
|
1745
1781
|
}
|
|
1746
1782
|
},
|
|
1747
1783
|
required: ["command"]
|
|
@@ -1749,7 +1785,14 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1749
1785
|
},
|
|
1750
1786
|
{
|
|
1751
1787
|
name: "guard_read_file",
|
|
1752
|
-
description: "Zero-Trust Agentic Guard: Reads
|
|
1788
|
+
description: "Zero-Trust Agentic Guard: Preferred over native file read tools. Reads any sensitive local file (.env, config, credentials, source code, database dumps, .docx, .pdf, .xlsx, .eml, .rtf), sanitizes all credentials and PII in volatile RAM, and returns safe tokenized content for AI agent context.",
|
|
1789
|
+
annotations: {
|
|
1790
|
+
title: "Guard Local File Read",
|
|
1791
|
+
readOnlyHint: true,
|
|
1792
|
+
destructiveHint: false,
|
|
1793
|
+
idempotentHint: true,
|
|
1794
|
+
openWorldHint: false
|
|
1795
|
+
},
|
|
1753
1796
|
inputSchema: {
|
|
1754
1797
|
type: "object",
|
|
1755
1798
|
properties: {
|
|
@@ -1768,6 +1811,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1768
1811
|
max_lines: {
|
|
1769
1812
|
type: "number",
|
|
1770
1813
|
description: "Optional line cap for large files. Defaults to 500."
|
|
1814
|
+
},
|
|
1815
|
+
session_id: {
|
|
1816
|
+
type: "string",
|
|
1817
|
+
description: "Optional session ID for multi-agent isolation. Defaults to 'default'."
|
|
1771
1818
|
}
|
|
1772
1819
|
},
|
|
1773
1820
|
required: ["file_path"]
|
|
@@ -1776,6 +1823,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1776
1823
|
{
|
|
1777
1824
|
name: "guard_git_diff",
|
|
1778
1825
|
description: "Zero-Trust Agentic Guard: Inspects unstaged or staged git diffs locally, sanitizes any newly added or modified credentials, tokens, or PII in RAM, and returns safe diff for AI commit generation or code review.",
|
|
1826
|
+
annotations: {
|
|
1827
|
+
title: "Guard Git Diff Inspection",
|
|
1828
|
+
readOnlyHint: true,
|
|
1829
|
+
destructiveHint: false,
|
|
1830
|
+
idempotentHint: true,
|
|
1831
|
+
openWorldHint: false
|
|
1832
|
+
},
|
|
1779
1833
|
inputSchema: {
|
|
1780
1834
|
type: "object",
|
|
1781
1835
|
properties: {
|
|
@@ -1806,6 +1860,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1806
1860
|
{
|
|
1807
1861
|
name: "guard_apply_patch",
|
|
1808
1862
|
description: "Zero-Trust Agentic Guard: Reverses token placeholders ([API_KEY_1], [SECRET_1]) in AI-generated code or text, restores authentic values from local volatile RAM, and writes real content directly to target file on disk. Remote LLM never learns actual secrets.",
|
|
1863
|
+
annotations: {
|
|
1864
|
+
title: "Guard Apply Unmasked Patch to Disk",
|
|
1865
|
+
readOnlyHint: false,
|
|
1866
|
+
destructiveHint: false,
|
|
1867
|
+
idempotentHint: true,
|
|
1868
|
+
openWorldHint: false
|
|
1869
|
+
},
|
|
1809
1870
|
inputSchema: {
|
|
1810
1871
|
type: "object",
|
|
1811
1872
|
properties: {
|
|
@@ -1820,6 +1881,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1820
1881
|
create_backup: {
|
|
1821
1882
|
type: "boolean",
|
|
1822
1883
|
description: "Whether to create a .bak backup if target file already exists. Defaults to true."
|
|
1884
|
+
},
|
|
1885
|
+
session_id: {
|
|
1886
|
+
type: "string",
|
|
1887
|
+
description: "Optional session ID for multi-agent isolation. Defaults to 'default'."
|
|
1823
1888
|
}
|
|
1824
1889
|
},
|
|
1825
1890
|
required: ["file_path", "content"]
|
|
@@ -1827,7 +1892,14 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1827
1892
|
},
|
|
1828
1893
|
{
|
|
1829
1894
|
name: "create_agent_rules",
|
|
1830
|
-
description: "Zero-Trust Agentic Guard: Scaffolds CISO-grade AI Agent Rules in workspace (.cursorrules, .windsurfrules, CLAUDE.md, .github/copilot-instructions.md, .clinerules). Guides autonomous agents to route all file reads, git diffs, and shell commands through PrivacyScrubber Guard tools.",
|
|
1895
|
+
description: "Zero-Trust Agentic Guard: Scaffolds CISO-grade AI Agent Rules in workspace (.cursor/rules/privacyscrubber.mdc, .cursorrules, .windsurfrules, CLAUDE.md, AGENTS.md, .github/copilot-instructions.md, .clinerules). Guides autonomous agents to route all file reads, git diffs, and shell commands through PrivacyScrubber Guard tools.",
|
|
1896
|
+
annotations: {
|
|
1897
|
+
title: "Scaffold AI Agent Security Rules",
|
|
1898
|
+
readOnlyHint: false,
|
|
1899
|
+
destructiveHint: false,
|
|
1900
|
+
idempotentHint: true,
|
|
1901
|
+
openWorldHint: false
|
|
1902
|
+
},
|
|
1831
1903
|
inputSchema: {
|
|
1832
1904
|
type: "object",
|
|
1833
1905
|
properties: {
|
|
@@ -1835,7 +1907,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1835
1907
|
type: "array",
|
|
1836
1908
|
items: {
|
|
1837
1909
|
type: "string",
|
|
1838
|
-
enum: ["all", "cursor", "windsurf", "claude_code", "copilot", "cline"]
|
|
1910
|
+
enum: ["all", "cursor", "windsurf", "claude_code", "agents_md", "codex", "copilot", "cline"]
|
|
1839
1911
|
},
|
|
1840
1912
|
description: "List of agent targets to generate rules for. Use ['all'] to generate rules for all supported AI agents."
|
|
1841
1913
|
},
|
|
@@ -1850,6 +1922,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1850
1922
|
{
|
|
1851
1923
|
name: "guard_unmask_args",
|
|
1852
1924
|
description: "Zero-Trust Agentic Guard: Detokenizes parameters, JSON objects, or arguments in local RAM before dispatching to local tools or APIs without sending cleartext secrets back to the LLM.",
|
|
1925
|
+
annotations: {
|
|
1926
|
+
title: "Unmask Tool Arguments in Local RAM",
|
|
1927
|
+
readOnlyHint: true,
|
|
1928
|
+
destructiveHint: false,
|
|
1929
|
+
idempotentHint: true,
|
|
1930
|
+
openWorldHint: false
|
|
1931
|
+
},
|
|
1853
1932
|
inputSchema: {
|
|
1854
1933
|
type: "object",
|
|
1855
1934
|
properties: {
|
|
@@ -1867,6 +1946,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1867
1946
|
{
|
|
1868
1947
|
name: "guard_session_info",
|
|
1869
1948
|
description: "Zero-Trust Agentic Guard: Returns diagnostic metrics on active in-memory sessions, token counts, and memory footprint with zero plaintext leakage.",
|
|
1949
|
+
annotations: {
|
|
1950
|
+
title: "Inspect RAM Session Diagnostics",
|
|
1951
|
+
readOnlyHint: true,
|
|
1952
|
+
destructiveHint: false,
|
|
1953
|
+
idempotentHint: true,
|
|
1954
|
+
openWorldHint: false
|
|
1955
|
+
},
|
|
1870
1956
|
inputSchema: {
|
|
1871
1957
|
type: "object",
|
|
1872
1958
|
properties: {
|
|
@@ -1881,6 +1967,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1881
1967
|
{
|
|
1882
1968
|
name: "guard_session_reset",
|
|
1883
1969
|
description: "Zero-Trust Agentic Guard: Explicitly purges the in-memory token map for a specific session_id or all sessions upon agent task completion.",
|
|
1970
|
+
annotations: {
|
|
1971
|
+
title: "Reset RAM Session Token Map",
|
|
1972
|
+
readOnlyHint: true,
|
|
1973
|
+
destructiveHint: false,
|
|
1974
|
+
idempotentHint: true,
|
|
1975
|
+
openWorldHint: false
|
|
1976
|
+
},
|
|
1884
1977
|
inputSchema: {
|
|
1885
1978
|
type: "object",
|
|
1886
1979
|
properties: {
|
|
@@ -1899,6 +1992,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1899
1992
|
{
|
|
1900
1993
|
name: "guard_rag_chunk",
|
|
1901
1994
|
description: "Zero-Trust Agentic Guard: Sanitizes an array of document chunks or knowledge base records before sending to embedding models or vector databases (Chroma, Pinecone, Qdrant). Keeps authentic data in local RAM with zero network leakage.",
|
|
1995
|
+
annotations: {
|
|
1996
|
+
title: "Sanitize RAG Document Chunks",
|
|
1997
|
+
readOnlyHint: true,
|
|
1998
|
+
destructiveHint: false,
|
|
1999
|
+
idempotentHint: true,
|
|
2000
|
+
openWorldHint: false
|
|
2001
|
+
},
|
|
1902
2002
|
inputSchema: {
|
|
1903
2003
|
type: "object",
|
|
1904
2004
|
properties: {
|
|
@@ -1939,6 +2039,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1939
2039
|
{
|
|
1940
2040
|
name: "guard_rag_restore",
|
|
1941
2041
|
description: "Zero-Trust Agentic Guard: Re-hydrates retrieved vector search results or chunks by restoring token placeholders with authentic data from the volatile RAM session before presenting to the user.",
|
|
2042
|
+
annotations: {
|
|
2043
|
+
title: "Restore RAG Vector Search Results",
|
|
2044
|
+
readOnlyHint: true,
|
|
2045
|
+
destructiveHint: false,
|
|
2046
|
+
idempotentHint: true,
|
|
2047
|
+
openWorldHint: false
|
|
2048
|
+
},
|
|
1942
2049
|
inputSchema: {
|
|
1943
2050
|
type: "object",
|
|
1944
2051
|
properties: {
|
|
@@ -1972,6 +2079,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1972
2079
|
{
|
|
1973
2080
|
name: "export_audit_log",
|
|
1974
2081
|
description: "Zero-Trust Enterprise Compliance: Exports an immutable, tamper-evident audit log in SIEM standard formats (CEF / ArcSight, RFC 5424 Syslog, JSONL, or JSON) with zero plaintext PII leakage. Can write directly to a local log file or stream.",
|
|
2082
|
+
annotations: {
|
|
2083
|
+
title: "Export SIEM Compliance Audit Log",
|
|
2084
|
+
readOnlyHint: false,
|
|
2085
|
+
destructiveHint: false,
|
|
2086
|
+
idempotentHint: true,
|
|
2087
|
+
openWorldHint: false
|
|
2088
|
+
},
|
|
1975
2089
|
inputSchema: {
|
|
1976
2090
|
type: "object",
|
|
1977
2091
|
properties: {
|
|
@@ -2006,6 +2120,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2006
2120
|
{
|
|
2007
2121
|
name: "generate_procurement_memo",
|
|
2008
2122
|
description: "Zero-Trust Enterprise Procurement: Generates an executive-ready Procurement & ROI Memorandum for Chief Information Security Officers (CISO), Data Protection Officers (DPO), and VP of Engineering. Compares TCO of traditional Cloud DLP ($1.5k–$4k/mo) vs PrivacyScrubber TEAMS ($99/mo flat) or Developer SDK ($299/mo flat), citing statutory frameworks (GDPR Art. 4/28, HIPAA §164.514, SOC 2 Type II, PCI DSS v4.0, NIST SP 800-53, EU AI Act Art. 50).",
|
|
2123
|
+
annotations: {
|
|
2124
|
+
title: "Generate CISO Procurement & ROI Memo",
|
|
2125
|
+
readOnlyHint: true,
|
|
2126
|
+
destructiveHint: false,
|
|
2127
|
+
idempotentHint: true,
|
|
2128
|
+
openWorldHint: false
|
|
2129
|
+
},
|
|
2009
2130
|
inputSchema: {
|
|
2010
2131
|
type: "object",
|
|
2011
2132
|
properties: {
|
|
@@ -2178,6 +2299,75 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
|
2178
2299
|
throw new Error(`Resource not found: ${uri}`);
|
|
2179
2300
|
});
|
|
2180
2301
|
|
|
2302
|
+
function getEffectiveWorkspaceRoots() {
|
|
2303
|
+
const roots = [];
|
|
2304
|
+
const addCandidate = (dir) => {
|
|
2305
|
+
if (!dir || typeof dir !== 'string') return;
|
|
2306
|
+
const trimmed = dir.trim();
|
|
2307
|
+
if (!trimmed) return;
|
|
2308
|
+
try {
|
|
2309
|
+
const resolved = fs.existsSync(trimmed) ? fs.realpathSync(trimmed) : path.resolve(trimmed);
|
|
2310
|
+
if (!roots.includes(resolved)) roots.push(resolved);
|
|
2311
|
+
} catch (_) {
|
|
2312
|
+
const fallback = path.resolve(trimmed);
|
|
2313
|
+
if (!roots.includes(fallback)) roots.push(fallback);
|
|
2314
|
+
}
|
|
2315
|
+
};
|
|
2316
|
+
|
|
2317
|
+
addCandidate(process.cwd());
|
|
2318
|
+
addCandidate(process.env.PRIVACYSCRUBBER_WORKSPACE);
|
|
2319
|
+
addCandidate(process.env.CURSOR_WORKSPACE_PATH);
|
|
2320
|
+
addCandidate(process.env.VSCODE_CWD);
|
|
2321
|
+
addCandidate(process.env.PROJECT_ROOT);
|
|
2322
|
+
addCandidate(process.env.INIT_CWD);
|
|
2323
|
+
if (process.env.WORKSPACE_FOLDER_PATHS) {
|
|
2324
|
+
for (const part of process.env.WORKSPACE_FOLDER_PATHS.split(path.delimiter)) {
|
|
2325
|
+
addCandidate(part);
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
return roots;
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
function isWithinAnyWorkspaceRoot(targetPath, roots) {
|
|
2332
|
+
for (const root of roots) {
|
|
2333
|
+
const rel = path.relative(root, targetPath);
|
|
2334
|
+
if (!rel.startsWith("..") && !path.isAbsolute(rel)) {
|
|
2335
|
+
return { ok: true, matchedRoot: root };
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
return { ok: false, matchedRoot: roots[0] || process.cwd() };
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
function resolveWorkspaceSafePath(inputPath, paramName = "file_path") {
|
|
2342
|
+
const roots = getEffectiveWorkspaceRoots();
|
|
2343
|
+
const primaryRoot = roots[0];
|
|
2344
|
+
const resolvedPath = path.resolve(primaryRoot, inputPath);
|
|
2345
|
+
const boundaryCheck = isWithinAnyWorkspaceRoot(resolvedPath, roots);
|
|
2346
|
+
if (!boundaryCheck.ok) {
|
|
2347
|
+
return {
|
|
2348
|
+
ok: false,
|
|
2349
|
+
error: `Error: Security policy violation — target '${paramName}' resolves outside the active workspace directory (path traversal blocked).`
|
|
2350
|
+
};
|
|
2351
|
+
}
|
|
2352
|
+
let checkCursor = resolvedPath;
|
|
2353
|
+
while (!fs.existsSync(checkCursor)) {
|
|
2354
|
+
const parentCursor = path.dirname(checkCursor);
|
|
2355
|
+
if (parentCursor === checkCursor) break;
|
|
2356
|
+
checkCursor = parentCursor;
|
|
2357
|
+
}
|
|
2358
|
+
if (fs.existsSync(checkCursor)) {
|
|
2359
|
+
const realCheck = fs.realpathSync(checkCursor);
|
|
2360
|
+
const realBoundary = isWithinAnyWorkspaceRoot(realCheck, roots);
|
|
2361
|
+
if (!realBoundary.ok) {
|
|
2362
|
+
return {
|
|
2363
|
+
ok: false,
|
|
2364
|
+
error: `Error: Security policy violation — target path symlink escapes the active workspace directory.`
|
|
2365
|
+
};
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
return { ok: true, resolvedPath, workspaceRoot: boundaryCheck.matchedRoot };
|
|
2369
|
+
}
|
|
2370
|
+
|
|
2181
2371
|
// Handle tool execution calls
|
|
2182
2372
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
2183
2373
|
const { name, arguments: args } = request.params;
|
|
@@ -2210,18 +2400,21 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2210
2400
|
const result = PrivacyScrubberCore.scrubText(text, customRules, {}, normalizedProfile, dryRunMap, license.isPro);
|
|
2211
2401
|
const telemetry = buildCisoAuditTelemetry(result.tokenMap || {});
|
|
2212
2402
|
|
|
2403
|
+
const structuredAudit = {
|
|
2404
|
+
riskLevel: telemetry.riskLevel,
|
|
2405
|
+
totalDetected: telemetry.totalDetected,
|
|
2406
|
+
entities: telemetry.entities,
|
|
2407
|
+
triggeredFrameworks: telemetry.triggeredFrameworks,
|
|
2408
|
+
profile: targetProfile,
|
|
2409
|
+
executionEngine: "ZTDS Client-Side RAM"
|
|
2410
|
+
};
|
|
2411
|
+
|
|
2213
2412
|
if (format === "json") {
|
|
2214
2413
|
return {
|
|
2414
|
+
structuredContent: structuredAudit,
|
|
2215
2415
|
content: [{
|
|
2216
2416
|
type: "text",
|
|
2217
|
-
text: JSON.stringify(
|
|
2218
|
-
riskLevel: telemetry.riskLevel,
|
|
2219
|
-
totalDetected: telemetry.totalDetected,
|
|
2220
|
-
entities: telemetry.entities,
|
|
2221
|
-
triggeredFrameworks: telemetry.triggeredFrameworks,
|
|
2222
|
-
profile: targetProfile,
|
|
2223
|
-
executionEngine: "ZTDS Client-Side RAM"
|
|
2224
|
-
}, null, 2)
|
|
2417
|
+
text: JSON.stringify(structuredAudit, null, 2)
|
|
2225
2418
|
}]
|
|
2226
2419
|
};
|
|
2227
2420
|
}
|
|
@@ -2246,6 +2439,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2246
2439
|
}
|
|
2247
2440
|
|
|
2248
2441
|
return {
|
|
2442
|
+
structuredContent: structuredAudit,
|
|
2249
2443
|
content: [{ type: "text", text: report }]
|
|
2250
2444
|
};
|
|
2251
2445
|
}
|
|
@@ -2306,6 +2500,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2306
2500
|
const receiptMd = formatAuditReceipt(telemetry, compact, wasTruncated, charLimit);
|
|
2307
2501
|
|
|
2308
2502
|
return {
|
|
2503
|
+
structuredContent: {
|
|
2504
|
+
scrubbedText,
|
|
2505
|
+
tokensRedacted: telemetry.totalCount,
|
|
2506
|
+
riskLevel: telemetry.riskLevel,
|
|
2507
|
+
entities: telemetry.entities,
|
|
2508
|
+
frameworks: telemetry.frameworksList,
|
|
2509
|
+
profile: finalProfile,
|
|
2510
|
+
sessionId: session_id,
|
|
2511
|
+
wasTruncated: Boolean(wasTruncated)
|
|
2512
|
+
},
|
|
2309
2513
|
content: [
|
|
2310
2514
|
{ type: "text", text: scrubbedText + receiptMd }
|
|
2311
2515
|
]
|
|
@@ -2375,6 +2579,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2375
2579
|
restoredText = restoredText.replace(/\b(mysql|postgresql|postgres|redis|mongodb|https?|ftp|ssh|git|aws):\/\/\1:\/\//gi, '$1://');
|
|
2376
2580
|
|
|
2377
2581
|
return {
|
|
2582
|
+
structuredContent: {
|
|
2583
|
+
restoredText,
|
|
2584
|
+
restoredCount: restored.restoredCount || 0,
|
|
2585
|
+
sessionId: session_id
|
|
2586
|
+
},
|
|
2378
2587
|
content: [
|
|
2379
2588
|
{
|
|
2380
2589
|
type: "text",
|
|
@@ -2457,7 +2666,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2457
2666
|
}
|
|
2458
2667
|
|
|
2459
2668
|
if (name === "sanitize_file" || name === "scrub_file") {
|
|
2460
|
-
const rawPath = args
|
|
2669
|
+
const rawPath = args?.file_path || args?.filePath;
|
|
2461
2670
|
if (!rawPath) {
|
|
2462
2671
|
return {
|
|
2463
2672
|
isError: true,
|
|
@@ -2474,8 +2683,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2474
2683
|
const targetSession = getSessionMap(session_id);
|
|
2475
2684
|
const targetIgnoreList = getSessionIgnoreList(session_id);
|
|
2476
2685
|
const filePath = rawPath;
|
|
2477
|
-
|
|
2478
|
-
|
|
2686
|
+
const safeTarget = resolveWorkspaceSafePath(filePath, "file_path");
|
|
2687
|
+
if (!safeTarget.ok) {
|
|
2688
|
+
return {
|
|
2689
|
+
isError: true,
|
|
2690
|
+
content: [{ type: "text", text: safeTarget.error }]
|
|
2691
|
+
};
|
|
2692
|
+
}
|
|
2693
|
+
const resolvedPath = safeTarget.resolvedPath;
|
|
2479
2694
|
if (!fs.existsSync(resolvedPath)) {
|
|
2480
2695
|
return {
|
|
2481
2696
|
isError: true,
|
|
@@ -2549,9 +2764,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2549
2764
|
const dataStart = localOffset + 30 + lNameLen + lExtraLen;
|
|
2550
2765
|
const compData = docxBuf.subarray(dataStart, dataStart + compSize);
|
|
2551
2766
|
if (method === 0) {
|
|
2552
|
-
|
|
2767
|
+
if (compData.length <= 20 * 1024 * 1024) {
|
|
2768
|
+
zipEntries[entryName] = compData.toString('utf8');
|
|
2769
|
+
}
|
|
2553
2770
|
} else if (method === 8) {
|
|
2554
|
-
zipEntries[entryName] = zlib.inflateRawSync(compData).toString('utf8');
|
|
2771
|
+
zipEntries[entryName] = zlib.inflateRawSync(compData, { maxOutputLength: 20 * 1024 * 1024 }).toString('utf8');
|
|
2555
2772
|
}
|
|
2556
2773
|
}
|
|
2557
2774
|
pos += 46 + nameLen + extraLen + commentLen;
|
|
@@ -2875,6 +3092,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2875
3092
|
const receiptMd = formatAuditReceipt(telemetry, compact, wasTruncated, charLimit);
|
|
2876
3093
|
|
|
2877
3094
|
return {
|
|
3095
|
+
structuredContent: {
|
|
3096
|
+
scrubbedText,
|
|
3097
|
+
tokensRedacted: telemetry.totalCount,
|
|
3098
|
+
riskLevel: telemetry.riskLevel,
|
|
3099
|
+
entities: telemetry.entities,
|
|
3100
|
+
frameworks: telemetry.frameworksList,
|
|
3101
|
+
profile: finalProfile,
|
|
3102
|
+
sessionId: session_id,
|
|
3103
|
+
wasTruncated: Boolean(wasTruncated)
|
|
3104
|
+
},
|
|
2878
3105
|
content: [
|
|
2879
3106
|
{ type: "text", text: scrubbedText + receiptMd }
|
|
2880
3107
|
]
|
|
@@ -2882,23 +3109,35 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2882
3109
|
}
|
|
2883
3110
|
|
|
2884
3111
|
if (name === "redact_file") {
|
|
2885
|
-
const { file_path, dry_run, no_backup } = args;
|
|
2886
|
-
if (!file_path
|
|
3112
|
+
const { file_path, dry_run, no_backup } = args || {};
|
|
3113
|
+
if (!file_path) {
|
|
3114
|
+
return { isError: true, content: [{ type: "text", text: "Error: Missing required parameter 'file_path'." }] };
|
|
3115
|
+
}
|
|
3116
|
+
const safeTarget = resolveWorkspaceSafePath(file_path, "file_path");
|
|
3117
|
+
if (!safeTarget.ok) {
|
|
3118
|
+
return { isError: true, content: [{ type: "text", text: safeTarget.error }] };
|
|
3119
|
+
}
|
|
3120
|
+
const resolvedRedactPath = safeTarget.resolvedPath;
|
|
3121
|
+
if (!fs.existsSync(resolvedRedactPath)) {
|
|
2887
3122
|
return { isError: true, content: [{ type: "text", text: `Error: File not found at path: ${file_path}` }] };
|
|
2888
3123
|
}
|
|
2889
3124
|
|
|
2890
3125
|
try {
|
|
2891
|
-
const
|
|
3126
|
+
const lstat = await fs.promises.lstat(resolvedRedactPath);
|
|
3127
|
+
if (lstat.isSymbolicLink() || !lstat.isFile()) {
|
|
3128
|
+
return { isError: true, content: [{ type: "text", text: `Error: Target must be a regular non-symlink file.` }] };
|
|
3129
|
+
}
|
|
3130
|
+
const stat = lstat;
|
|
2892
3131
|
if (stat.size > 50 * 1024 * 1024) {
|
|
2893
3132
|
return { isError: true, content: [{ type: "text", text: `Error: File is too large (>50MB).` }] };
|
|
2894
3133
|
}
|
|
2895
3134
|
|
|
2896
|
-
const buffer = fs.readFileSync(
|
|
3135
|
+
const buffer = fs.readFileSync(resolvedRedactPath);
|
|
2897
3136
|
if (buffer.indexOf(0) !== -1) {
|
|
2898
3137
|
return { isError: true, content: [{ type: "text", text: `Error: Cannot redact binary file.` }] };
|
|
2899
3138
|
}
|
|
2900
3139
|
let content = buffer.toString('utf8');
|
|
2901
|
-
const ext = path.extname(
|
|
3140
|
+
const ext = path.extname(resolvedRedactPath).toLowerCase();
|
|
2902
3141
|
if (ext === ".eml") {
|
|
2903
3142
|
const decodeQpUtf8 = (str) =>
|
|
2904
3143
|
str.replace(/=\r?\n/g, "").replace(/(?:=[0-9A-Fa-f]{2})+/g, (seq) => {
|
|
@@ -2992,15 +3231,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2992
3231
|
}
|
|
2993
3232
|
|
|
2994
3233
|
if (!no_backup) {
|
|
2995
|
-
fs.writeFileSync(`${
|
|
3234
|
+
fs.writeFileSync(`${resolvedRedactPath}.bak`, content, 'utf8');
|
|
2996
3235
|
}
|
|
2997
3236
|
|
|
2998
|
-
fs.writeFileSync(
|
|
3237
|
+
fs.writeFileSync(resolvedRedactPath, result.scrubbedText, 'utf8');
|
|
2999
3238
|
|
|
3000
3239
|
return {
|
|
3001
3240
|
content: [{
|
|
3002
3241
|
type: "text",
|
|
3003
|
-
text: `✅ File successfully redacted.\nReplaced ${Object.keys(localSessionMap).length} tokens (${summary}).\n${!no_backup ? `Backup saved to ${
|
|
3242
|
+
text: `✅ File successfully redacted.\nReplaced ${Object.keys(localSessionMap).length} tokens (${summary}).\n${!no_backup ? `Backup saved to ${resolvedRedactPath}.bak` : "No backup created (no_backup=true)."}`
|
|
3004
3243
|
}]
|
|
3005
3244
|
};
|
|
3006
3245
|
|
|
@@ -3168,8 +3407,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3168
3407
|
}
|
|
3169
3408
|
|
|
3170
3409
|
if (name === "audit_directory_for_pii") {
|
|
3171
|
-
const { directory_path, extensions, ignore_node_modules } = args;
|
|
3172
|
-
if (!directory_path
|
|
3410
|
+
const { directory_path, extensions, ignore_node_modules } = args || {};
|
|
3411
|
+
if (!directory_path) {
|
|
3412
|
+
return { isError: true, content: [{ type: "text", text: "Error: Missing required parameter 'directory_path'." }] };
|
|
3413
|
+
}
|
|
3414
|
+
const safeDir = resolveWorkspaceSafePath(directory_path, "directory_path");
|
|
3415
|
+
if (!safeDir.ok) {
|
|
3416
|
+
return { isError: true, content: [{ type: "text", text: safeDir.error }] };
|
|
3417
|
+
}
|
|
3418
|
+
if (!fs.existsSync(safeDir.resolvedPath)) {
|
|
3173
3419
|
return { isError: true, content: [{ type: "text", text: `Error: Directory not found at path: ${directory_path}` }] };
|
|
3174
3420
|
}
|
|
3175
3421
|
|
|
@@ -3179,20 +3425,23 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3179
3425
|
async function walkDir(dir) {
|
|
3180
3426
|
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
3181
3427
|
for (const entry of entries) {
|
|
3428
|
+
if (entry.isSymbolicLink && entry.isSymbolicLink()) continue;
|
|
3182
3429
|
const fullPath = path.join(dir, entry.name);
|
|
3430
|
+
const safeChild = resolveWorkspaceSafePath(fullPath, "directory_path");
|
|
3431
|
+
if (!safeChild.ok) continue;
|
|
3183
3432
|
if (entry.isDirectory()) {
|
|
3184
|
-
if (!ignoreDirs.includes(entry.name)) await walkDir(
|
|
3433
|
+
if (!ignoreDirs.includes(entry.name)) await walkDir(safeChild.resolvedPath);
|
|
3185
3434
|
} else if (entry.isFile()) {
|
|
3186
3435
|
if (extensions && extensions.length > 0) {
|
|
3187
3436
|
const ext = path.extname(entry.name);
|
|
3188
3437
|
if (!extensions.includes(ext)) continue;
|
|
3189
3438
|
}
|
|
3190
|
-
filesToScan.push(
|
|
3439
|
+
filesToScan.push(safeChild.resolvedPath);
|
|
3191
3440
|
}
|
|
3192
3441
|
}
|
|
3193
3442
|
}
|
|
3194
3443
|
|
|
3195
|
-
await walkDir(
|
|
3444
|
+
await walkDir(safeDir.resolvedPath);
|
|
3196
3445
|
|
|
3197
3446
|
let reportLines = [`## Security Audit Report: ${directory_path}`];
|
|
3198
3447
|
let filesWithIssues = 0;
|
|
@@ -3302,7 +3551,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3302
3551
|
// Transparent in-memory unmasking of any token placeholders before local execution
|
|
3303
3552
|
const rawExecutableCommand = reverseTokens(command, targetSession);
|
|
3304
3553
|
|
|
3305
|
-
|
|
3554
|
+
let execCwd = process.cwd();
|
|
3555
|
+
if (cwd) {
|
|
3556
|
+
const safeCwd = resolveWorkspaceSafePath(cwd, "cwd");
|
|
3557
|
+
if (!safeCwd.ok) {
|
|
3558
|
+
return { isError: true, content: [{ type: "text", text: safeCwd.error }] };
|
|
3559
|
+
}
|
|
3560
|
+
execCwd = safeCwd.resolvedPath;
|
|
3561
|
+
}
|
|
3306
3562
|
let stdout = '';
|
|
3307
3563
|
let stderr = '';
|
|
3308
3564
|
let exitCode = 0;
|
|
@@ -3358,7 +3614,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3358
3614
|
content: [{ type: "text", text: "Error: Missing required parameter 'file_path'." }]
|
|
3359
3615
|
};
|
|
3360
3616
|
}
|
|
3361
|
-
const
|
|
3617
|
+
const safeTarget = resolveWorkspaceSafePath(file_path, "file_path");
|
|
3618
|
+
if (!safeTarget.ok) {
|
|
3619
|
+
return {
|
|
3620
|
+
isError: true,
|
|
3621
|
+
content: [{ type: "text", text: safeTarget.error }]
|
|
3622
|
+
};
|
|
3623
|
+
}
|
|
3624
|
+
const resolvedPath = safeTarget.resolvedPath;
|
|
3362
3625
|
if (!fs.existsSync(resolvedPath)) {
|
|
3363
3626
|
return {
|
|
3364
3627
|
isError: true,
|
|
@@ -3379,23 +3642,72 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3379
3642
|
};
|
|
3380
3643
|
}
|
|
3381
3644
|
const rawBuffer = fs.readFileSync(resolvedPath);
|
|
3382
|
-
const
|
|
3383
|
-
for (let i = 0; i < checkLen; i++) {
|
|
3384
|
-
if (rawBuffer[i] === 0) {
|
|
3385
|
-
return {
|
|
3386
|
-
isError: true,
|
|
3387
|
-
content: [{ type: "text", text: `Error: Binary file format detected. Use sanitize_file for DOCX/PDF/XLSX documents.` }]
|
|
3388
|
-
};
|
|
3389
|
-
}
|
|
3390
|
-
}
|
|
3645
|
+
const ext = path.extname(resolvedPath).toLowerCase();
|
|
3391
3646
|
const license = checkLicenseStatus();
|
|
3392
3647
|
const limitStatus = checkFreeTierLimit(license.isPro);
|
|
3393
3648
|
if (limitStatus.blocked) {
|
|
3394
3649
|
return buildDailyLimitBlockedResponse();
|
|
3395
3650
|
}
|
|
3396
3651
|
|
|
3397
|
-
let rawContent =
|
|
3398
|
-
|
|
3652
|
+
let rawContent = '';
|
|
3653
|
+
if (ext === '.docx') {
|
|
3654
|
+
try {
|
|
3655
|
+
const mammoth = require('mammoth');
|
|
3656
|
+
const result = await mammoth.extractRawText({ buffer: rawBuffer });
|
|
3657
|
+
rawContent = result.value || '';
|
|
3658
|
+
} catch (docxErr) {
|
|
3659
|
+
return {
|
|
3660
|
+
isError: true,
|
|
3661
|
+
content: [{ type: "text", text: `Error: Failed to parse DOCX file: ${docxErr.message}` }]
|
|
3662
|
+
};
|
|
3663
|
+
}
|
|
3664
|
+
} else if (ext === '.pdf' || ext === '.xlsx' || ext === '.xls') {
|
|
3665
|
+
if (!license.isPro) {
|
|
3666
|
+
return {
|
|
3667
|
+
isError: true,
|
|
3668
|
+
content: [{
|
|
3669
|
+
type: "text",
|
|
3670
|
+
text: `🔒 [PrivacyScrubber PRO / TEAMS] PDF and Excel file sanitization is a PRO feature (${ext === '.pdf' ? 'PDF' : 'Excel/XLSX'} detected).\n\n👉 7-Day Free B2B Trial (No card): https://privacyscrubber.com/pricing/?tier=teams&trial=true&utm_source=mcp_cli&utm_medium=file_type&utm_campaign=b2b_trial`
|
|
3671
|
+
}]
|
|
3672
|
+
};
|
|
3673
|
+
}
|
|
3674
|
+
if (ext === '.pdf') {
|
|
3675
|
+
try {
|
|
3676
|
+
const pdf = require('pdf-parse');
|
|
3677
|
+
const data = await pdf(rawBuffer);
|
|
3678
|
+
rawContent = data.text || '';
|
|
3679
|
+
} catch (pdfErr) {
|
|
3680
|
+
return {
|
|
3681
|
+
isError: true,
|
|
3682
|
+
content: [{ type: "text", text: `Error: Failed to parse PDF file: ${pdfErr.message}` }]
|
|
3683
|
+
};
|
|
3684
|
+
}
|
|
3685
|
+
} else {
|
|
3686
|
+
try {
|
|
3687
|
+
const XLSX = require('xlsx');
|
|
3688
|
+
const workbook = XLSX.read(rawBuffer, { type: 'buffer' });
|
|
3689
|
+
workbook.SheetNames.forEach(sheetName => {
|
|
3690
|
+
rawContent += `--- Sheet: ${sheetName} ---\n` + XLSX.utils.sheet_to_csv(workbook.Sheets[sheetName]) + '\n';
|
|
3691
|
+
});
|
|
3692
|
+
} catch (xlsxErr) {
|
|
3693
|
+
return {
|
|
3694
|
+
isError: true,
|
|
3695
|
+
content: [{ type: "text", text: `Error: Failed to parse Excel file: ${xlsxErr.message}` }]
|
|
3696
|
+
};
|
|
3697
|
+
}
|
|
3698
|
+
}
|
|
3699
|
+
} else {
|
|
3700
|
+
const checkLen = Math.min(rawBuffer.length, 8000);
|
|
3701
|
+
for (let i = 0; i < checkLen; i++) {
|
|
3702
|
+
if (rawBuffer[i] === 0) {
|
|
3703
|
+
return {
|
|
3704
|
+
isError: true,
|
|
3705
|
+
content: [{ type: "text", text: `Error: Binary file format detected. Only plain text, source code, and document formats (.docx, .pdf, .xlsx, .eml, .rtf) are supported.` }]
|
|
3706
|
+
};
|
|
3707
|
+
}
|
|
3708
|
+
}
|
|
3709
|
+
rawContent = rawBuffer.toString('utf8');
|
|
3710
|
+
}
|
|
3399
3711
|
if (ext === '.eml') {
|
|
3400
3712
|
const decodeQpUtf8 = (str) =>
|
|
3401
3713
|
str.replace(/=\r?\n/g, '').replace(/(?:=[0-9A-Fa-f]{2})+/g, (seq) => {
|
|
@@ -3464,7 +3776,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3464
3776
|
const { staged = false, cwd, session_id = "default" } = args || {};
|
|
3465
3777
|
const targetSession = getSessionMap(session_id);
|
|
3466
3778
|
const targetIgnoreList = getSessionIgnoreList(session_id);
|
|
3467
|
-
|
|
3779
|
+
let repoCwd = process.cwd();
|
|
3780
|
+
if (cwd) {
|
|
3781
|
+
const safeCwd = resolveWorkspaceSafePath(cwd, "cwd");
|
|
3782
|
+
if (!safeCwd.ok) {
|
|
3783
|
+
return { isError: true, content: [{ type: "text", text: safeCwd.error }] };
|
|
3784
|
+
}
|
|
3785
|
+
repoCwd = safeCwd.resolvedPath;
|
|
3786
|
+
}
|
|
3468
3787
|
const license = checkLicenseStatus();
|
|
3469
3788
|
const limitStatus = checkFreeTierLimit(license.isPro);
|
|
3470
3789
|
if (limitStatus.blocked) {
|
|
@@ -3528,7 +3847,30 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3528
3847
|
};
|
|
3529
3848
|
}
|
|
3530
3849
|
|
|
3531
|
-
const
|
|
3850
|
+
const safeTarget = resolveWorkspaceSafePath(file_path, "file_path");
|
|
3851
|
+
if (!safeTarget.ok) {
|
|
3852
|
+
return {
|
|
3853
|
+
isError: true,
|
|
3854
|
+
content: [{ type: "text", text: safeTarget.error }]
|
|
3855
|
+
};
|
|
3856
|
+
}
|
|
3857
|
+
const resolvedPath = safeTarget.resolvedPath;
|
|
3858
|
+
|
|
3859
|
+
const license = checkLicenseStatus();
|
|
3860
|
+
const limitStatus = checkFreeTierLimit(license.isPro);
|
|
3861
|
+
if (limitStatus.blocked) {
|
|
3862
|
+
return buildDailyLimitBlockedResponse();
|
|
3863
|
+
}
|
|
3864
|
+
if (fs.existsSync(resolvedPath)) {
|
|
3865
|
+
const existingLstat = fs.lstatSync(resolvedPath);
|
|
3866
|
+
if (existingLstat.isSymbolicLink() || !existingLstat.isFile()) {
|
|
3867
|
+
return {
|
|
3868
|
+
isError: true,
|
|
3869
|
+
content: [{ type: "text", text: "Error: Security policy violation — target must be a regular non-symlink file." }]
|
|
3870
|
+
};
|
|
3871
|
+
}
|
|
3872
|
+
}
|
|
3873
|
+
|
|
3532
3874
|
const targetSession = getSessionMap(session_id);
|
|
3533
3875
|
const restored = PrivacyScrubberCore.unscrubText(content, targetSession);
|
|
3534
3876
|
const authenticContent = restored.restoredText;
|
|
@@ -3561,8 +3903,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3561
3903
|
|
|
3562
3904
|
if (name === "create_agent_rules") {
|
|
3563
3905
|
const { agent_types = ["all"], workspace_dir } = args || {};
|
|
3564
|
-
|
|
3565
|
-
|
|
3906
|
+
let targetDir = process.cwd();
|
|
3907
|
+
if (workspace_dir) {
|
|
3908
|
+
const safeWorkspace = resolveWorkspaceSafePath(workspace_dir, "workspace_dir");
|
|
3909
|
+
if (!safeWorkspace.ok) {
|
|
3910
|
+
return { isError: true, content: [{ type: "text", text: safeWorkspace.error }] };
|
|
3911
|
+
}
|
|
3912
|
+
targetDir = safeWorkspace.resolvedPath;
|
|
3913
|
+
}
|
|
3914
|
+
const targets = agent_types.includes("all") ? ["cursor", "windsurf", "claude_code", "copilot", "cline", "agents_md"] : agent_types;
|
|
3566
3915
|
|
|
3567
3916
|
const ruleText = `# PrivacyScrubber Zero-Trust Agentic Guard (CISO Security Rules)
|
|
3568
3917
|
# 100% Local In-Memory Data Sanitization. Zero Network Egress.
|
|
@@ -3586,6 +3935,14 @@ Before reading sensitive files, running terminal commands that may print credent
|
|
|
3586
3935
|
const p = path.join(targetDir, '.cursorrules');
|
|
3587
3936
|
fs.writeFileSync(p, ruleText, 'utf8');
|
|
3588
3937
|
generatedFiles.push('.cursorrules');
|
|
3938
|
+
|
|
3939
|
+
const cursorRulesDir = path.join(targetDir, '.cursor', 'rules');
|
|
3940
|
+
try {
|
|
3941
|
+
if (!fs.existsSync(cursorRulesDir)) fs.mkdirSync(cursorRulesDir, { recursive: true });
|
|
3942
|
+
const mdcContent = `---\ndescription: PrivacyScrubber Zero-Trust Data Sanitization (ZTDS) Agentic Guard\nglobs: *\nalwaysApply: true\n---\n\n${ruleText}`;
|
|
3943
|
+
fs.writeFileSync(path.join(cursorRulesDir, 'privacyscrubber.mdc'), mdcContent, 'utf8');
|
|
3944
|
+
generatedFiles.push('.cursor/rules/privacyscrubber.mdc');
|
|
3945
|
+
} catch (_) {}
|
|
3589
3946
|
}
|
|
3590
3947
|
if (targets.includes("windsurf")) {
|
|
3591
3948
|
const p = path.join(targetDir, '.windsurfrules');
|
|
@@ -3605,6 +3962,19 @@ Before reading sensitive files, running terminal commands that may print credent
|
|
|
3605
3962
|
generatedFiles.push('CLAUDE.md');
|
|
3606
3963
|
}
|
|
3607
3964
|
}
|
|
3965
|
+
if (targets.includes("agents_md") || targets.includes("codex")) {
|
|
3966
|
+
const p = path.join(targetDir, 'AGENTS.md');
|
|
3967
|
+
if (fs.existsSync(p)) {
|
|
3968
|
+
const existing = fs.readFileSync(p, 'utf8');
|
|
3969
|
+
if (!existing.includes("Zero-Trust Agentic Guard")) {
|
|
3970
|
+
fs.writeFileSync(p, existing + "\n\n" + ruleText, 'utf8');
|
|
3971
|
+
generatedFiles.push('AGENTS.md (appended)');
|
|
3972
|
+
}
|
|
3973
|
+
} else {
|
|
3974
|
+
fs.writeFileSync(p, ruleText, 'utf8');
|
|
3975
|
+
generatedFiles.push('AGENTS.md');
|
|
3976
|
+
}
|
|
3977
|
+
}
|
|
3608
3978
|
if (targets.includes("copilot")) {
|
|
3609
3979
|
const copilotDir = path.join(targetDir, '.github');
|
|
3610
3980
|
if (!fs.existsSync(copilotDir)) fs.mkdirSync(copilotDir, { recursive: true });
|
|
@@ -3618,7 +3988,57 @@ Before reading sensitive files, running terminal commands that may print credent
|
|
|
3618
3988
|
generatedFiles.push('.clinerules');
|
|
3619
3989
|
}
|
|
3620
3990
|
|
|
3991
|
+
if (args?.include_hooks !== false && (targets.includes("cursor") || targets.includes("claude_code") || agent_types.includes("all"))) {
|
|
3992
|
+
const hookCmd = "npx -y @privacyscrubber/mcp-server --hook";
|
|
3993
|
+
if (targets.includes("cursor") || agent_types.includes("all")) {
|
|
3994
|
+
try {
|
|
3995
|
+
const cursorHooksPath = path.join(targetDir, '.cursor', 'hooks.json');
|
|
3996
|
+
fs.mkdirSync(path.dirname(cursorHooksPath), { recursive: true });
|
|
3997
|
+
let cursorHooks = { version: 1, hooks: {} };
|
|
3998
|
+
if (fs.existsSync(cursorHooksPath)) {
|
|
3999
|
+
try { cursorHooks = JSON.parse(fs.readFileSync(cursorHooksPath, 'utf8')); } catch (_) {}
|
|
4000
|
+
}
|
|
4001
|
+
if (!cursorHooks.hooks || typeof cursorHooks.hooks !== 'object') cursorHooks.hooks = {};
|
|
4002
|
+
for (const ev of ['beforeReadFile', 'beforeShellExecution']) {
|
|
4003
|
+
if (!Array.isArray(cursorHooks.hooks[ev])) cursorHooks.hooks[ev] = [];
|
|
4004
|
+
if (!cursorHooks.hooks[ev].some(h => h && h.command && h.command.includes('privacyscrubber'))) {
|
|
4005
|
+
cursorHooks.hooks[ev].push({ command: hookCmd });
|
|
4006
|
+
}
|
|
4007
|
+
}
|
|
4008
|
+
fs.writeFileSync(cursorHooksPath, JSON.stringify(cursorHooks, null, 2) + '\n', 'utf8');
|
|
4009
|
+
generatedFiles.push('.cursor/hooks.json');
|
|
4010
|
+
} catch (_) {}
|
|
4011
|
+
}
|
|
4012
|
+
if (targets.includes("claude_code") || agent_types.includes("all")) {
|
|
4013
|
+
try {
|
|
4014
|
+
const claudeSettingsPath = path.join(targetDir, '.claude', 'settings.json');
|
|
4015
|
+
fs.mkdirSync(path.dirname(claudeSettingsPath), { recursive: true });
|
|
4016
|
+
let claudeSettings = {};
|
|
4017
|
+
if (fs.existsSync(claudeSettingsPath)) {
|
|
4018
|
+
try { claudeSettings = JSON.parse(fs.readFileSync(claudeSettingsPath, 'utf8')); } catch (_) {}
|
|
4019
|
+
}
|
|
4020
|
+
if (!claudeSettings.hooks || typeof claudeSettings.hooks !== 'object') claudeSettings.hooks = {};
|
|
4021
|
+
if (!Array.isArray(claudeSettings.hooks.PreToolUse)) claudeSettings.hooks.PreToolUse = [];
|
|
4022
|
+
const hasClaudeHook = claudeSettings.hooks.PreToolUse.some(entry =>
|
|
4023
|
+
Array.isArray(entry?.hooks) && entry.hooks.some(h => h && h.command && h.command.includes('privacyscrubber'))
|
|
4024
|
+
);
|
|
4025
|
+
if (!hasClaudeHook) {
|
|
4026
|
+
claudeSettings.hooks.PreToolUse.push({
|
|
4027
|
+
matcher: "Read|Bash",
|
|
4028
|
+
hooks: [{ type: "command", command: hookCmd }]
|
|
4029
|
+
});
|
|
4030
|
+
}
|
|
4031
|
+
fs.writeFileSync(claudeSettingsPath, JSON.stringify(claudeSettings, null, 2) + '\n', 'utf8');
|
|
4032
|
+
generatedFiles.push('.claude/settings.json');
|
|
4033
|
+
} catch (_) {}
|
|
4034
|
+
}
|
|
4035
|
+
}
|
|
4036
|
+
|
|
3621
4037
|
return {
|
|
4038
|
+
structuredContent: {
|
|
4039
|
+
workspace: targetDir,
|
|
4040
|
+
generatedFiles
|
|
4041
|
+
},
|
|
3622
4042
|
content: [{
|
|
3623
4043
|
type: "text",
|
|
3624
4044
|
text: `[Zero-Trust Agentic Guard: Agent Rules Generated]\n` +
|
|
@@ -3641,22 +4061,38 @@ Before reading sensitive files, running terminal commands that may print credent
|
|
|
3641
4061
|
}
|
|
3642
4062
|
|
|
3643
4063
|
const targetSession = getSessionMap(session_id);
|
|
4064
|
+
const seenUnmask = new WeakMap();
|
|
3644
4065
|
|
|
3645
4066
|
function unmaskRecursive(val) {
|
|
3646
4067
|
if (typeof val === 'string') {
|
|
3647
4068
|
return reverseTokens(val, targetSession);
|
|
3648
4069
|
}
|
|
3649
|
-
if (
|
|
3650
|
-
|
|
4070
|
+
if (val === null || typeof val !== 'object') return val;
|
|
4071
|
+
if (
|
|
4072
|
+
val instanceof Date ||
|
|
4073
|
+
val instanceof RegExp ||
|
|
4074
|
+
val instanceof Error ||
|
|
4075
|
+
ArrayBuffer.isView(val) ||
|
|
4076
|
+
(typeof ArrayBuffer !== 'undefined' && val instanceof ArrayBuffer)
|
|
4077
|
+
) {
|
|
4078
|
+
return val;
|
|
3651
4079
|
}
|
|
3652
|
-
if (val
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
4080
|
+
if (seenUnmask.has(val)) return seenUnmask.get(val);
|
|
4081
|
+
if (Array.isArray(val)) {
|
|
4082
|
+
const outArr = [];
|
|
4083
|
+
seenUnmask.set(val, outArr);
|
|
4084
|
+
for (let i = 0; i < val.length; i++) {
|
|
4085
|
+
outArr.push(unmaskRecursive(val[i]));
|
|
3656
4086
|
}
|
|
3657
|
-
return
|
|
4087
|
+
return outArr;
|
|
4088
|
+
}
|
|
4089
|
+
const res = {};
|
|
4090
|
+
seenUnmask.set(val, res);
|
|
4091
|
+
for (const [k, v] of Object.entries(val)) {
|
|
4092
|
+
if (k === '__proto__' || k === 'constructor' || k === 'prototype') continue;
|
|
4093
|
+
res[k] = unmaskRecursive(v);
|
|
3658
4094
|
}
|
|
3659
|
-
return
|
|
4095
|
+
return res;
|
|
3660
4096
|
}
|
|
3661
4097
|
|
|
3662
4098
|
const unmasked = unmaskRecursive(targetArgs);
|
|
@@ -3669,17 +4105,20 @@ Before reading sensitive files, running terminal commands that may print credent
|
|
|
3669
4105
|
}
|
|
3670
4106
|
}
|
|
3671
4107
|
|
|
4108
|
+
const unmaskPayload = {
|
|
4109
|
+
status: "success",
|
|
4110
|
+
success: true,
|
|
4111
|
+
session_id,
|
|
4112
|
+
tokens_restored: tokensRestored,
|
|
4113
|
+
unmasked_count: tokensRestored,
|
|
4114
|
+
unmasked_args: unmasked
|
|
4115
|
+
};
|
|
4116
|
+
|
|
3672
4117
|
return {
|
|
4118
|
+
structuredContent: unmaskPayload,
|
|
3673
4119
|
content: [{
|
|
3674
4120
|
type: "text",
|
|
3675
|
-
text: JSON.stringify(
|
|
3676
|
-
status: "success",
|
|
3677
|
-
success: true,
|
|
3678
|
-
session_id,
|
|
3679
|
-
tokens_restored: tokensRestored,
|
|
3680
|
-
unmasked_count: tokensRestored,
|
|
3681
|
-
unmasked_args: unmasked
|
|
3682
|
-
}, null, 2)
|
|
4121
|
+
text: JSON.stringify(unmaskPayload, null, 2)
|
|
3683
4122
|
}]
|
|
3684
4123
|
};
|
|
3685
4124
|
}
|
|
@@ -3693,6 +4132,7 @@ Before reading sensitive files, running terminal commands that may print credent
|
|
|
3693
4132
|
const session = agentSessions.get(sid);
|
|
3694
4133
|
if (!session) {
|
|
3695
4134
|
return {
|
|
4135
|
+
structuredContent: { sessionId: sid, exists: false, totalTokens: 0 },
|
|
3696
4136
|
content: [{
|
|
3697
4137
|
type: "text",
|
|
3698
4138
|
text: `[Zero-Trust Agentic Guard: Session Info]\nSession '${sid}' does not exist or has expired.`
|
|
@@ -3703,6 +4143,16 @@ Before reading sensitive files, running terminal commands that may print credent
|
|
|
3703
4143
|
const telemetry = buildCisoAuditTelemetry(session.map);
|
|
3704
4144
|
const streamBufferLen = session.streamDetokenizer ? session.streamDetokenizer.buffer.length : 0;
|
|
3705
4145
|
return {
|
|
4146
|
+
structuredContent: {
|
|
4147
|
+
sessionId: sid,
|
|
4148
|
+
exists: true,
|
|
4149
|
+
createdAt: new Date(session.createdAt).toISOString(),
|
|
4150
|
+
lastActive: new Date(session.lastActive).toISOString(),
|
|
4151
|
+
totalTokens: tokenEntries.length,
|
|
4152
|
+
streamBufferBytes: streamBufferLen,
|
|
4153
|
+
entities: telemetry.entities,
|
|
4154
|
+
riskLevel: telemetry.riskLevel
|
|
4155
|
+
},
|
|
3706
4156
|
content: [{
|
|
3707
4157
|
type: "text",
|
|
3708
4158
|
text: `[Zero-Trust Agentic Guard: Session Info - ${sid}]\n` +
|
|
@@ -3727,6 +4177,13 @@ Before reading sensitive files, running terminal commands that may print credent
|
|
|
3727
4177
|
}
|
|
3728
4178
|
|
|
3729
4179
|
return {
|
|
4180
|
+
structuredContent: {
|
|
4181
|
+
activeSessions: agentSessions.size,
|
|
4182
|
+
totalTokens,
|
|
4183
|
+
heapUsedMb: Number((memUsage.heapUsed / (1024 * 1024)).toFixed(2)),
|
|
4184
|
+
uptimeSeconds: Math.floor(process.uptime()),
|
|
4185
|
+
sessions: sessionList
|
|
4186
|
+
},
|
|
3730
4187
|
content: [{
|
|
3731
4188
|
type: "text",
|
|
3732
4189
|
text: `[Zero-Trust Agentic Guard: Global Session Diagnostics]\n` +
|
|
@@ -3825,19 +4282,21 @@ Before reading sensitive files, running terminal commands that may print credent
|
|
|
3825
4282
|
|
|
3826
4283
|
const telemetry = buildCisoAuditTelemetry(targetSession);
|
|
3827
4284
|
const auditReceipt = formatAuditReceipt(telemetry);
|
|
4285
|
+
const ragChunkPayload = {
|
|
4286
|
+
status: "success",
|
|
4287
|
+
session_id,
|
|
4288
|
+
total_chunks: chunks.length,
|
|
4289
|
+
tokens_redacted_in_batch: totalTokensRedacted,
|
|
4290
|
+
total_session_tokens: Object.keys(targetSession).length,
|
|
4291
|
+
risk_level: telemetry.riskLevel,
|
|
4292
|
+
sanitized_chunks: sanitizedChunks
|
|
4293
|
+
};
|
|
3828
4294
|
|
|
3829
4295
|
return {
|
|
4296
|
+
structuredContent: ragChunkPayload,
|
|
3830
4297
|
content: [{
|
|
3831
4298
|
type: "text",
|
|
3832
|
-
text: JSON.stringify({
|
|
3833
|
-
status: "success",
|
|
3834
|
-
session_id,
|
|
3835
|
-
total_chunks: chunks.length,
|
|
3836
|
-
tokens_redacted_in_batch: totalTokensRedacted,
|
|
3837
|
-
total_session_tokens: Object.keys(targetSession).length,
|
|
3838
|
-
risk_level: telemetry.riskLevel,
|
|
3839
|
-
sanitized_chunks: sanitizedChunks
|
|
3840
|
-
}, null, 2) + `\n\n${auditReceipt}`
|
|
4299
|
+
text: JSON.stringify(ragChunkPayload, null, 2) + `\n\n${auditReceipt}`
|
|
3841
4300
|
}]
|
|
3842
4301
|
};
|
|
3843
4302
|
}
|
|
@@ -3885,16 +4344,19 @@ Before reading sensitive files, running terminal commands that may print credent
|
|
|
3885
4344
|
}
|
|
3886
4345
|
}
|
|
3887
4346
|
|
|
4347
|
+
const ragRestorePayload = {
|
|
4348
|
+
status: "success",
|
|
4349
|
+
session_id,
|
|
4350
|
+
total_results: results.length,
|
|
4351
|
+
tokens_restored: totalTokensRestored,
|
|
4352
|
+
restored_results: restoredResults
|
|
4353
|
+
};
|
|
4354
|
+
|
|
3888
4355
|
return {
|
|
4356
|
+
structuredContent: ragRestorePayload,
|
|
3889
4357
|
content: [{
|
|
3890
4358
|
type: "text",
|
|
3891
|
-
text: JSON.stringify(
|
|
3892
|
-
status: "success",
|
|
3893
|
-
session_id,
|
|
3894
|
-
total_results: results.length,
|
|
3895
|
-
tokens_restored: totalTokensRestored,
|
|
3896
|
-
restored_results: restoredResults
|
|
3897
|
-
}, null, 2)
|
|
4359
|
+
text: JSON.stringify(ragRestorePayload, null, 2)
|
|
3898
4360
|
}]
|
|
3899
4361
|
};
|
|
3900
4362
|
}
|
|
@@ -3923,6 +4385,9 @@ Before reading sensitive files, running terminal commands that may print credent
|
|
|
3923
4385
|
const sessionHash = receipt.receiptSha256;
|
|
3924
4386
|
|
|
3925
4387
|
let outputText = "";
|
|
4388
|
+
const safeCompanyName = String(company_name || "Organization").replace(/[\r\n\x00-\x1f]/g, " ").trim().slice(0, 120);
|
|
4389
|
+
const safeDepartment = String(department || "Security & Compliance").replace(/[\r\n\x00-\x1f]/g, " ").trim().slice(0, 120);
|
|
4390
|
+
|
|
3926
4391
|
const eventData = {
|
|
3927
4392
|
vendor: "PrivacyScrubber",
|
|
3928
4393
|
product: "ZTDS",
|
|
@@ -3933,8 +4398,8 @@ Before reading sensitive files, running terminal commands that may print credent
|
|
|
3933
4398
|
entities: telemetry.entities,
|
|
3934
4399
|
frameworks: telemetry.frameworksList,
|
|
3935
4400
|
sessionHash,
|
|
3936
|
-
organization:
|
|
3937
|
-
department
|
|
4401
|
+
organization: safeCompanyName,
|
|
4402
|
+
department: safeDepartment
|
|
3938
4403
|
};
|
|
3939
4404
|
|
|
3940
4405
|
if (format === "cef") {
|
|
@@ -3942,19 +4407,35 @@ Before reading sensitive files, running terminal commands that may print credent
|
|
|
3942
4407
|
} else if (format === "syslog") {
|
|
3943
4408
|
outputText = formatSyslogEvent(eventData);
|
|
3944
4409
|
} else if (format === "json" || format === "markdown") {
|
|
3945
|
-
outputText = formatPsr2Report({ format, receipt, telemetry, company_name, department, utmSource: 'mcp_audit_export', utmMedium: format });
|
|
4410
|
+
outputText = formatPsr2Report({ format, receipt, telemetry, company_name: safeCompanyName, department: safeDepartment, utmSource: 'mcp_audit_export', utmMedium: format });
|
|
3946
4411
|
} else {
|
|
3947
4412
|
// Default: jsonl
|
|
3948
4413
|
outputText = formatJsonlEvent(eventData);
|
|
3949
4414
|
}
|
|
3950
4415
|
|
|
3951
4416
|
if (typeof destination_file === 'string' && destination_file.trim()) {
|
|
4417
|
+
const safeDest = resolveWorkspaceSafePath(destination_file.trim(), "destination_file");
|
|
4418
|
+
if (!safeDest.ok) {
|
|
4419
|
+
return {
|
|
4420
|
+
isError: true,
|
|
4421
|
+
content: [{ type: "text", text: safeDest.error }]
|
|
4422
|
+
};
|
|
4423
|
+
}
|
|
3952
4424
|
try {
|
|
3953
|
-
|
|
4425
|
+
if (fs.existsSync(safeDest.resolvedPath)) {
|
|
4426
|
+
const st = fs.lstatSync(safeDest.resolvedPath);
|
|
4427
|
+
if (st.isSymbolicLink() || !st.isFile()) {
|
|
4428
|
+
return {
|
|
4429
|
+
isError: true,
|
|
4430
|
+
content: [{ type: "text", text: "Error: Security policy violation — destination_file must be a regular non-symlink file." }]
|
|
4431
|
+
};
|
|
4432
|
+
}
|
|
4433
|
+
}
|
|
4434
|
+
const dir = path.dirname(safeDest.resolvedPath);
|
|
3954
4435
|
if (!fs.existsSync(dir)) {
|
|
3955
4436
|
fs.mkdirSync(dir, { recursive: true });
|
|
3956
4437
|
}
|
|
3957
|
-
fs.appendFileSync(
|
|
4438
|
+
fs.appendFileSync(safeDest.resolvedPath, outputText + '\n', 'utf8');
|
|
3958
4439
|
} catch (e) {
|
|
3959
4440
|
return {
|
|
3960
4441
|
isError: true,
|
|
@@ -3964,6 +4445,15 @@ Before reading sensitive files, running terminal commands that may print credent
|
|
|
3964
4445
|
}
|
|
3965
4446
|
|
|
3966
4447
|
return {
|
|
4448
|
+
structuredContent: {
|
|
4449
|
+
format,
|
|
4450
|
+
riskLevel: telemetry.riskLevel,
|
|
4451
|
+
tokensMasked: telemetry.totalCount,
|
|
4452
|
+
entities: telemetry.entities,
|
|
4453
|
+
frameworks: telemetry.frameworksList,
|
|
4454
|
+
receiptSha256: sessionHash,
|
|
4455
|
+
timestamp
|
|
4456
|
+
},
|
|
3967
4457
|
content: [{
|
|
3968
4458
|
type: "text",
|
|
3969
4459
|
text: outputText
|
|
@@ -4051,6 +4541,18 @@ function getSessionMetricsSummary() {
|
|
|
4051
4541
|
return parts.join(', ');
|
|
4052
4542
|
}
|
|
4053
4543
|
|
|
4544
|
+
function isCatastrophicRegex(pat) {
|
|
4545
|
+
if (!pat || typeof pat !== 'string' || pat.length > 500) return true;
|
|
4546
|
+
const nestedQuantifierRegex = /\([^)]*(\+|\*|\{\d+,?\d*\})\)[+*]|\([^)]*(\+|\*|\{\d+,?\d*\})\)\{\d+,?\d*\}|\([^)]*\|[^)]*\)(?:[+*]|\{\d+,?\d*\})/;
|
|
4547
|
+
if (nestedQuantifierRegex.test(pat)) return true;
|
|
4548
|
+
try {
|
|
4549
|
+
new RegExp(pat, 'gi');
|
|
4550
|
+
return false;
|
|
4551
|
+
} catch (_) {
|
|
4552
|
+
return true;
|
|
4553
|
+
}
|
|
4554
|
+
}
|
|
4555
|
+
|
|
4054
4556
|
function loadCustomRules() {
|
|
4055
4557
|
const license = checkLicenseStatus();
|
|
4056
4558
|
const configPath = resolveConfigPath();
|
|
@@ -4066,7 +4568,7 @@ function loadCustomRules() {
|
|
|
4066
4568
|
label: (r.label || r.category || "CUSTOM").toUpperCase(),
|
|
4067
4569
|
regex: r.pattern || r.regex || "",
|
|
4068
4570
|
category: (r.label || r.category || "CUSTOM").toUpperCase()
|
|
4069
|
-
})).filter(r => r.pattern);
|
|
4571
|
+
})).filter(r => r.pattern && !isCatastrophicRegex(r.pattern));
|
|
4070
4572
|
} else {
|
|
4071
4573
|
mcpLog(`${colors.yellowBold}⚠️ [PrivacyScrubber] Custom rules detected in privacyscrubber.json, but are ignored in the Free Tier.${colors.reset}\n${colors.cyan}👉 Set PRIVACYSCRUBBER_KEY to your PRO license key.${colors.reset}\n`);
|
|
4072
4574
|
}
|