@link-foundation/sensitive-data-sanitizer 0.0.0-stage → 0.11.31

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.
Files changed (78) hide show
  1. package/CHANGELOG.md +530 -0
  2. package/LICENSE +24 -0
  3. package/README.md +184 -2
  4. package/bin/legacy-example-package-name.js +95 -0
  5. package/bin/sensitive-data-sanitizer.js +430 -0
  6. package/bin/stream.js +56 -0
  7. package/docs/API.md +336 -0
  8. package/docs/COVERAGE.md +136 -0
  9. package/docs/HISTORY-REMEDIATION.md +41 -0
  10. package/examples/combined-scanners.mjs +23 -0
  11. package/examples/presidio-bridge.py +32 -0
  12. package/examples/realistic-fakes.mjs +18 -0
  13. package/examples/sanitize-session.mjs +27 -0
  14. package/examples/shared-tool-logs.mjs +21 -0
  15. package/examples/stream-jsonl.mjs +17 -0
  16. package/package.json +124 -4
  17. package/src/adapters.js +204 -0
  18. package/src/arithmetic.js +11 -0
  19. package/src/confidence.js +65 -0
  20. package/src/context.js +424 -0
  21. package/src/credentials.js +54 -0
  22. package/src/detection.js +293 -0
  23. package/src/encoded.js +233 -0
  24. package/src/engines.js +348 -0
  25. package/src/entities.js +166 -0
  26. package/src/escape-spans.js +20 -0
  27. package/src/eslint-plugin.d.ts +10 -0
  28. package/src/eslint-plugin.js +193 -0
  29. package/src/external.js +169 -0
  30. package/src/fake-names.js +222 -0
  31. package/src/fake.js +524 -0
  32. package/src/files.js +131 -0
  33. package/src/gitleaks.js +194 -0
  34. package/src/history-rewrite.js +339 -0
  35. package/src/history.js +103 -0
  36. package/src/identity.js +274 -0
  37. package/src/index.d.ts +321 -0
  38. package/src/index.js +40 -0
  39. package/src/jsonl-stream.js +198 -0
  40. package/src/keywords.js +45 -0
  41. package/src/known.js +164 -0
  42. package/src/legacy.d.ts +3 -0
  43. package/src/legacy.js +2 -0
  44. package/src/locales.js +217 -0
  45. package/src/log-bytes.js +85 -0
  46. package/src/names.js +65 -0
  47. package/src/national-checks.js +129 -0
  48. package/src/national.js +128 -0
  49. package/src/outbound.js +28 -0
  50. package/src/personal-context.js +85 -0
  51. package/src/personal.js +248 -0
  52. package/src/projection.js +126 -0
  53. package/src/propagation.js +163 -0
  54. package/src/public-verifier.js +77 -0
  55. package/src/public.js +153 -0
  56. package/src/record-worker.js +20 -0
  57. package/src/rules.js +210 -0
  58. package/src/sanitizer.js +794 -0
  59. package/src/specimens.js +34 -0
  60. package/src/stream-identity.js +129 -0
  61. package/src/stream-worker.js +18 -0
  62. package/src/stream.js +449 -0
  63. package/src/structured.js +278 -0
  64. package/src/transforms.js +192 -0
  65. package/src/transliteration.js +75 -0
  66. package/src/utf8.js +60 -0
  67. package/src/vendor/entities/aws.json +42 -0
  68. package/src/vendor/entities/azure.json +182 -0
  69. package/src/vendor/entities/google.json +219 -0
  70. package/src/vendor/gitleaks/LICENSE +21 -0
  71. package/src/vendor/gitleaks/provenance.json +8 -0
  72. package/src/vendor/gitleaks/rules-0.json +268 -0
  73. package/src/vendor/gitleaks/rules-1.json +1794 -0
  74. package/src/vendor/gitleaks/rules-2.json +292 -0
  75. package/src/vendor/gitleaks/rules-3.json +263 -0
  76. package/src/vendor/gitleaks/rules-4.json +277 -0
  77. package/src/vendor/gitleaks/rules-5.json +154 -0
  78. package/src/worker-options.js +27 -0
