@link-foundation/sensitive-data-sanitizer 0.0.0-stage → 0.12.0
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/CHANGELOG.md +578 -0
- package/LICENSE +24 -0
- package/README.md +184 -2
- package/bin/legacy-example-package-name.js +95 -0
- package/bin/sensitive-data-sanitizer.js +430 -0
- package/bin/stream.js +56 -0
- package/docs/API.md +336 -0
- package/docs/COVERAGE.md +136 -0
- package/docs/HISTORY-REMEDIATION.md +41 -0
- package/examples/combined-scanners.mjs +23 -0
- package/examples/presidio-bridge.py +32 -0
- package/examples/realistic-fakes.mjs +18 -0
- package/examples/sanitize-session.mjs +27 -0
- package/examples/shared-tool-logs.mjs +21 -0
- package/examples/stream-jsonl.mjs +17 -0
- package/package.json +124 -4
- package/src/adapters.js +204 -0
- package/src/arithmetic.js +11 -0
- package/src/confidence.js +65 -0
- package/src/context.js +424 -0
- package/src/credentials.js +54 -0
- package/src/detection.js +293 -0
- package/src/encoded.js +233 -0
- package/src/engines.js +348 -0
- package/src/entities.js +166 -0
- package/src/escape-spans.js +20 -0
- package/src/eslint-plugin.d.ts +10 -0
- package/src/eslint-plugin.js +193 -0
- package/src/external.js +169 -0
- package/src/fake-names.js +222 -0
- package/src/fake.js +524 -0
- package/src/files.js +131 -0
- package/src/gitleaks.js +194 -0
- package/src/history-rewrite.js +339 -0
- package/src/history.js +103 -0
- package/src/identity.js +274 -0
- package/src/index.d.ts +321 -0
- package/src/index.js +40 -0
- package/src/jsonl-stream.js +198 -0
- package/src/keywords.js +45 -0
- package/src/known.js +164 -0
- package/src/legacy.d.ts +3 -0
- package/src/legacy.js +2 -0
- package/src/locales.js +217 -0
- package/src/log-bytes.js +85 -0
- package/src/names.js +65 -0
- package/src/national-checks.js +129 -0
- package/src/national.js +128 -0
- package/src/outbound.js +28 -0
- package/src/personal-context.js +85 -0
- package/src/personal.js +248 -0
- package/src/projection.js +126 -0
- package/src/propagation.js +163 -0
- package/src/public-verifier.js +77 -0
- package/src/public.js +153 -0
- package/src/record-worker.js +20 -0
- package/src/rules.js +210 -0
- package/src/sanitizer.js +794 -0
- package/src/specimens.js +34 -0
- package/src/stream-identity.js +129 -0
- package/src/stream-worker.js +18 -0
- package/src/stream.js +449 -0
- package/src/structured.js +278 -0
- package/src/transforms.js +192 -0
- package/src/transliteration.js +75 -0
- package/src/utf8.js +60 -0
- package/src/vendor/entities/aws.json +42 -0
- package/src/vendor/entities/azure.json +182 -0
- package/src/vendor/entities/google.json +219 -0
- package/src/vendor/gitleaks/LICENSE +21 -0
- package/src/vendor/gitleaks/provenance.json +8 -0
- package/src/vendor/gitleaks/rules-0.json +268 -0
- package/src/vendor/gitleaks/rules-1.json +1794 -0
- package/src/vendor/gitleaks/rules-2.json +292 -0
- package/src/vendor/gitleaks/rules-3.json +263 -0
- package/src/vendor/gitleaks/rules-4.json +277 -0
- package/src/vendor/gitleaks/rules-5.json +154 -0
- package/src/worker-options.js +27 -0
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync, realpathSync } from 'node:fs';
|
|
3
|
+
import { fileURLToPath, URL } from 'node:url';
|
|
4
|
+
import { resolve } from 'node:path';
|
|
5
|
+
import { createSanitizer } from '../src/engines.js';
|
|
6
|
+
import { failure } from '../src/detection.js';
|
|
7
|
+
import {
|
|
8
|
+
readTextFile,
|
|
9
|
+
atomicWrite,
|
|
10
|
+
listTextCandidates,
|
|
11
|
+
decodeText,
|
|
12
|
+
} from '../src/files.js';
|
|
13
|
+
import { auditGitHistory } from '../src/history.js';
|
|
14
|
+
import { rewriteGitHistory } from '../src/history-rewrite.js';
|
|
15
|
+
import {
|
|
16
|
+
knownSecretsFromGitHubAuth,
|
|
17
|
+
knownSecretsFromEnv,
|
|
18
|
+
knownSecretsFromGitHubConfig,
|
|
19
|
+
} from '../src/known.js';
|
|
20
|
+
import { encodeLogText } from '../src/log-bytes.js';
|
|
21
|
+
import { createWikidataVerifier } from '../src/public-verifier.js';
|
|
22
|
+
import { streamCommand } from './stream.js';
|
|
23
|
+
import { sessionStructuralFields } from '../src/structured.js';
|
|
24
|
+
import {
|
|
25
|
+
createGitleaksDetector,
|
|
26
|
+
createTrufflehogDetector,
|
|
27
|
+
createPresidioDetector,
|
|
28
|
+
} from '../src/external.js';
|
|
29
|
+
|
|
30
|
+
const help = `Usage: sensitive-data-sanitizer <redact|scan|history> [path|-] [options]
|
|
31
|
+
|
|
32
|
+
redact Sanitize one text log or stdin; publish only after verification
|
|
33
|
+
scan Report metadata for a file, directory, or stdin (never source text)
|
|
34
|
+
history Audit local reachable Git blobs, commit/tag metadata, and reflogs
|
|
35
|
+
history rewrite SOURCE --output DIRECTORY [--apply]
|
|
36
|
+
Preview a fresh mirror clone; --apply rewrites only that clone
|
|
37
|
+
|
|
38
|
+
--config PATH JSON knownSecrets, knownPersonal, publicEntities, limits
|
|
39
|
+
--output PATH Write a new private output file atomically (redact only)
|
|
40
|
+
--in-place Atomically replace the input file (redact only)
|
|
41
|
+
--native-only Disable Secretlint; selected local detectors remain required
|
|
42
|
+
--credentials-only Preserve personal/account details; redact credentials only
|
|
43
|
+
--env-secrets Include values of all credential-named environment variables
|
|
44
|
+
--gh-config Include tokens from the local gh hosts.yml credential file
|
|
45
|
+
--paranoid Add entropy detection (can redact innocent identifiers)
|
|
46
|
+
--gitleaks Require locally installed Gitleaks in the detector union
|
|
47
|
+
--trufflehog Require local TruffleHog with verification/updates disabled
|
|
48
|
+
--presidio PATH Require a local Presidio JSON bridge (see examples)
|
|
49
|
+
--python COMMAND Python executable for the bridge (default python3)
|
|
50
|
+
--model NAME Installed model passed to the selected bridge
|
|
51
|
+
--language CODE Model language passed to the selected bridge
|
|
52
|
+
--stream Process text records; files use a bounded worker
|
|
53
|
+
--two-pass Collect identities first, then publish the complete stream
|
|
54
|
+
--json Sanitize JSON strings and keys; preserve session metadata
|
|
55
|
+
--jsonl Sanitize JSONL records; combine with --stream for sessions
|
|
56
|
+
--identity-mask First/last 2 identity mask (publication profile default)
|
|
57
|
+
--profile NAME Publication profile (redact default; config can override)
|
|
58
|
+
--max-record-bytes N Maximum record bytes (text: total limit; JSONL: 8388608)
|
|
59
|
+
--hive-mask Opt-in first/last 3 mask for values longer than 12 characters
|
|
60
|
+
--preserve-encoding Re-encode sanitized encoded payloads with round-trip checks
|
|
61
|
+
--gh-auth Read local gh auth token and mask its exact value
|
|
62
|
+
--verify-public Opt-in Wikidata name lookup (sends candidate public names)
|
|
63
|
+
--apply Apply history rewrite to the fresh clone, then audit it
|
|
64
|
+
--filter-repo PATH git-filter-repo executable for history rewrite
|
|
65
|
+
--max-bytes NUMBER Input byte limit (default 33554432; stream: 1073741824)
|
|
66
|
+
--help Show help
|
|
67
|
+
--version Show package version
|
|
68
|
+
|
|
69
|
+
Exit codes: 0 successful redact / clean scan; 1 findings; 2 error/incomplete.
|
|
70
|
+
Directory scans omit .git and node_modules. Binary files are reported as
|
|
71
|
+
skipped and produce exit 2. History operations never push.`;
|
|
72
|
+
|
|
73
|
+
function parseArgs(argv) {
|
|
74
|
+
const command = argv[0];
|
|
75
|
+
const rewrite = command === 'history' && argv[1] === 'rewrite';
|
|
76
|
+
const config = { command, rewrite, path: '-' };
|
|
77
|
+
const boolean = new Map([
|
|
78
|
+
['--in-place', 'inPlace'],
|
|
79
|
+
['--native-only', 'nativeOnly'],
|
|
80
|
+
['--credentials-only', 'credentialsOnly'],
|
|
81
|
+
['--env-secrets', 'envSecrets'],
|
|
82
|
+
['--gh-config', 'ghConfig'],
|
|
83
|
+
['--paranoid', 'paranoid'],
|
|
84
|
+
['--gitleaks', 'gitleaks'],
|
|
85
|
+
['--trufflehog', 'trufflehog'],
|
|
86
|
+
['--stream', 'stream'],
|
|
87
|
+
['--two-pass', 'twoPass'],
|
|
88
|
+
['--json', 'json'],
|
|
89
|
+
['--jsonl', 'jsonl'],
|
|
90
|
+
['--identity-mask', 'identityMask'],
|
|
91
|
+
['--hive-mask', 'hiveMask'],
|
|
92
|
+
['--preserve-encoding', 'preserveEncoding'],
|
|
93
|
+
['--gh-auth', 'ghAuth'],
|
|
94
|
+
['--verify-public', 'verifyPublic'],
|
|
95
|
+
['--apply', 'apply'],
|
|
96
|
+
]);
|
|
97
|
+
const valued = new Map([
|
|
98
|
+
['--config', 'config'],
|
|
99
|
+
['--output', 'output'],
|
|
100
|
+
['--max-bytes', 'maxBytes'],
|
|
101
|
+
['--presidio', 'presidio'],
|
|
102
|
+
['--python', 'python'],
|
|
103
|
+
['--model', 'model'],
|
|
104
|
+
['--language', 'language'],
|
|
105
|
+
['--max-record-bytes', 'maxRecordBytes'],
|
|
106
|
+
['--filter-repo', 'filterRepo'],
|
|
107
|
+
['--profile', 'profile'],
|
|
108
|
+
]);
|
|
109
|
+
let hasPath = false;
|
|
110
|
+
for (let i = rewrite ? 2 : 1; i < argv.length; i++) {
|
|
111
|
+
const arg = argv[i];
|
|
112
|
+
if (boolean.has(arg)) {
|
|
113
|
+
config[boolean.get(arg)] = true;
|
|
114
|
+
} else if (valued.has(arg)) {
|
|
115
|
+
if (!argv[i + 1] || argv[i + 1].startsWith('--')) {
|
|
116
|
+
throw failure('ERR_ARGUMENT');
|
|
117
|
+
}
|
|
118
|
+
config[valued.get(arg)] = argv[++i];
|
|
119
|
+
} else if (!hasPath && !arg.startsWith('--')) {
|
|
120
|
+
config.path = arg;
|
|
121
|
+
hasPath = true;
|
|
122
|
+
} else {
|
|
123
|
+
throw failure('ERR_ARGUMENT');
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
validateArgs(config);
|
|
127
|
+
return config;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function validateArgs(config) {
|
|
131
|
+
config.maxBytes = Number(config.maxBytes ?? defaultByteLimit(config));
|
|
132
|
+
if (
|
|
133
|
+
!['redact', 'scan', 'history'].includes(config.command) ||
|
|
134
|
+
!Number.isSafeInteger(config.maxBytes) ||
|
|
135
|
+
config.maxBytes <= 0
|
|
136
|
+
) {
|
|
137
|
+
throw failure('ERR_ARGUMENT');
|
|
138
|
+
}
|
|
139
|
+
if (
|
|
140
|
+
(config.output && config.inPlace) ||
|
|
141
|
+
((config.output || config.inPlace) &&
|
|
142
|
+
config.command !== 'redact' &&
|
|
143
|
+
!config.rewrite) ||
|
|
144
|
+
(config.inPlace && config.path === '-') ||
|
|
145
|
+
(config.command === 'history' && config.path === '-')
|
|
146
|
+
) {
|
|
147
|
+
throw failure('ERR_ARGUMENT');
|
|
148
|
+
}
|
|
149
|
+
validateFeatureArgs(config);
|
|
150
|
+
validateOutput(config);
|
|
151
|
+
validateBridge(config);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function validateFeatureArgs(config) {
|
|
155
|
+
config.maxRecordBytes = Number(
|
|
156
|
+
config.maxRecordBytes ?? (config.jsonl ? 8388608 : config.maxBytes)
|
|
157
|
+
);
|
|
158
|
+
if (
|
|
159
|
+
!Number.isSafeInteger(config.maxRecordBytes) ||
|
|
160
|
+
config.maxRecordBytes <= 0 ||
|
|
161
|
+
(config.stream && config.command !== 'redact') ||
|
|
162
|
+
((config.apply || config.filterRepo) && !config.rewrite) ||
|
|
163
|
+
(config.rewrite &&
|
|
164
|
+
(!config.output || config.inPlace || config.path === '-'))
|
|
165
|
+
) {
|
|
166
|
+
throw failure('ERR_ARGUMENT');
|
|
167
|
+
}
|
|
168
|
+
validateStructuredArgs(config);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function validateStructuredArgs(config) {
|
|
172
|
+
if (config.twoPass && !config.stream) {
|
|
173
|
+
throw failure('ERR_ARGUMENT');
|
|
174
|
+
}
|
|
175
|
+
if ((config.json && config.jsonl) || (config.stream && config.json)) {
|
|
176
|
+
throw failure('ERR_ARGUMENT');
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function validateBridge(config) {
|
|
181
|
+
if (!config.presidio && (config.python || config.model || config.language)) {
|
|
182
|
+
throw failure('ERR_ARGUMENT');
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function validateOutput(config) {
|
|
187
|
+
if (
|
|
188
|
+
config.output &&
|
|
189
|
+
config.path !== '-' &&
|
|
190
|
+
resolve(config.output) === resolve(config.path)
|
|
191
|
+
) {
|
|
192
|
+
throw failure('ERR_OUTPUT');
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function stdinText(stream, maxBytes, options) {
|
|
197
|
+
const chunks = [];
|
|
198
|
+
let size = 0;
|
|
199
|
+
for await (const chunk of stream) {
|
|
200
|
+
const buffer = Buffer.from(chunk);
|
|
201
|
+
size += buffer.length;
|
|
202
|
+
if (size > maxBytes) {
|
|
203
|
+
throw failure('ERR_LIMIT');
|
|
204
|
+
}
|
|
205
|
+
chunks.push(buffer);
|
|
206
|
+
}
|
|
207
|
+
return decodeText(Buffer.concat(chunks), options);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export async function runCli(
|
|
211
|
+
argv,
|
|
212
|
+
{
|
|
213
|
+
stdin = process.stdin,
|
|
214
|
+
stdout = process.stdout,
|
|
215
|
+
stderr = process.stderr,
|
|
216
|
+
} = {}
|
|
217
|
+
) {
|
|
218
|
+
try {
|
|
219
|
+
if (!argv.length || ['--help', '-h'].includes(argv[0])) {
|
|
220
|
+
stdout.write(`${help}\n`);
|
|
221
|
+
return 0;
|
|
222
|
+
}
|
|
223
|
+
if (['--version', '-v'].includes(argv[0])) {
|
|
224
|
+
stdout.write(
|
|
225
|
+
`${JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version}\n`
|
|
226
|
+
);
|
|
227
|
+
return 0;
|
|
228
|
+
}
|
|
229
|
+
const config = parseArgs(argv);
|
|
230
|
+
const options = await loadOptions(config);
|
|
231
|
+
config.lossless = !options.structured;
|
|
232
|
+
const engine = createSanitizer(options);
|
|
233
|
+
const context = { stdin, stdout };
|
|
234
|
+
if (config.command === 'history') {
|
|
235
|
+
return await historyCommand(config, engine, context);
|
|
236
|
+
}
|
|
237
|
+
if (config.stream) {
|
|
238
|
+
return await streamCommand(config, engine, options, context);
|
|
239
|
+
}
|
|
240
|
+
if (config.command === 'redact') {
|
|
241
|
+
return await redactCommand(config, engine, context);
|
|
242
|
+
}
|
|
243
|
+
return await scanCommand(config, engine, context);
|
|
244
|
+
} catch (error) {
|
|
245
|
+
const location =
|
|
246
|
+
Number.isSafeInteger(error.record) && Number.isSafeInteger(error.offset)
|
|
247
|
+
? `; record ${error.record}, byte ${error.offset}`
|
|
248
|
+
: '';
|
|
249
|
+
stderr.write(
|
|
250
|
+
`Sanitization failed; output was blocked (${/^ERR_[A-Z_]+$/.test(error.code) ? error.code : 'ERR_SANITIZATION'}${location}).\n`
|
|
251
|
+
);
|
|
252
|
+
return 2;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function loadOptions(config) {
|
|
257
|
+
let options = {};
|
|
258
|
+
if (config.config) {
|
|
259
|
+
try {
|
|
260
|
+
options = JSON.parse(await readTextFile(config.config, 1024 * 1024));
|
|
261
|
+
} catch {
|
|
262
|
+
throw failure('ERR_CONFIG');
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (!options || typeof options !== 'object' || Array.isArray(options)) {
|
|
266
|
+
throw failure('ERR_CONFIG');
|
|
267
|
+
}
|
|
268
|
+
options.profile =
|
|
269
|
+
config.profile ??
|
|
270
|
+
options.profile ??
|
|
271
|
+
(config.command === 'redact' ? 'publication' : undefined);
|
|
272
|
+
if (config.nativeOnly) {
|
|
273
|
+
options.secretlint = false;
|
|
274
|
+
}
|
|
275
|
+
if (config.paranoid) {
|
|
276
|
+
options.paranoid = true;
|
|
277
|
+
}
|
|
278
|
+
await featureOptions(config, options);
|
|
279
|
+
options.detectors = selectedDetectors(config, options.detectors);
|
|
280
|
+
options.maxInputLength = Math.min(
|
|
281
|
+
options.maxInputLength ?? config.maxBytes,
|
|
282
|
+
config.maxBytes
|
|
283
|
+
);
|
|
284
|
+
return options;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async function featureOptions(config, options) {
|
|
288
|
+
await localCredentials(config, options);
|
|
289
|
+
if (config.credentialsOnly) {
|
|
290
|
+
options.credentialsOnly = true;
|
|
291
|
+
}
|
|
292
|
+
if (config.json || config.jsonl) {
|
|
293
|
+
options.structured = config.jsonl ? 'jsonl' : 'json';
|
|
294
|
+
options.structuralFields ??= sessionStructuralFields;
|
|
295
|
+
}
|
|
296
|
+
if (config.identityMask) {
|
|
297
|
+
options.identityMask = true;
|
|
298
|
+
}
|
|
299
|
+
if (config.hiveMask) {
|
|
300
|
+
options.transformation = { mode: 'hive-mask' };
|
|
301
|
+
}
|
|
302
|
+
if (config.preserveEncoding) {
|
|
303
|
+
options.preserveEncoding = true;
|
|
304
|
+
}
|
|
305
|
+
if (config.verifyPublic) {
|
|
306
|
+
options.verifyPublic = createWikidataVerifier();
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async function localCredentials(config, options) {
|
|
311
|
+
for (const [flag, load] of [
|
|
312
|
+
['envSecrets', knownSecretsFromEnv],
|
|
313
|
+
['ghConfig', knownSecretsFromGitHubConfig],
|
|
314
|
+
['ghAuth', knownSecretsFromGitHubAuth],
|
|
315
|
+
]) {
|
|
316
|
+
if (config[flag]) {
|
|
317
|
+
options.knownSecrets = [
|
|
318
|
+
...(options.knownSecrets ?? []),
|
|
319
|
+
...(await load()),
|
|
320
|
+
];
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function selectedDetectors(config, detectors = []) {
|
|
326
|
+
const selected = [...detectors];
|
|
327
|
+
for (const [flag, factory] of [
|
|
328
|
+
['gitleaks', createGitleaksDetector],
|
|
329
|
+
['trufflehog', createTrufflehogDetector],
|
|
330
|
+
]) {
|
|
331
|
+
if (config[flag]) {
|
|
332
|
+
selected.push(factory());
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
if (config.presidio) {
|
|
336
|
+
const args = [resolve(config.presidio)];
|
|
337
|
+
for (const name of ['model', 'language']) {
|
|
338
|
+
if (config[name]) {
|
|
339
|
+
args.push(`--${name}`, config[name]);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
selected.push(
|
|
343
|
+
createPresidioDetector({
|
|
344
|
+
command: config.python ?? 'python3',
|
|
345
|
+
args,
|
|
346
|
+
})
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
return selected;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function inputText(path, config, stdin) {
|
|
353
|
+
const options = { lossless: config.lossless };
|
|
354
|
+
return path === '-'
|
|
355
|
+
? stdinText(stdin, config.maxBytes, options)
|
|
356
|
+
: readTextFile(path, config.maxBytes, options);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async function redactCommand(config, engine, { stdin, stdout }) {
|
|
360
|
+
const result = await engine.sanitize(
|
|
361
|
+
await inputText(config.path, config, stdin)
|
|
362
|
+
);
|
|
363
|
+
if (config.output || config.inPlace) {
|
|
364
|
+
await atomicWrite(
|
|
365
|
+
config.output ?? config.path,
|
|
366
|
+
encodeLogText(result.text),
|
|
367
|
+
{
|
|
368
|
+
replace: Boolean(config.inPlace),
|
|
369
|
+
}
|
|
370
|
+
);
|
|
371
|
+
} else {
|
|
372
|
+
stdout.write(encodeLogText(result.text));
|
|
373
|
+
}
|
|
374
|
+
return 0;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
async function historyCommand(config, engine, { stdout }) {
|
|
378
|
+
if (config.rewrite) {
|
|
379
|
+
const result = await rewriteGitHistory(config.path, config.output, {
|
|
380
|
+
sanitizer: engine,
|
|
381
|
+
apply: Boolean(config.apply),
|
|
382
|
+
filterRepoCommand: config.filterRepo ?? 'git-filter-repo',
|
|
383
|
+
maxBytes: config.maxBytes,
|
|
384
|
+
});
|
|
385
|
+
stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
386
|
+
return 0;
|
|
387
|
+
}
|
|
388
|
+
const result = await auditGitHistory(config.path, {
|
|
389
|
+
sanitizer: engine,
|
|
390
|
+
maxBytes: config.maxBytes,
|
|
391
|
+
});
|
|
392
|
+
stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
393
|
+
return !result.complete ? 2 : result.findings.length ? 1 : 0;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async function scanCommand(config, engine, { stdin, stdout }) {
|
|
397
|
+
const files =
|
|
398
|
+
config.path === '-' ? ['-'] : await listTextCandidates(config.path);
|
|
399
|
+
const results = [],
|
|
400
|
+
skipped = [];
|
|
401
|
+
for (const file of files) {
|
|
402
|
+
const path = (await engine.sanitize(file)).text;
|
|
403
|
+
try {
|
|
404
|
+
results.push({
|
|
405
|
+
path,
|
|
406
|
+
findings: await engine.inspect(await inputText(file, config, stdin)),
|
|
407
|
+
});
|
|
408
|
+
} catch (error) {
|
|
409
|
+
if (['ERR_BINARY', 'ERR_ENCODING', 'ERR_LIMIT'].includes(error.code)) {
|
|
410
|
+
skipped.push({ path, reason: error.code });
|
|
411
|
+
} else {
|
|
412
|
+
throw error;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
const complete = skipped.length === 0;
|
|
417
|
+
stdout.write(`${JSON.stringify({ complete, results, skipped }, null, 2)}\n`);
|
|
418
|
+
return !complete ? 2 : results.some((r) => r.findings.length) ? 1 : 0;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (
|
|
422
|
+
process.argv[1] &&
|
|
423
|
+
realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))
|
|
424
|
+
) {
|
|
425
|
+
process.exitCode = await runCli(process.argv.slice(2));
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function defaultByteLimit(config) {
|
|
429
|
+
return config.stream ? 1073741824 : 33554432;
|
|
430
|
+
}
|
package/bin/stream.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
2
|
+
import { createReadStream } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { once } from 'node:events';
|
|
6
|
+
import { sanitizeFileBounded, sanitizeStreamToFile } from '../src/stream.js';
|
|
7
|
+
|
|
8
|
+
export async function streamCommand(
|
|
9
|
+
config,
|
|
10
|
+
engine,
|
|
11
|
+
options,
|
|
12
|
+
{ stdin, stdout }
|
|
13
|
+
) {
|
|
14
|
+
let spool;
|
|
15
|
+
try {
|
|
16
|
+
const destination =
|
|
17
|
+
config.output ?? (config.inPlace ? config.path : undefined);
|
|
18
|
+
if (!destination) {
|
|
19
|
+
spool = await mkdtemp(join(tmpdir(), 'sanitizer-cli-'));
|
|
20
|
+
}
|
|
21
|
+
const target = destination ?? join(spool, 'output');
|
|
22
|
+
const limits = {
|
|
23
|
+
maxRecordBytes: config.maxRecordBytes,
|
|
24
|
+
maxTotalBytes: config.maxBytes,
|
|
25
|
+
replace: Boolean(config.inPlace),
|
|
26
|
+
twoPass: Boolean(config.twoPass),
|
|
27
|
+
};
|
|
28
|
+
if (config.path === '-') {
|
|
29
|
+
await sanitizeStreamToFile(stdin, target, {
|
|
30
|
+
...limits,
|
|
31
|
+
sanitizer: engine,
|
|
32
|
+
sanitizerOptions: options,
|
|
33
|
+
});
|
|
34
|
+
} else {
|
|
35
|
+
const serializable = !options.detectors?.length && !options.verifyPublic;
|
|
36
|
+
await sanitizeFileBounded(config.path, target, {
|
|
37
|
+
...limits,
|
|
38
|
+
...(serializable
|
|
39
|
+
? { sanitizerOptions: options }
|
|
40
|
+
: { sanitizer: engine, sanitizerOptions: options }),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
if (spool) {
|
|
44
|
+
for await (const chunk of createReadStream(target)) {
|
|
45
|
+
if (stdout.write(chunk) === false) {
|
|
46
|
+
await once(stdout, 'drain');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return 0;
|
|
51
|
+
} finally {
|
|
52
|
+
if (spool) {
|
|
53
|
+
await rm(spool, { recursive: true, force: true });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|