@privacyscrubber/sdk 2.2.4 → 2.2.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/LICENSE.md +1 -1
- package/README.md +245 -31
- package/cli.js +123 -7
- package/examples/langchain-rag.js +81 -0
- package/examples/llamaindex-rag.js +81 -0
- package/examples/vector-guard-rag.js +137 -0
- package/index.d.ts +123 -0
- package/index.js +674 -7
- package/package.json +2 -1
- package/ps-license-manager.js +1 -1
- package/ps-pii-engine.cjs +1 -1
- package/scrubber-core.cjs +1 -1
package/index.js
CHANGED
|
@@ -12,6 +12,7 @@ import { createRequire } from 'module';
|
|
|
12
12
|
const require = createRequire(import.meta.url);
|
|
13
13
|
import fs from 'fs';
|
|
14
14
|
import path from 'path';
|
|
15
|
+
import os from 'os';
|
|
15
16
|
import { execSync } from 'child_process';
|
|
16
17
|
import { Transform } from 'stream';
|
|
17
18
|
|
|
@@ -223,7 +224,7 @@ function emitDevSecretNoticeIfNeeded(isPro, tokenMap = {}, limit = 15000) {
|
|
|
223
224
|
process.stderr.write(
|
|
224
225
|
`\n[PrivacyScrubber] Intercepted ${count} API secret(s) before LLM egress: ${secretSample}\n` +
|
|
225
226
|
`[PrivacyScrubber] Protected in local RAM (0 byte network egress). Free Tier active (${limit.toLocaleString()} char cap).\n` +
|
|
226
|
-
`[PrivacyScrubber] Developer SDK ($
|
|
227
|
+
`[PrivacyScrubber] Developer SDK ($299/mo flat) for unlimited backend nodes, CI/CD gates & ETL pipelines: https://privacyscrubber.com/pricing?tier=sdk&src=cli_leak_guard\n\n`
|
|
227
228
|
);
|
|
228
229
|
}
|
|
229
230
|
}
|
|
@@ -288,13 +289,13 @@ Pre-configured for 30 specialized industry profiles and compliance frameworks:
|
|
|
288
289
|
- Latency: +200ms - 400ms added network roundtrip per AI prompt.
|
|
289
290
|
- Sub-processor Risk: Requires vendor DPA, external network egress.
|
|
290
291
|
* PrivacyScrubber Developer SDK:
|
|
291
|
-
- Cost: $
|
|
292
|
+
- Cost: $299 / month flat (or $2,990 / year) for unlimited nodes, microservices, and prompts.
|
|
292
293
|
- Latency: <0.1ms (in-process RAM).
|
|
293
294
|
- Sub-processor Risk: Zero (pure in-memory software library).
|
|
294
295
|
|
|
295
296
|
5. ACTION REQUESTED
|
|
296
297
|
Approve commercial license upgrade for ${developer} (${teamSize} engineers, unlimited backend nodes):
|
|
297
|
-
* License Tier: Developer SDK ($
|
|
298
|
+
* License Tier: Developer SDK ($299/month flat or $2,990/year)
|
|
298
299
|
* Direct Procurement & License Activation: https://privacyscrubber.com/pricing?tier=sdk&src=ciso_memo
|
|
299
300
|
* Enterprise / Invoicing Contact: enterprise@privacyscrubber.com
|
|
300
301
|
`;
|
|
@@ -341,7 +342,7 @@ The SDK enforces out-of-the-box sanitization matching mandatory compliance contr
|
|
|
341
342
|
|
|
342
343
|
| Evaluation Factor | Traditional Cloud DLP (AWS/GCP/Nightfall) | PrivacyScrubber Developer SDK |
|
|
343
344
|
| :--- | :--- | :--- |
|
|
344
|
-
| **Pricing Model** | \$1,500 – \$4,000 / month (metered per GB/token) | **\$
|
|
345
|
+
| **Pricing Model** | \$1,500 – \$4,000 / month (metered per GB/token) | **\$299 / month flat** (unlimited nodes & tokens) |
|
|
345
346
|
| **Network Egress** | Requires outbound network calls to DLP vendor | **0 bytes** (100% local Node.js process memory) |
|
|
346
347
|
| **Added Latency** | +200ms – +450ms network round-trip | **<0.1ms** (in-memory regular expressions) |
|
|
347
348
|
| **Vendor Sub-processors** | Third-party cloud vendor added to vendor list | **None** (zero-network software library) |
|
|
@@ -350,7 +351,7 @@ The SDK enforces out-of-the-box sanitization matching mandatory compliance contr
|
|
|
350
351
|
---
|
|
351
352
|
|
|
352
353
|
### 5. Procurement Recommendation
|
|
353
|
-
We request approval to acquire a **Developer SDK License** (\$
|
|
354
|
+
We request approval to acquire a **Developer SDK License** (\$299/month flat or \$2,990/year) to unlock unlimited throughput across all microservices and CI/CD pipelines.
|
|
354
355
|
|
|
355
356
|
* **Direct Procurement Portal:** [https://privacyscrubber.com/pricing?tier=sdk&src=ciso_memo](https://privacyscrubber.com/pricing?tier=sdk&src=ciso_memo)
|
|
356
357
|
* **Procurement / Invoicing Contact:** \`enterprise@privacyscrubber.com\`
|
|
@@ -821,6 +822,16 @@ export function wrapOpenAI(openaiClient, options = {}) {
|
|
|
821
822
|
const res = engine.sanitize(msg.content, { existingSessionMap: sessionTokenMap });
|
|
822
823
|
Object.assign(sessionTokenMap, res.tokenMap);
|
|
823
824
|
return { ...msg, content: res.scrubbedText };
|
|
825
|
+
} else if (Array.isArray(msg.content)) {
|
|
826
|
+
const sanitizedChunks = msg.content.map(chunk => {
|
|
827
|
+
if (chunk && chunk.type === 'text' && typeof chunk.text === 'string') {
|
|
828
|
+
const res = engine.sanitize(chunk.text, { existingSessionMap: sessionTokenMap });
|
|
829
|
+
Object.assign(sessionTokenMap, res.tokenMap);
|
|
830
|
+
return { ...chunk, text: res.scrubbedText };
|
|
831
|
+
}
|
|
832
|
+
return chunk;
|
|
833
|
+
});
|
|
834
|
+
return { ...msg, content: sanitizedChunks };
|
|
824
835
|
}
|
|
825
836
|
return msg;
|
|
826
837
|
});
|
|
@@ -836,8 +847,17 @@ export function wrapOpenAI(openaiClient, options = {}) {
|
|
|
836
847
|
|
|
837
848
|
if (response && Array.isArray(response.choices)) {
|
|
838
849
|
response.choices.forEach(choice => {
|
|
839
|
-
if (choice.message
|
|
840
|
-
|
|
850
|
+
if (choice.message) {
|
|
851
|
+
if (typeof choice.message.content === 'string') {
|
|
852
|
+
choice.message.content = engine.restore(choice.message.content, sessionTokenMap).restoredText;
|
|
853
|
+
} else if (Array.isArray(choice.message.content)) {
|
|
854
|
+
choice.message.content = choice.message.content.map(chunk => {
|
|
855
|
+
if (chunk && chunk.type === 'text' && typeof chunk.text === 'string') {
|
|
856
|
+
return { ...chunk, text: engine.restore(chunk.text, sessionTokenMap).restoredText };
|
|
857
|
+
}
|
|
858
|
+
return chunk;
|
|
859
|
+
});
|
|
860
|
+
}
|
|
841
861
|
}
|
|
842
862
|
});
|
|
843
863
|
}
|
|
@@ -859,6 +879,364 @@ export function createLangChainTransform(options = {}) {
|
|
|
859
879
|
};
|
|
860
880
|
}
|
|
861
881
|
|
|
882
|
+
/**
|
|
883
|
+
* LangChain Document Transformer for RAG pre-ingestion pipelines.
|
|
884
|
+
* Compliant with LangChain BaseDocumentTransformer and Runnable interfaces.
|
|
885
|
+
*
|
|
886
|
+
* Intercepts LangChain Document objects ({ pageContent, metadata }) before
|
|
887
|
+
* embedding or vector storage, scrubbing PII in pageContent and optional sensitive metadata keys.
|
|
888
|
+
*
|
|
889
|
+
* @param {Object} [options={}]
|
|
890
|
+
* @param {string} [options.profile='General'] Detection profile ('General', 'Medical', 'Finance', 'Dev', etc.)
|
|
891
|
+
* @param {string[]} [options.sensitiveMetadataKeys=[]] Metadata keys to sanitize (e.g. ['author', 'user_id', 'contact'])
|
|
892
|
+
* @param {boolean} [options.attachTelemetry=true] Whether to attach _ztds_telemetry to document metadata
|
|
893
|
+
* @param {boolean} [options.attachTokenMap=false] Whether to attach _ztds_token_map to document metadata
|
|
894
|
+
* @returns {Object} Document transformer with transformDocuments and invoke methods
|
|
895
|
+
*/
|
|
896
|
+
export function createDocumentTransformer(options = {}) {
|
|
897
|
+
const {
|
|
898
|
+
profile = 'General',
|
|
899
|
+
sensitiveMetadataKeys = [],
|
|
900
|
+
attachTelemetry = true,
|
|
901
|
+
attachTokenMap = false,
|
|
902
|
+
...engineOptions
|
|
903
|
+
} = options;
|
|
904
|
+
|
|
905
|
+
const engine = new PrivacyScrubberEngine({ profile, ...engineOptions });
|
|
906
|
+
|
|
907
|
+
async function transformDocuments(documents) {
|
|
908
|
+
if (!Array.isArray(documents)) {
|
|
909
|
+
throw new TypeError("documents must be an array of LangChain Document objects");
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
const transformed = [];
|
|
913
|
+
for (const doc of documents) {
|
|
914
|
+
const pageContent = doc.pageContent || doc.text || "";
|
|
915
|
+
const metadata = { ...(doc.metadata || {}) };
|
|
916
|
+
|
|
917
|
+
// Sanitize page content
|
|
918
|
+
const scrubResult = engine.sanitize(pageContent, { profile });
|
|
919
|
+
|
|
920
|
+
// Sanitize designated sensitive metadata fields if present
|
|
921
|
+
if (Array.isArray(sensitiveMetadataKeys) && sensitiveMetadataKeys.length > 0) {
|
|
922
|
+
for (const key of sensitiveMetadataKeys) {
|
|
923
|
+
if (metadata[key] && typeof metadata[key] === 'string') {
|
|
924
|
+
const metaScrub = engine.sanitize(metadata[key], { profile });
|
|
925
|
+
metadata[key] = metaScrub.scrubbedText;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
if (attachTelemetry) {
|
|
931
|
+
metadata._ztds_telemetry = {
|
|
932
|
+
riskLevel: scrubResult.telemetry?.riskLevel || 'CLEAN (ZERO PII)',
|
|
933
|
+
totalCount: scrubResult.count || 0,
|
|
934
|
+
timestamp: new Date().toISOString()
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
if (attachTokenMap && scrubResult.tokenMap) {
|
|
939
|
+
metadata._ztds_token_map = { ...scrubResult.tokenMap };
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
transformed.push({
|
|
943
|
+
...doc,
|
|
944
|
+
pageContent: scrubResult.scrubbedText,
|
|
945
|
+
metadata
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
return transformed;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
return {
|
|
953
|
+
transformDocuments,
|
|
954
|
+
invoke: async (documents) => transformDocuments(documents),
|
|
955
|
+
sessionMap: engine.sessionMap,
|
|
956
|
+
restore: (output, tokenMap) => engine.restore(output, tokenMap || engine.sessionMap),
|
|
957
|
+
resetSession: () => engine.resetSession()
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
export const createLangChainDocumentTransformer = createDocumentTransformer;
|
|
961
|
+
|
|
962
|
+
/**
|
|
963
|
+
* LlamaIndex Node/Document Transform component for RAG ingestion.
|
|
964
|
+
*
|
|
965
|
+
* Compatible with LlamaIndex ingestion pipelines and custom transformations.
|
|
966
|
+
* Sanitizes node text while preserving node IDs, relationships, and metadata.
|
|
967
|
+
*
|
|
968
|
+
* @param {Object} [options={}]
|
|
969
|
+
* @returns {Object} LlamaIndex transform with transform method
|
|
970
|
+
*/
|
|
971
|
+
export function createLlamaIndexTransform(options = {}) {
|
|
972
|
+
const { profile = 'General', ...engineOptions } = options;
|
|
973
|
+
const engine = new PrivacyScrubberEngine({ profile, ...engineOptions });
|
|
974
|
+
|
|
975
|
+
return {
|
|
976
|
+
transform: (nodes) => {
|
|
977
|
+
if (!nodes) return nodes;
|
|
978
|
+
const isArray = Array.isArray(nodes);
|
|
979
|
+
const list = isArray ? nodes : [nodes];
|
|
980
|
+
|
|
981
|
+
const transformed = list.map((node) => {
|
|
982
|
+
const text = typeof node.getContent === 'function'
|
|
983
|
+
? node.getContent()
|
|
984
|
+
: (node.text || node.pageContent || '');
|
|
985
|
+
const result = engine.sanitize(text, { profile });
|
|
986
|
+
|
|
987
|
+
if (typeof node.setContent === 'function') {
|
|
988
|
+
node.setContent(result.scrubbedText);
|
|
989
|
+
} else {
|
|
990
|
+
node.text = result.scrubbedText;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
if (node.metadata) {
|
|
994
|
+
node.metadata._ztds_sanitized = true;
|
|
995
|
+
node.metadata._ztds_pii_count = result.count;
|
|
996
|
+
}
|
|
997
|
+
return node;
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
return isArray ? transformed : transformed[0];
|
|
1001
|
+
},
|
|
1002
|
+
sessionMap: engine.sessionMap,
|
|
1003
|
+
restore: (text, map) => engine.restore(text, map || engine.sessionMap),
|
|
1004
|
+
resetSession: () => engine.resetSession()
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
/**
|
|
1009
|
+
* Universal Vector Database Ingestion Guard (Chroma, Pinecone, Qdrant, Weaviate, pgvector, Milvus).
|
|
1010
|
+
*
|
|
1011
|
+
* Guarantees zero-PII vector embedding and storage in local/cloud vector databases.
|
|
1012
|
+
* Provides pre-ingestion record sanitization, query alignment, search result re-hydration,
|
|
1013
|
+
* and transparent proxy wrapping of vector store clients.
|
|
1014
|
+
*
|
|
1015
|
+
* @param {Object} [options={}]
|
|
1016
|
+
* @param {string} [options.profile='General']
|
|
1017
|
+
* @param {string[]} [options.sensitiveMetadataKeys=[]]
|
|
1018
|
+
* @param {boolean} [options.autoRestoreResults=true]
|
|
1019
|
+
* @returns {Object} Vector ingestion guard utilities
|
|
1020
|
+
*/
|
|
1021
|
+
export function createVectorIngestionGuard(options = {}) {
|
|
1022
|
+
const {
|
|
1023
|
+
profile = 'General',
|
|
1024
|
+
sensitiveMetadataKeys = [],
|
|
1025
|
+
autoRestoreResults = true,
|
|
1026
|
+
...engineOptions
|
|
1027
|
+
} = options;
|
|
1028
|
+
|
|
1029
|
+
const engine = new PrivacyScrubberEngine({ profile, ...engineOptions });
|
|
1030
|
+
|
|
1031
|
+
/**
|
|
1032
|
+
* Sanitizes raw database records prior to vector store upsert / add.
|
|
1033
|
+
* Supports standard formats:
|
|
1034
|
+
* - Chroma: { ids: [], documents: [], metadatas: [] }
|
|
1035
|
+
* - Pinecone: [{ id, values, metadata }]
|
|
1036
|
+
* - Qdrant: [{ id, vector, payload: { text, ... } }]
|
|
1037
|
+
* - Universal: [{ id, text, metadata }]
|
|
1038
|
+
*/
|
|
1039
|
+
function sanitizeRecords(records) {
|
|
1040
|
+
if (!records) return records;
|
|
1041
|
+
|
|
1042
|
+
// Chroma-style columnar batch: { ids, documents, metadatas }
|
|
1043
|
+
if (records.documents && Array.isArray(records.documents)) {
|
|
1044
|
+
const sanitizedDocs = [];
|
|
1045
|
+
const sanitizedMetas = records.metadatas ? [...records.metadatas] : [];
|
|
1046
|
+
let totalTokens = 0;
|
|
1047
|
+
|
|
1048
|
+
records.documents.forEach((doc, idx) => {
|
|
1049
|
+
const res = engine.sanitize(doc || '', { profile });
|
|
1050
|
+
sanitizedDocs.push(res.scrubbedText);
|
|
1051
|
+
totalTokens += res.count;
|
|
1052
|
+
|
|
1053
|
+
if (sanitizedMetas[idx]) {
|
|
1054
|
+
const meta = { ...sanitizedMetas[idx] };
|
|
1055
|
+
if (sensitiveMetadataKeys.length > 0) {
|
|
1056
|
+
for (const k of sensitiveMetadataKeys) {
|
|
1057
|
+
if (typeof meta[k] === 'string') {
|
|
1058
|
+
meta[k] = engine.sanitize(meta[k], { profile }).scrubbedText;
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
meta._ztds_sanitized = true;
|
|
1063
|
+
sanitizedMetas[idx] = meta;
|
|
1064
|
+
}
|
|
1065
|
+
});
|
|
1066
|
+
|
|
1067
|
+
return {
|
|
1068
|
+
...records,
|
|
1069
|
+
documents: sanitizedDocs,
|
|
1070
|
+
metadatas: sanitizedMetas,
|
|
1071
|
+
_ztds_total_tokens: totalTokens
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// Array of record objects (Pinecone, Qdrant, Weaviate, LangChain vectorstores)
|
|
1076
|
+
if (Array.isArray(records)) {
|
|
1077
|
+
let totalTokens = 0;
|
|
1078
|
+
const sanitized = records.map((record) => {
|
|
1079
|
+
const copy = { ...record };
|
|
1080
|
+
// Handle text / document / pageContent / payload.text
|
|
1081
|
+
const textField = copy.text !== undefined ? 'text'
|
|
1082
|
+
: copy.document !== undefined ? 'document'
|
|
1083
|
+
: copy.pageContent !== undefined ? 'pageContent'
|
|
1084
|
+
: null;
|
|
1085
|
+
|
|
1086
|
+
if (textField && typeof copy[textField] === 'string') {
|
|
1087
|
+
const res = engine.sanitize(copy[textField], { profile });
|
|
1088
|
+
copy[textField] = res.scrubbedText;
|
|
1089
|
+
totalTokens += res.count;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
// Handle Qdrant payload
|
|
1093
|
+
if (copy.payload && typeof copy.payload === 'object') {
|
|
1094
|
+
copy.payload = { ...copy.payload };
|
|
1095
|
+
if (typeof copy.payload.text === 'string') {
|
|
1096
|
+
const res = engine.sanitize(copy.payload.text, { profile });
|
|
1097
|
+
copy.payload.text = res.scrubbedText;
|
|
1098
|
+
totalTokens += res.count;
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
// Handle metadata
|
|
1103
|
+
if (copy.metadata && typeof copy.metadata === 'object') {
|
|
1104
|
+
copy.metadata = { ...copy.metadata };
|
|
1105
|
+
for (const k of sensitiveMetadataKeys) {
|
|
1106
|
+
if (typeof copy.metadata[k] === 'string') {
|
|
1107
|
+
copy.metadata[k] = engine.sanitize(copy.metadata[k], { profile }).scrubbedText;
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
copy.metadata._ztds_sanitized = true;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
return copy;
|
|
1114
|
+
});
|
|
1115
|
+
|
|
1116
|
+
return sanitized;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
return records;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
/**
|
|
1123
|
+
* Sanitizes user queries before generating search embeddings.
|
|
1124
|
+
* Ensures search vectors align with sanitized tokenized corpus representations.
|
|
1125
|
+
*/
|
|
1126
|
+
function sanitizeQuery(query) {
|
|
1127
|
+
if (typeof query !== 'string') return query;
|
|
1128
|
+
const res = engine.sanitize(query, { profile });
|
|
1129
|
+
const matches = res.scrubbedText.match(/\[[A-Z_]+_\d+\]/g);
|
|
1130
|
+
const tokensDetected = matches ? matches.length : (res.count || 0);
|
|
1131
|
+
return {
|
|
1132
|
+
query: res.scrubbedText,
|
|
1133
|
+
rawQuery: query,
|
|
1134
|
+
tokensDetected,
|
|
1135
|
+
tokenMap: res.tokenMap
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
/**
|
|
1140
|
+
* Restores search results retrieved from vector DB using the volatile session map.
|
|
1141
|
+
*/
|
|
1142
|
+
function restoreSearchResults(results, customTokenMap = null) {
|
|
1143
|
+
const map = customTokenMap || engine.sessionMap;
|
|
1144
|
+
if (!results || Object.keys(map).length === 0) return results;
|
|
1145
|
+
|
|
1146
|
+
function restoreText(t) {
|
|
1147
|
+
if (typeof t !== 'string') return t;
|
|
1148
|
+
return engine.restore(t, map).restoredText;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
if (Array.isArray(results)) {
|
|
1152
|
+
return results.map(item => {
|
|
1153
|
+
const copy = { ...item };
|
|
1154
|
+
if (typeof copy.pageContent === 'string') copy.pageContent = restoreText(copy.pageContent);
|
|
1155
|
+
if (typeof copy.text === 'string') copy.text = restoreText(copy.text);
|
|
1156
|
+
if (typeof copy.document === 'string') copy.document = restoreText(copy.document);
|
|
1157
|
+
if (copy.payload && typeof copy.payload === 'object') {
|
|
1158
|
+
copy.payload = { ...copy.payload };
|
|
1159
|
+
if (typeof copy.payload.text === 'string') {
|
|
1160
|
+
copy.payload.text = restoreText(copy.payload.text);
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
return copy;
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
return results;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
/**
|
|
1171
|
+
* Wraps an existing vector store client instance with zero-trust hooks.
|
|
1172
|
+
*/
|
|
1173
|
+
function wrapVectorStore(vectorStore) {
|
|
1174
|
+
const proxy = new Proxy(vectorStore, {
|
|
1175
|
+
get(target, prop, receiver) {
|
|
1176
|
+
const orig = Reflect.get(target, prop, receiver);
|
|
1177
|
+
if (typeof orig !== 'function') return orig;
|
|
1178
|
+
|
|
1179
|
+
// Intercept ingestion methods: addDocuments, upsert, add, insert
|
|
1180
|
+
if (prop === 'addDocuments') {
|
|
1181
|
+
return async function(docs, ...rest) {
|
|
1182
|
+
if (!Array.isArray(docs)) return orig.apply(target, [docs, ...rest]);
|
|
1183
|
+
const safeDocs = docs.map(doc => {
|
|
1184
|
+
const pageContent = doc.pageContent || doc.text || "";
|
|
1185
|
+
const metadata = { ...(doc.metadata || {}) };
|
|
1186
|
+
const scrubResult = engine.sanitize(pageContent, { profile });
|
|
1187
|
+
for (const key of sensitiveMetadataKeys) {
|
|
1188
|
+
if (metadata[key] && typeof metadata[key] === 'string') {
|
|
1189
|
+
metadata[key] = engine.sanitize(metadata[key], { profile }).scrubbedText;
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
metadata._ztds_telemetry = {
|
|
1193
|
+
riskLevel: scrubResult.telemetry?.riskLevel || 'CLEAN (ZERO PII)',
|
|
1194
|
+
totalCount: scrubResult.count || 0,
|
|
1195
|
+
timestamp: new Date().toISOString()
|
|
1196
|
+
};
|
|
1197
|
+
return { ...doc, pageContent: scrubResult.scrubbedText, metadata };
|
|
1198
|
+
});
|
|
1199
|
+
return orig.apply(target, [safeDocs, ...rest]);
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
if (prop === 'upsert' || prop === 'add' || prop === 'insert') {
|
|
1204
|
+
return async function(records, ...rest) {
|
|
1205
|
+
const safeRecords = sanitizeRecords(records);
|
|
1206
|
+
return orig.apply(target, [safeRecords, ...rest]);
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
// Intercept query methods: similaritySearch, query, search
|
|
1211
|
+
if (prop === 'similaritySearch' || prop === 'query' || prop === 'search') {
|
|
1212
|
+
return async function(queryArg, ...rest) {
|
|
1213
|
+
const safeQ = typeof queryArg === 'string' ? sanitizeQuery(queryArg).query : queryArg;
|
|
1214
|
+
const rawResults = await orig.apply(target, [safeQ, ...rest]);
|
|
1215
|
+
if (autoRestoreResults) {
|
|
1216
|
+
return restoreSearchResults(rawResults);
|
|
1217
|
+
}
|
|
1218
|
+
return rawResults;
|
|
1219
|
+
};
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
return orig.bind(target);
|
|
1223
|
+
}
|
|
1224
|
+
});
|
|
1225
|
+
|
|
1226
|
+
return proxy;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
return {
|
|
1230
|
+
sanitizeRecords,
|
|
1231
|
+
sanitizeQuery,
|
|
1232
|
+
restoreSearchResults,
|
|
1233
|
+
wrapVectorStore,
|
|
1234
|
+
sessionMap: engine.sessionMap,
|
|
1235
|
+
restore: (text, map) => engine.restore(text, map || engine.sessionMap),
|
|
1236
|
+
resetSession: () => engine.resetSession()
|
|
1237
|
+
};
|
|
1238
|
+
}
|
|
1239
|
+
|
|
862
1240
|
/**
|
|
863
1241
|
* Helper to escape special regex characters.
|
|
864
1242
|
*/
|
|
@@ -1229,6 +1607,248 @@ export function createExpressMiddleware(options = {}) {
|
|
|
1229
1607
|
* @param {object} [options] - Options: companyName, department, format ('markdown'|'json'|'summary').
|
|
1230
1608
|
* @returns {object|string}
|
|
1231
1609
|
*/
|
|
1610
|
+
export function formatCefEvent(eventData = {}) {
|
|
1611
|
+
const {
|
|
1612
|
+
vendor = 'PrivacyScrubber',
|
|
1613
|
+
product = 'ZTDS',
|
|
1614
|
+
version = '2.2.5',
|
|
1615
|
+
eventClassId = 'PII_REDACTION',
|
|
1616
|
+
eventName = 'Zero-Trust PII Sanitization Executed',
|
|
1617
|
+
riskLevel = 'LOW EXPOSURE',
|
|
1618
|
+
tokensMasked = 0,
|
|
1619
|
+
entities = {},
|
|
1620
|
+
frameworks = ['ZTDS Standard'],
|
|
1621
|
+
sessionHash = '',
|
|
1622
|
+
source = 'local_ram',
|
|
1623
|
+
action = 'redact_and_mask',
|
|
1624
|
+
organization = '',
|
|
1625
|
+
department = ''
|
|
1626
|
+
} = eventData;
|
|
1627
|
+
|
|
1628
|
+
let severity = 1;
|
|
1629
|
+
if (riskLevel.includes('CRITICAL')) severity = 9;
|
|
1630
|
+
else if (riskLevel.includes('MODERATE')) severity = 6;
|
|
1631
|
+
else if (riskLevel.includes('LOW')) severity = 3;
|
|
1632
|
+
else severity = 0;
|
|
1633
|
+
|
|
1634
|
+
const entitiesStr = Object.entries(entities).map(([k, v]) => `${k}:${v}`).join(',') || 'none';
|
|
1635
|
+
const frameworksStr = Array.isArray(frameworks) ? frameworks.join(',') : String(frameworks);
|
|
1636
|
+
const safeMsg = `Sanitized ${tokensMasked} entity tokens in volatile RAM. 0 bytes network egress.`;
|
|
1637
|
+
|
|
1638
|
+
const extension = [
|
|
1639
|
+
`src=${source}`,
|
|
1640
|
+
`act=${action}`,
|
|
1641
|
+
`cs1Label=RiskLevel cs1=${riskLevel.replace(/\s+/g, '_')}`,
|
|
1642
|
+
`cn1Label=TokensMasked cn1=${tokensMasked}`,
|
|
1643
|
+
`cs2Label=Frameworks cs2=${frameworksStr.replace(/\s+/g, '_')}`,
|
|
1644
|
+
`cs3Label=Entities cs3=${entitiesStr}`,
|
|
1645
|
+
sessionHash ? `cs4Label=SessionHash cs4=${sessionHash.substring(0, 16)}` : '',
|
|
1646
|
+
organization ? `suser=${organization.replace(/\s+/g, '_')}` : '',
|
|
1647
|
+
department ? `cs5Label=Department cs5=${department.replace(/\s+/g, '_')}` : '',
|
|
1648
|
+
`msg=${safeMsg}`
|
|
1649
|
+
].filter(Boolean).join(' ');
|
|
1650
|
+
|
|
1651
|
+
return `CEF:0|${vendor}|${product}|${version}|${eventClassId}|${eventName}|${severity}|${extension}`;
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
export function formatSyslogEvent(eventData = {}) {
|
|
1655
|
+
const {
|
|
1656
|
+
timestamp = new Date().toISOString(),
|
|
1657
|
+
hostname = (typeof os !== 'undefined' && typeof os.hostname === 'function' ? os.hostname() : 'localhost'),
|
|
1658
|
+
pid = (typeof process !== 'undefined' && process.pid ? process.pid : 1),
|
|
1659
|
+
riskLevel = 'LOW EXPOSURE',
|
|
1660
|
+
tokensMasked = 0,
|
|
1661
|
+
frameworks = ['ZTDS Standard'],
|
|
1662
|
+
sessionHash = '',
|
|
1663
|
+
organization = ''
|
|
1664
|
+
} = eventData;
|
|
1665
|
+
|
|
1666
|
+
let pri = 134; // local0.info
|
|
1667
|
+
if (riskLevel.includes('CRITICAL')) pri = 132; // local0.warning
|
|
1668
|
+
else if (riskLevel.includes('MODERATE')) pri = 133; // local0.notice
|
|
1669
|
+
|
|
1670
|
+
const frameworksStr = Array.isArray(frameworks) ? frameworks.join(';') : String(frameworks);
|
|
1671
|
+
const hashPrefix = sessionHash.substring(0, 16) || 'none';
|
|
1672
|
+
const orgStr = organization ? ` org="${organization.replace(/"/g, '')}"` : '';
|
|
1673
|
+
|
|
1674
|
+
const sd = `[ztds@49152 risk="${riskLevel}" tokens="${tokensMasked}" frameworks="${frameworksStr}" session="${hashPrefix}"${orgStr}]`;
|
|
1675
|
+
const msg = `Zero-Trust Data Sanitization executed in RAM. ${tokensMasked} token(s) masked. 0 bytes network egress.`;
|
|
1676
|
+
|
|
1677
|
+
return `<${pri}>1 ${timestamp} ${hostname} privacyscrubber ${pid} ZTDS - ${sd} ${msg}`;
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
export function formatJsonlEvent(eventData = {}) {
|
|
1681
|
+
const {
|
|
1682
|
+
timestamp = new Date().toISOString(),
|
|
1683
|
+
version = '2.2.5',
|
|
1684
|
+
protocol = 'ZTDS',
|
|
1685
|
+
event = 'sanitization',
|
|
1686
|
+
riskLevel = 'LOW EXPOSURE',
|
|
1687
|
+
tokensMasked = 0,
|
|
1688
|
+
entities = {},
|
|
1689
|
+
frameworks = ['ZTDS Standard'],
|
|
1690
|
+
sessionHash = '',
|
|
1691
|
+
organization = '',
|
|
1692
|
+
department = '',
|
|
1693
|
+
executionMs = 0,
|
|
1694
|
+
metadata = {}
|
|
1695
|
+
} = eventData;
|
|
1696
|
+
|
|
1697
|
+
const record = {
|
|
1698
|
+
timestamp,
|
|
1699
|
+
version,
|
|
1700
|
+
protocol,
|
|
1701
|
+
event,
|
|
1702
|
+
risk_level: riskLevel,
|
|
1703
|
+
tokens_masked: tokensMasked,
|
|
1704
|
+
entities,
|
|
1705
|
+
frameworks,
|
|
1706
|
+
session_hash: sessionHash,
|
|
1707
|
+
zero_egress: true,
|
|
1708
|
+
verification_mode: "100% Offline (In-Memory RAM)",
|
|
1709
|
+
...(organization && { organization }),
|
|
1710
|
+
...(department && { department }),
|
|
1711
|
+
...(executionMs && { execution_ms: executionMs }),
|
|
1712
|
+
...(Object.keys(metadata).length > 0 && { metadata })
|
|
1713
|
+
};
|
|
1714
|
+
|
|
1715
|
+
return JSON.stringify(record);
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
export function createAuditLogger(options = {}) {
|
|
1719
|
+
const {
|
|
1720
|
+
format = 'jsonl',
|
|
1721
|
+
destination = null,
|
|
1722
|
+
organization = 'PrivacyScrubber Enterprise Client',
|
|
1723
|
+
department = 'SecOps'
|
|
1724
|
+
} = options;
|
|
1725
|
+
|
|
1726
|
+
let fileHandle = null;
|
|
1727
|
+
let streamHandle = null;
|
|
1728
|
+
const buffer = [];
|
|
1729
|
+
|
|
1730
|
+
if (typeof destination === 'string' && destination.trim()) {
|
|
1731
|
+
try {
|
|
1732
|
+
const dir = path.dirname(destination);
|
|
1733
|
+
if (!fs.existsSync(dir)) {
|
|
1734
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1735
|
+
}
|
|
1736
|
+
fileHandle = destination;
|
|
1737
|
+
} catch {}
|
|
1738
|
+
} else if (destination && typeof destination.write === 'function') {
|
|
1739
|
+
streamHandle = destination;
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
function formatEvent(eventData) {
|
|
1743
|
+
const enriched = {
|
|
1744
|
+
organization,
|
|
1745
|
+
department,
|
|
1746
|
+
timestamp: new Date().toISOString(),
|
|
1747
|
+
...eventData
|
|
1748
|
+
};
|
|
1749
|
+
|
|
1750
|
+
if (format === 'cef') {
|
|
1751
|
+
return formatCefEvent(enriched);
|
|
1752
|
+
}
|
|
1753
|
+
if (format === 'syslog') {
|
|
1754
|
+
return formatSyslogEvent(enriched);
|
|
1755
|
+
}
|
|
1756
|
+
if (format === 'json') {
|
|
1757
|
+
return JSON.stringify(enriched, null, 2);
|
|
1758
|
+
}
|
|
1759
|
+
return formatJsonlEvent(enriched);
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
function writeEntry(line) {
|
|
1763
|
+
buffer.push(line);
|
|
1764
|
+
if (fileHandle) {
|
|
1765
|
+
try {
|
|
1766
|
+
fs.appendFileSync(fileHandle, line + '\n', 'utf8');
|
|
1767
|
+
} catch (e) {
|
|
1768
|
+
console.error('[PrivacyScrubber AuditLogger] File write error:', e);
|
|
1769
|
+
}
|
|
1770
|
+
} else if (streamHandle) {
|
|
1771
|
+
try {
|
|
1772
|
+
streamHandle.write(line + '\n');
|
|
1773
|
+
} catch (e) {
|
|
1774
|
+
console.error('[PrivacyScrubber AuditLogger] Stream write error:', e);
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
function logSanitization(result, context = {}) {
|
|
1780
|
+
if (!result) return '';
|
|
1781
|
+
let tokenMap = {};
|
|
1782
|
+
let telemetry = null;
|
|
1783
|
+
let count = 0;
|
|
1784
|
+
let execMs = 0;
|
|
1785
|
+
|
|
1786
|
+
if (result.tokenMap && typeof result.tokenMap === 'object') {
|
|
1787
|
+
tokenMap = result.tokenMap;
|
|
1788
|
+
telemetry = result.telemetry;
|
|
1789
|
+
count = result.count !== undefined ? result.count : Object.keys(tokenMap).length;
|
|
1790
|
+
execMs = result.executionMs || 0;
|
|
1791
|
+
} else if (typeof result === 'object') {
|
|
1792
|
+
tokenMap = result;
|
|
1793
|
+
count = result.count !== undefined ? result.count : (result.tokensMasked !== undefined ? result.tokensMasked : Object.keys(tokenMap).length);
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
if (!telemetry) {
|
|
1797
|
+
telemetry = buildAuditTelemetry(tokenMap);
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
let sessionHash = '';
|
|
1801
|
+
try {
|
|
1802
|
+
const cryptoMod = require('crypto');
|
|
1803
|
+
sessionHash = cryptoMod.createHash('sha256').update(JSON.stringify(tokenMap) + Date.now()).digest('hex');
|
|
1804
|
+
} catch {
|
|
1805
|
+
sessionHash = 'ztds_' + Math.random().toString(16).substring(2, 10);
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
const eventData = {
|
|
1809
|
+
event: 'pii_sanitization',
|
|
1810
|
+
riskLevel: telemetry.riskLevel || 'CLEAN (ZERO PII)',
|
|
1811
|
+
tokensMasked: count,
|
|
1812
|
+
entities: telemetry.entities || {},
|
|
1813
|
+
frameworks: telemetry.frameworksList || ['ZTDS Standard'],
|
|
1814
|
+
sessionHash,
|
|
1815
|
+
executionMs: execMs,
|
|
1816
|
+
metadata: context.metadata || {}
|
|
1817
|
+
};
|
|
1818
|
+
|
|
1819
|
+
const formatted = formatEvent(eventData);
|
|
1820
|
+
writeEntry(formatted);
|
|
1821
|
+
return formatted;
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
function logEvent(eventData) {
|
|
1825
|
+
const formatted = formatEvent(eventData);
|
|
1826
|
+
writeEntry(formatted);
|
|
1827
|
+
return formatted;
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
function getEntries() {
|
|
1831
|
+
return [...buffer];
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
function clear() {
|
|
1835
|
+
buffer.length = 0;
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
return {
|
|
1839
|
+
formatEvent,
|
|
1840
|
+
logSanitization,
|
|
1841
|
+
logEvent,
|
|
1842
|
+
log: logEvent,
|
|
1843
|
+
getEntries,
|
|
1844
|
+
getBuffer: getEntries,
|
|
1845
|
+
clear,
|
|
1846
|
+
clearBuffer: clear,
|
|
1847
|
+
format,
|
|
1848
|
+
destination
|
|
1849
|
+
};
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1232
1852
|
export function generateAuditReport(sessionMap = {}, options = {}) {
|
|
1233
1853
|
const { companyName = 'PrivacyScrubber Client', department = 'SecOps / Compliance', format = 'markdown' } = options;
|
|
1234
1854
|
const telemetry = buildAuditTelemetry(sessionMap);
|
|
@@ -1244,6 +1864,45 @@ export function generateAuditReport(sessionMap = {}, options = {}) {
|
|
|
1244
1864
|
const timestamp = new Date().toISOString();
|
|
1245
1865
|
const entitySummary = Object.entries(telemetry.entities).map(([t, count]) => `[${t}]: ${count}`).join(', ') || 'None (Clean)';
|
|
1246
1866
|
|
|
1867
|
+
if (format === 'cef') {
|
|
1868
|
+
return formatCefEvent({
|
|
1869
|
+
organization: companyName,
|
|
1870
|
+
department: department,
|
|
1871
|
+
timestamp,
|
|
1872
|
+
sessionHash,
|
|
1873
|
+
riskLevel: telemetry.riskLevel,
|
|
1874
|
+
tokensMasked: telemetry.totalCount,
|
|
1875
|
+
entities: telemetry.entities,
|
|
1876
|
+
frameworks: telemetry.frameworksList
|
|
1877
|
+
});
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
if (format === 'syslog') {
|
|
1881
|
+
return formatSyslogEvent({
|
|
1882
|
+
organization: companyName,
|
|
1883
|
+
department: department,
|
|
1884
|
+
timestamp,
|
|
1885
|
+
sessionHash,
|
|
1886
|
+
riskLevel: telemetry.riskLevel,
|
|
1887
|
+
tokensMasked: telemetry.totalCount,
|
|
1888
|
+
entities: telemetry.entities,
|
|
1889
|
+
frameworks: telemetry.frameworksList
|
|
1890
|
+
});
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
if (format === 'jsonl') {
|
|
1894
|
+
return formatJsonlEvent({
|
|
1895
|
+
organization: companyName,
|
|
1896
|
+
department: department,
|
|
1897
|
+
timestamp,
|
|
1898
|
+
sessionHash,
|
|
1899
|
+
riskLevel: telemetry.riskLevel,
|
|
1900
|
+
tokensMasked: telemetry.totalCount,
|
|
1901
|
+
entities: telemetry.entities,
|
|
1902
|
+
frameworks: telemetry.frameworksList
|
|
1903
|
+
});
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1247
1906
|
if (format === 'json') {
|
|
1248
1907
|
return {
|
|
1249
1908
|
protocol: "Zero-Trust Data Sanitization (ZTDS)",
|
|
@@ -1676,11 +2335,19 @@ export default {
|
|
|
1676
2335
|
buildAuditTelemetry,
|
|
1677
2336
|
formatAuditReceipt,
|
|
1678
2337
|
generateAuditReport,
|
|
2338
|
+
formatCefEvent,
|
|
2339
|
+
formatSyslogEvent,
|
|
2340
|
+
formatJsonlEvent,
|
|
2341
|
+
createAuditLogger,
|
|
1679
2342
|
generateProcurementMemo,
|
|
1680
2343
|
PrivacyScrubberEngine,
|
|
1681
2344
|
PrivacyScrubberLicenseError,
|
|
1682
2345
|
wrapOpenAI,
|
|
1683
2346
|
createLangChainTransform,
|
|
2347
|
+
createDocumentTransformer,
|
|
2348
|
+
createLangChainDocumentTransformer,
|
|
2349
|
+
createLlamaIndexTransform,
|
|
2350
|
+
createVectorIngestionGuard,
|
|
1684
2351
|
StreamingDetokenizer,
|
|
1685
2352
|
createStreamTokenRestorer,
|
|
1686
2353
|
createNodeStreamTokenRestorer,
|