@@ -0,0 +1,27 @@
1
+ import { createSanitizer } from '../src/index.js';
2
+
3
+ // Synthetic data: supply real knownSecrets from your secret store in memory.
4
+ const sanitizer = createSanitizer({
5
+ profile: 'publication',
6
+ structured: 'jsonl',
7
+ structuralFields: ['id', 'type', 'name', 'description'],
8
+ knownSecrets: ['an-opaque-synthetic-secret'],
9
+ publicEntities: [
10
+ {
11
+ type: 'EMAIL',
12
+ value: 'press@example.org',
13
+ source: 'https://example.org/contact',
14
+ reviewedAt: '2026-10-07',
15
+ },
16
+ ],
17
+ });
18
+ const session = JSON.stringify({
19
+ type: 'tool_result',
20
+ id: 'req_123',
21
+ name: 'Read',
22
+ stdout: 'password=short contact=private@example.org',
23
+ publicContact: 'press@example.org',
24
+ });
25
+ const result = await sanitizer.sanitizeJsonl(session);
26
+ console.log(result.text);
27
+ console.log({ findings: result.findings.length });
@@ -0,0 +1,21 @@
1
+ import { createReadStream } from 'node:fs';
2
+ import {
3
+ knownSecretsFromEnv,
4
+ knownSecretsFromGitHubConfig,
5
+ sanitizeStreamToFile,
6
+ } from '../src/index.js';
7
+
8
+ // Shared integration for upload-log tools. Publish the output only after the
9
+ // atomic operation succeeds. Neither helper prints credential values.
10
+ const [input, output] = process.argv.slice(2);
11
+ if (!input || !output) {
12
+ throw new Error('Usage: node examples/shared-tool-logs.mjs INPUT OUTPUT');
13
+ }
14
+ await sanitizeStreamToFile(createReadStream(input), output, {
15
+ twoPass: true,
16
+ credentialsOnly: true,
17
+ knownSecrets: [
18
+ ...knownSecretsFromEnv(),
19
+ ...(await knownSecretsFromGitHubConfig()),
20
+ ],
21
+ });
@@ -0,0 +1,17 @@
1
+ import { createReadStream } from 'node:fs';
2
+ import { sanitizeStreamToFile } from '../src/index.js';
3
+
4
+ const [source, target] = process.argv.slice(2);
5
+ if (!source || !target) {
6
+ throw new Error(
7
+ 'Usage: node examples/stream-jsonl.mjs SOURCE.jsonl NEWFILE.jsonl'
8
+ );
9
+ }
10
+ await sanitizeStreamToFile(createReadStream(source), target, {
11
+ profile: 'publication',
12
+ structured: 'jsonl',
13
+ // Collect identities before redacting earlier unlabelled occurrences.
14
+ twoPass: true,
15
+ workers: 2,
16
+ });
17
+ console.log('Verified session written:', target);
package/package.json CHANGED
@@ -1,6 +1,126 @@
1
1
  {
2
2
  "name": "@link-foundation/sensitive-data-sanitizer",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.11.31",
4
+ "description": "Offline sensitive-text sanitization with credential, multilingual personal-data and scanner union detection",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "types": "src/index.d.ts",
8
+ "bin": {
9
+ "sensitive-data-sanitizer": "./bin/sensitive-data-sanitizer.js",
10
+ "sensitive-data-sanitizer-legacy": "./bin/legacy-example-package-name.js"
11
+ },
12
+ "files": [
13
+ "bin/",
14
+ "src/",
15
+ "docs/API.md",
16
+ "docs/COVERAGE.md",
17
+ "docs/HISTORY-REMEDIATION.md",
18
+ "examples/combined-scanners.mjs",
19
+ "examples/presidio-bridge.py",
20
+ "examples/sanitize-session.mjs",
21
+ "examples/realistic-fakes.mjs",
22
+ "examples/stream-jsonl.mjs",
23
+ "CHANGELOG.md",
24
+ "LICENSE",
25
+ "README.md",
26
+ "examples/shared-tool-logs.mjs"
27
+ ],
28
+ "exports": {
29
+ ".": {
30
+ "types": "./src/index.d.ts",
31
+ "import": "./src/index.js"
32
+ },
33
+ "./eslint-plugin": {
34
+ "types": "./src/eslint-plugin.d.ts",
35
+ "import": "./src/eslint-plugin.js"
36
+ },
37
+ "./legacy": {
38
+ "types": "./src/legacy.d.ts",
39
+ "browser": "./src/arithmetic.js",
40
+ "import": "./src/legacy.js"
41
+ }
42
+ },
43
+ "scripts": {
44
+ "test": "node --test --test-timeout=30000 tests/*.test.js",
45
+ "lint": "node scripts/lint.mjs",
46
+ "lint:all": "eslint .",
47
+ "lint:fix": "eslint . --fix",
48
+ "format": "prettier --write .",
49
+ "format:check": "prettier --check .",
50
+ "check:duplication": "jscpd .",
51
+ "check:secrets": "secretlint \"**/*\"",
52
+ "check": "npm run lint && npm run format:check && npm run check:duplication",
53
+ "example:web:dev": "npm --prefix examples/universal-app run dev",
54
+ "example:web:build": "npm --prefix examples/universal-app run build",
55
+ "example:web:preview": "npm --prefix examples/universal-app run preview",
56
+ "example:web:preview-images": "node scripts/update-preview-images.mjs",
57
+ "example:desktop:package": "npm --prefix examples/universal-app run desktop:package",
58
+ "example:desktop:make": "npm --prefix examples/universal-app run desktop:make",
59
+ "example:mobile:sync": "npm --prefix examples/universal-app run mobile:sync",
60
+ "prepare": "node scripts/install-git-hooks.mjs",
61
+ "changeset": "changeset",
62
+ "changeset:version": "node scripts/changeset-version.mjs",
63
+ "changeset:publish": "changeset publish",
64
+ "changeset:status": "changeset status --since=origin/main"
65
+ },
66
+ "keywords": [
67
+ "sanitization",
68
+ "redaction",
69
+ "secrets",
70
+ "pii",
71
+ "privacy",
72
+ "logs",
73
+ "cli"
74
+ ],
75
+ "author": "",
76
+ "license": "Unlicense",
77
+ "devEngines": {
78
+ "packageManager": {
79
+ "name": "npm"
80
+ }
81
+ },
82
+ "publishConfig": {
83
+ "access": "public"
84
+ },
85
+ "repository": {
86
+ "type": "git",
87
+ "url": "git+https://github.com/link-foundation/sensitive-data-sanitizer.git"
88
+ },
89
+ "engines": {
90
+ "node": ">=22.13.0"
91
+ },
92
+ "devDependencies": {
93
+ "@changesets/cli": "^3.0.3",
94
+ "eslint": "^9.38.0",
95
+ "eslint-config-prettier": "^10.1.8",
96
+ "eslint-plugin-prettier": "^5.5.4",
97
+ "husky": "^9.1.7",
98
+ "jscpd": "^5.4.0",
99
+ "lint-staged": "^16.4.0",
100
+ "mrz": "^5.0.2",
101
+ "prettier": "^3.6.2",
102
+ "secretlint": "13.0.7",
103
+ "test-anywhere": "^0.8.48"
104
+ },
105
+ "lint-staged": {
106
+ "*.{js,mjs,cjs}": [
107
+ "eslint --fix --max-warnings 0 --no-warn-ignored",
108
+ "prettier --write",
109
+ "prettier --check"
110
+ ],
111
+ "*.md": [
112
+ "prettier --write",
113
+ "prettier --check"
114
+ ]
115
+ },
116
+ "dependencies": {
117
+ "@faker-js/faker": "^10.6.0",
118
+ "@secretlint/core": "13.0.7",
119
+ "@secretlint/profiler": "13.0.7",
120
+ "@secretlint/secretlint-rule-preset-recommend": "13.0.7",
121
+ "libphonenumber-js": "1.13.14",
122
+ "re2-wasm": "1.0.2",
123
+ "re2js": "^2.9.0",
124
+ "yaml": "^2.9.1"
125
+ }
126
+ }
@@ -0,0 +1,204 @@
1
+ import { failure, escapePattern } from './detection.js';
2
+ import { codePointRange } from './projection.js';
3
+ import { decodedRuns } from './encoded.js';
4
+ import { sanitizeFinding } from './sanitizer.js';
5
+
6
+ function occurrences(text, values, rule) {
7
+ const result = [];
8
+ for (const value of new Set(values)) {
9
+ if (typeof value !== 'string' || !value || /^\*+$/.test(value)) {
10
+ throw failure('ERR_REPORT');
11
+ }
12
+ let found = false;
13
+ for (const match of text.matchAll(new RegExp(escapePattern(value), 'g'))) {
14
+ found = true;
15
+ result.push({
16
+ start: match.index,
17
+ end: match.index + value.length,
18
+ type: 'SECRET',
19
+ category: 'credential',
20
+ rule,
21
+ });
22
+ }
23
+ // Scanners can report decoded values; mask their entire source capsule.
24
+ for (const run of decodedRuns(text)) {
25
+ if (run.text.includes(value)) {
26
+ found = true;
27
+ result.push({
28
+ start: run.start,
29
+ end: run.end,
30
+ type: 'SECRET',
31
+ category: 'credential',
32
+ rule,
33
+ });
34
+ }
35
+ }
36
+ if (!found) {
37
+ throw failure('ERR_REPORT');
38
+ }
39
+ }
40
+ return result;
41
+ }
42
+
43
+ export function fromGitleaks(text, report) {
44
+ if (!Array.isArray(report)) {
45
+ throw failure('ERR_REPORT');
46
+ }
47
+ return occurrences(
48
+ text,
49
+ report.map((entry) => entry.Secret),
50
+ 'gitleaks'
51
+ );
52
+ }
53
+
54
+ export function fromTrufflehog(text, report) {
55
+ if (!Array.isArray(report)) {
56
+ throw failure('ERR_REPORT');
57
+ }
58
+ return occurrences(text, report.flatMap(truffleValues), 'trufflehog');
59
+ }
60
+
61
+ function truffleValues(entry) {
62
+ const values = [entry?.Raw, entry?.RawV2];
63
+ if (
64
+ values.some(
65
+ (value) =>
66
+ value !== null && value !== undefined && typeof value !== 'string'
67
+ ) ||
68
+ !values.some((value) => typeof value === 'string' && value.length > 0)
69
+ ) {
70
+ throw failure('ERR_REPORT');
71
+ }
72
+ return values.filter(
73
+ (value) => typeof value === 'string' && value.length > 0
74
+ );
75
+ }
76
+
77
+ export function fromDetectSecrets(text, report, filename) {
78
+ const entries = report?.results?.[filename];
79
+ if (!Array.isArray(entries)) {
80
+ throw failure('ERR_REPORT');
81
+ }
82
+ const lines = text.split('\n');
83
+ const offsets = [0];
84
+ for (const line of lines) {
85
+ offsets.push(offsets.at(-1) + line.length + 1);
86
+ }
87
+ return entries.map((entry) => {
88
+ const index = entry.line_number - 1;
89
+ if (
90
+ !Number.isInteger(index) ||
91
+ index < 0 ||
92
+ index >= lines.length ||
93
+ !lines[index].length
94
+ ) {
95
+ throw failure('ERR_REPORT');
96
+ }
97
+ // Hashed baselines contain no literal secret; redact the complete line.
98
+ return {
99
+ start: offsets[index],
100
+ end: offsets[index] + lines[index].length,
101
+ type: 'SECRET',
102
+ category: 'credential',
103
+ rule: 'detect-secrets',
104
+ };
105
+ });
106
+ }
107
+
108
+ export function fromPresidio(text, report) {
109
+ if (!Array.isArray(report)) {
110
+ throw failure('ERR_REPORT');
111
+ }
112
+ const types = new Set([
113
+ 'PERSON',
114
+ 'ORGANIZATION',
115
+ 'EMAIL_ADDRESS',
116
+ 'PHONE_NUMBER',
117
+ 'LOCATION',
118
+ 'DATE_TIME',
119
+ 'CREDIT_CARD',
120
+ 'IP_ADDRESS',
121
+ 'IBAN_CODE',
122
+ 'US_SSN',
123
+ 'US_PASSPORT',
124
+ 'PASSWORD',
125
+ ]);
126
+ return report.map((entry) => {
127
+ if (
128
+ !types.has(entry.entity_type) &&
129
+ !/^[A-Z]{2}_[A-Z_]{1,50}$/.test(entry.entity_type)
130
+ ) {
131
+ throw failure('ERR_REPORT');
132
+ }
133
+ let range;
134
+ try {
135
+ range = codePointRange(text, entry.start, entry.end);
136
+ } catch {
137
+ throw failure('ERR_REPORT');
138
+ }
139
+ const type =
140
+ {
141
+ EMAIL_ADDRESS: 'EMAIL',
142
+ PHONE_NUMBER: 'PHONE',
143
+ LOCATION: 'ADDRESS',
144
+ PASSWORD: 'SECRET',
145
+ }[entry.entity_type] ?? entry.entity_type;
146
+ return {
147
+ ...range,
148
+ type,
149
+ category: type === 'SECRET' ? 'credential' : 'personal',
150
+ rule: 'presidio',
151
+ };
152
+ });
153
+ }
154
+
155
+ export function byteRange(text, start, end) {
156
+ if (
157
+ !Number.isInteger(start) ||
158
+ !Number.isInteger(end) ||
159
+ start < 0 ||
160
+ end <= start
161
+ ) {
162
+ throw failure('ERR_REPORT');
163
+ }
164
+ let bytes = 0,
165
+ units = 0;
166
+ const offsets = new Map([[0, 0]]);
167
+ for (const char of text) {
168
+ bytes += Buffer.byteLength(char, 'utf8');
169
+ units += char.length;
170
+ offsets.set(bytes, units);
171
+ }
172
+ if (!offsets.has(start) || !offsets.has(end)) {
173
+ throw failure('ERR_REPORT');
174
+ }
175
+ return { start: offsets.get(start), end: offsets.get(end) };
176
+ }
177
+
178
+ /** Adapt a trusted detector's offset report without retaining quotes or scores. */
179
+ export function fromOffsetReport(text, entries, { unit, rule } = {}) {
180
+ if (
181
+ !Array.isArray(entries) ||
182
+ !['utf16', 'codepoint', 'byte'].includes(unit)
183
+ ) {
184
+ throw failure('ERR_REPORT');
185
+ }
186
+ return entries.map((entry) => {
187
+ let range;
188
+ try {
189
+ if (unit === 'byte') {
190
+ range = byteRange(text, entry.start, entry.end);
191
+ } else if (unit === 'codepoint') {
192
+ range = codePointRange(text, entry.start, entry.end);
193
+ } else {
194
+ range = { start: entry.start, end: entry.end };
195
+ }
196
+ return sanitizeFinding(
197
+ { ...range, category: entry.category, type: entry.type, rule },
198
+ text.length
199
+ );
200
+ } catch {
201
+ throw failure('ERR_REPORT');
202
+ }
203
+ });
204
+ }
@@ -0,0 +1,11 @@
1
+ /** Browser-compatible utilities retained from the original template. */
2
+
3
+ /** Add two numbers. */
4
+ export const add = (a, b) => a + b;
5
+
6
+ /** Multiply two numbers. */
7
+ export const multiply = (a, b) => a * b;
8
+
9
+ /** Resolve after the requested number of milliseconds. */
10
+ export const delay = (ms) =>
11
+ new Promise((resolve) => globalThis.setTimeout(resolve, ms));
@@ -0,0 +1,65 @@
1
+ // A valid check digit is common in arbitrary log numbers. Context decides
2
+ // whether that evidence merits a replacement; inspect retains low scores.
3
+ export function threshold(options) {
4
+ return options.minConfidence ?? options.threshold ?? 0.5;
5
+ }
6
+ export function actionable(finding, options) {
7
+ return (
8
+ finding.kept !== 'fake' &&
9
+ (finding.category === 'credential' ||
10
+ finding.confidence >= threshold(options))
11
+ );
12
+ }
13
+ export function numericNoise(text, start) {
14
+ const before = text
15
+ .slice(Math.max(0, start - 80), start)
16
+ .split(/[\r\n|;]/)
17
+ .at(-1);
18
+ return /(?<![\p{L}\p{N}])(?:timestamp|ts|time|created_at|updated_at|elapsed|duration|ms|epoch|order|run|size|bytes|port|pid|line|col)["']?[ :=_-]*$/iu.test(
19
+ before
20
+ );
21
+ }
22
+ export function epochNumber(value) {
23
+ const digits = value.replace(/\D/g, '');
24
+ const seconds = digits.length === 13 ? Number(digits) / 1000 : Number(digits);
25
+ return (
26
+ [10, 13].includes(digits.length) &&
27
+ seconds >= 946684800 &&
28
+ seconds < 4102444800
29
+ );
30
+ }
31
+ export function cardEvidence(text, start, value) {
32
+ const identityContext =
33
+ /(?<![\p{L}\p{N}_])(?:card|visa|mastercard|amex|discover|jcb|unionpay|mir|diners|карта)(?![\p{L}\p{N}_])/iu.test(
34
+ text.slice(Math.max(0, start - 64), start + value.length + 32)
35
+ );
36
+ return identityContext || (!numericNoise(text, start) && !epochNumber(value));
37
+ }
38
+ export function cardNetwork(value) {
39
+ const d = value.replace(/\D/g, ''),
40
+ n = d.length;
41
+ return primaryCard(d, n) || otherCard(d, n);
42
+ }
43
+ function primaryCard(d, n) {
44
+ return (
45
+ (/^4/.test(d) && [13, 16, 19].includes(n)) ||
46
+ ((/^5[1-5]/.test(d) ||
47
+ (Number(d.slice(0, 4)) >= 2221 && Number(d.slice(0, 4)) <= 2720)) &&
48
+ n === 16) ||
49
+ (/^3[47]/.test(d) && n === 15) ||
50
+ ((/^6011|^65|^64[4-9]/.test(d) ||
51
+ (Number(d.slice(0, 6)) >= 622126 && Number(d.slice(0, 6)) <= 622925)) &&
52
+ [16, 19].includes(n))
53
+ );
54
+ }
55
+ function otherCard(d, n) {
56
+ return (
57
+ (Number(d.slice(0, 4)) >= 3528 &&
58
+ Number(d.slice(0, 4)) <= 3589 &&
59
+ n >= 16 &&
60
+ n <= 19) ||
61
+ (/^62/.test(d) && n >= 16 && n <= 19) ||
62
+ (/^220[0-4]/.test(d) && n >= 16 && n <= 19) ||
63
+ (/^3(?:0[0-5]|[68])/.test(d) && n === 14)
64
+ );
65
+ }