logshield-cli 0.3.1 → 0.3.3

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 ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2025 Hendra Afria
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md CHANGED
@@ -1,12 +1,111 @@
1
1
  ---
2
+
2
3
  # LogShield
3
4
 
4
- LogShield is a CLI tool to **redact sensitive data from logs** before sharing them with others, AI tools, or public channels.
5
+ [![npm version](https://img.shields.io/npm/v/logshield-cli)](https://www.npmjs.com/package/logshield-cli)
6
+ [![npm downloads](https://img.shields.io/npm/dm/logshield-cli)](https://www.npmjs.com/package/logshield-cli)
7
+ [![CI](https://github.com/your-org/logshield/actions/workflows/ci.yml/badge.svg)](https://github.com/your-org/logshield/actions)
8
+
9
+ Deterministic log sanitization for developers.
10
+
11
+ LogShield is a CLI tool that scans logs and redacts **real secrets** (API keys, tokens, credentials) before logs are shared with others, AI tools, CI systems, or public channels.
12
+
13
+ It is designed to be **predictable, conservative, and safe for production pipelines**.
14
+
15
+ ---
16
+
17
+ ## Website & Documentation
18
+
19
+ The website and documentation live in the `/docs` directory.
20
+ They are deployed to **[https://logshield.dev](https://logshield.dev)** via Vercel.
21
+
22
+ ---
23
+
24
+ ## Why LogShield exists
25
+
26
+ Logs are frequently copied into:
27
+
28
+ - CI/CD logs
29
+ - Error reports
30
+ - Issue trackers
31
+ - Chat tools
32
+ - AI assistants
33
+
34
+ Once a secret appears there, it is already leaked.
35
+
36
+ Most existing tools fail because they:
37
+
38
+ - Redact too aggressively (false positives)
39
+ - Behave differently across runs
40
+ - Hide what was redacted and why
41
+
42
+ LogShield intentionally avoids those failures.
43
+
44
+ ---
45
+
46
+ ## Core principles (non-negotiable)
47
+
48
+ ### 1. Deterministic output
49
+
50
+ The same input always produces the same output.
51
+
52
+ - No randomness
53
+ - No environment-dependent behavior
54
+ - Safe for CI, audits, and reproducibility
55
+
56
+ ---
57
+
58
+ ### 2. Zero false-positive fatality
59
+
60
+ LogShield must **not** redact non-secrets.
61
+
62
+ - IDs, hashes, order numbers, references must survive
63
+ - Losing debugging context is worse than missing a secret
64
+
65
+ When in doubt, LogShield prefers **not** to redact.
5
66
 
6
- Designed to be safe by default, deterministic, and free of runtime dependencies.
7
67
  ---
8
68
 
9
- ## Install
69
+ ### 3. Explicit redaction markers
70
+
71
+ Every redaction is explicit and consistent.
72
+
73
+ Examples:
74
+
75
+ ```
76
+ <REDACTED_PASSWORD>
77
+ <REDACTED_API_KEY_HEADER>
78
+ <REDACTED_AUTH_BEARER>
79
+ <REDACTED_EMAIL>
80
+ ```
81
+
82
+ No generic `[REDACTED]` placeholders.
83
+
84
+ ---
85
+
86
+ ## What LogShield does
87
+
88
+ - Scans plain-text logs
89
+ - Applies a fixed, deterministic rule set
90
+ - Replaces matched secrets with explicit markers
91
+
92
+ It does **not** learn, guess, or infer intent.
93
+
94
+ ---
95
+
96
+ ## What LogShield deliberately does NOT do
97
+
98
+ - No AI / LLM inference
99
+ - No entropy or probabilistic guessing
100
+ - No silent behavior changes
101
+ - No telemetry
102
+ - No network calls
103
+
104
+ These are intentional design decisions.
105
+
106
+ ---
107
+
108
+ ## Installation
10
109
 
11
110
  ```bash
12
111
  npm install -g logshield-cli
@@ -14,61 +113,227 @@ npm install -g logshield-cli
14
113
 
15
114
  ---
16
115
 
17
- ## Usage
116
+ ## CLI Usage
18
117
 
19
- Scan a log file:
118
+ ```bash
119
+ logshield scan [file]
120
+ ```
121
+
122
+ If a file is not provided and input is piped, LogShield automatically reads from **STDIN**.
123
+
124
+ ---
125
+
126
+ ## CLI Flags
127
+
128
+ - `--strict`
129
+ Aggressive, security-first redaction
130
+
131
+ - `--stdin`
132
+ Explicitly force reading from STDIN
133
+
134
+ - `--dry-run`
135
+ Detect sensitive data without modifying output
136
+
137
+ - `--fail-on-detect`
138
+ Exit with code `1` if any redaction is detected (CI-friendly)
139
+
140
+ - `--summary`
141
+ Print a compact redaction summary
142
+
143
+ - `--json`
144
+ JSON output (cannot be combined with `--dry-run`)
145
+
146
+ - `--version`
147
+ Print CLI version
148
+
149
+ - `--help`
150
+ Show help
151
+
152
+ ---
153
+
154
+ ## Basic usage
155
+
156
+ ### Scan a file
20
157
 
21
158
  ```bash
22
159
  logshield scan app.log
23
160
  ```
24
161
 
25
- Scan from stdin:
162
+ ### Scan from STDIN (recommended for CI)
26
163
 
27
164
  ```bash
28
165
  cat app.log | logshield scan
29
166
  ```
30
167
 
31
- Strict mode (more aggressive):
168
+ `--stdin` is optional; piped input is auto-detected.
169
+
170
+ ---
171
+
172
+ ## Dry-run (REPORT MODE, CI-safe)
173
+
174
+ Use `--dry-run` to **detect** sensitive data without modifying output.
175
+
176
+ ```bash
177
+ cat app.log | logshield scan --dry-run
178
+ ```
179
+
180
+ ### Output
181
+
182
+ ```
183
+ [DRY RUN] Detected redactions:
184
+ OAUTH_ACCESS_TOKEN x1
185
+ AUTH_BEARER x2
186
+ EMAIL x1
187
+ PASSWORD x1
188
+
189
+ No output was modified.
190
+ Use without --dry-run to apply.
191
+ ```
192
+
193
+ ### Properties
194
+
195
+ - No log content is echoed
196
+ - Deterministic and snapshot-friendly
197
+ - Safe for CI pipelines
198
+
199
+ ---
200
+
201
+ ## Fail CI on detection
202
+
203
+ Use `--fail-on-detect` to exit with code `1` when secrets are found.
204
+
205
+ ```bash
206
+ cat app.log | logshield scan --dry-run --fail-on-detect
207
+ ```
208
+
209
+ Typical CI pattern:
32
210
 
33
211
  ```bash
34
- logshield scan app.log --strict
212
+ logshield scan --dry-run --fail-on-detect < logs.txt
35
213
  ```
36
214
 
37
- JSON output:
215
+ ---
216
+
217
+ ## GitHub Actions (example)
218
+
219
+ Minimal CI integration example:
220
+
221
+ ```yaml
222
+ name: LogShield
223
+
224
+ on:
225
+ push:
226
+ pull_request:
227
+
228
+ jobs:
229
+ logshield:
230
+ runs-on: ubuntu-latest
231
+ steps:
232
+ - uses: actions/checkout@v4
233
+
234
+ - uses: actions/setup-node@v4
235
+ with:
236
+ node-version: 18
237
+
238
+ - run: npm install -g logshield-cli
239
+
240
+ - name: Scan logs
241
+ run: |
242
+ cat logs.txt | logshield scan --dry-run --fail-on-detect
243
+ ```
244
+
245
+ This will **fail the pipeline** if any secret is detected.
246
+
247
+ ---
248
+
249
+ ## Apply redaction
250
+
251
+ To actually sanitize logs, run **without** `--dry-run`:
38
252
 
39
253
  ```bash
40
- logshield scan app.log --json
254
+ cat app.log | logshield scan > sanitized.log
41
255
  ```
42
256
 
43
- Summary only (printed to stderr):
257
+ ---
258
+
259
+ ## Strict mode
260
+
261
+ Enable more aggressive detection rules:
44
262
 
45
263
  ```bash
46
- logshield scan app.log --summary
264
+ logshield scan --strict < logs.txt
47
265
  ```
48
266
 
49
267
  ---
50
268
 
51
- ## What Gets Redacted
269
+ ## Summary output
270
+
271
+ Print a compact rule-based summary:
272
+
273
+ ```bash
274
+ logshield scan --summary < logs.txt
275
+ ```
276
+
277
+ Example:
278
+
279
+ ```
280
+ LogShield Summary
281
+ PASSWORD: 2
282
+ API_KEY_HEADER: 1
283
+ ```
284
+
285
+ ---
286
+
287
+ ## JSON output
288
+
289
+ Structured output for tooling and automation:
290
+
291
+ ```bash
292
+ logshield scan --json < logs.txt
293
+ ```
294
+
295
+ Notes:
296
+
297
+ - `--json` **cannot** be combined with `--dry-run`
298
+ - Output schema is stable within v0.3.x
299
+
300
+ ---
301
+
302
+ ## Exit codes
303
+
304
+ | Code | Meaning |
305
+ | ---: | ------------------------------------ |
306
+ | 0 | Success / no detection |
307
+ | 1 | Detection found (`--fail-on-detect`) |
308
+ | 2 | Runtime or input error |
309
+
310
+ ---
311
+
312
+ ## What gets redacted
313
+
314
+ Depending on rules and mode:
52
315
 
53
- - API keys
54
316
  - Passwords
55
- - JWT tokens
56
- - `Bearer <TOKEN>` (always redacted)
57
- - Stripe keys
58
- - Cloud credentials (AWS, etc.)
59
- - Credit cards (Luhn-validated)
317
+ - API key headers
318
+ - Authorization bearer tokens
319
+ - JWTs
320
+ - Emails
321
+ - URLs with embedded credentials
322
+ - Database credentials
323
+ - Cloud provider credentials
324
+ - Credit card numbers (Luhn-validated)
60
325
 
61
326
  ---
62
327
 
63
328
  ## Modes
64
329
 
65
- ### Default (recommended)
330
+ ### Default mode (recommended)
66
331
 
67
332
  - Conservative
68
333
  - Low false positives
69
334
  - Safe for sharing logs publicly
70
335
 
71
- ### Strict
336
+ ### Strict mode
72
337
 
73
338
  - Aggressive
74
339
  - Security-first
@@ -76,21 +341,36 @@ logshield scan app.log --summary
76
341
 
77
342
  ---
78
343
 
79
- ## Design Guarantees
344
+ ## Guarantees
345
+
346
+ LogShield guarantees:
80
347
 
81
348
  - Deterministic output
82
- - Zero runtime dependencies
83
- - Snapshot-tested & contract-tested
84
- - No network calls
349
+ - Stable behavior within **v0.3.x**
350
+ - No runtime dependencies
351
+ - Snapshot-tested and contract-tested
85
352
  - No telemetry
353
+ - No network access
86
354
 
87
355
  ---
88
356
 
89
- ## Example
357
+ ## Non-goals
90
358
 
91
- ```bash
92
- cat server.log | logshield scan --strict --summary
93
- ```
359
+ LogShield is **not**:
360
+
361
+ - A DLP system
362
+ - A runtime security monitor
363
+ - A secret rotation solution
364
+
365
+ It is a **last-line safety net**, not a primary defense.
366
+
367
+ ---
368
+
369
+ ## Status
370
+
371
+ - Engine behavior locked
372
+ - Rule set evolving conservatively
373
+ - Open and free by design
94
374
 
95
375
  ---
96
376
 
@@ -9,6 +9,9 @@ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
9
  var __esm = (fn, res) => function __init() {
10
10
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
11
11
  };
12
+ var __commonJS = (cb, mod) => function __require() {
13
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
14
+ };
12
15
  var __export = (target, all) => {
13
16
  for (var name in all)
14
17
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -59,7 +62,6 @@ async function readInput(file) {
59
62
  var import_node_fs;
60
63
  var init_readInput = __esm({
61
64
  "src/cli/readInput.ts"() {
62
- "use strict";
63
65
  import_node_fs = __toESM(require("node:fs"));
64
66
  }
65
67
  });
@@ -78,29 +80,6 @@ function writeOutput(result, opts) {
78
80
  }
79
81
  var init_writeOutput = __esm({
80
82
  "src/cli/writeOutput.ts"() {
81
- "use strict";
82
- }
83
- });
84
-
85
- // src/cli/summary.ts
86
- var summary_exports = {};
87
- __export(summary_exports, {
88
- printSummary: () => printSummary
89
- });
90
- function printSummary(matches) {
91
- const counter = {};
92
- for (const m of matches) {
93
- counter[m.rule] = (counter[m.rule] || 0) + 1;
94
- }
95
- process.stderr.write("LogShield Summary\n");
96
- for (const [rule, count] of Object.entries(counter)) {
97
- process.stderr.write(`${rule}: ${count}
98
- `);
99
- }
100
- }
101
- var init_summary = __esm({
102
- "src/cli/summary.ts"() {
103
- "use strict";
104
83
  }
105
84
  });
106
85
 
@@ -118,6 +97,9 @@ function applyRules(input, rules, ctx, matches) {
118
97
  value: match
119
98
  });
120
99
  }
100
+ if (ctx.dryRun) {
101
+ return match;
102
+ }
121
103
  return replaced;
122
104
  });
123
105
  }
@@ -125,7 +107,6 @@ function applyRules(input, rules, ctx, matches) {
125
107
  }
126
108
  var init_applyRules = __esm({
127
109
  "src/engine/applyRules.ts"() {
128
- "use strict";
129
110
  }
130
111
  });
131
112
 
@@ -140,7 +121,6 @@ function guardInput(input) {
140
121
  var MAX_SIZE;
141
122
  var init_guard = __esm({
142
123
  "src/engine/guard.ts"() {
143
- "use strict";
144
124
  MAX_SIZE = 200 * 1024;
145
125
  }
146
126
  });
@@ -149,7 +129,6 @@ var init_guard = __esm({
149
129
  var tokenRules;
150
130
  var init_tokens = __esm({
151
131
  "src/rules/tokens.ts"() {
152
- "use strict";
153
132
  tokenRules = [
154
133
  {
155
134
  name: "JWT",
@@ -184,7 +163,6 @@ var init_tokens = __esm({
184
163
  var credentialRules;
185
164
  var init_credentials = __esm({
186
165
  "src/rules/credentials.ts"() {
187
- "use strict";
188
166
  credentialRules = [
189
167
  {
190
168
  name: "PASSWORD",
@@ -217,7 +195,6 @@ var init_credentials = __esm({
217
195
  var cloudRules;
218
196
  var init_cloud = __esm({
219
197
  "src/rules/cloud.ts"() {
220
- "use strict";
221
198
  cloudRules = [
222
199
  {
223
200
  name: "AWS_ACCESS_KEY",
@@ -256,7 +233,6 @@ function isValidLuhn(input) {
256
233
  }
257
234
  var init_luhn = __esm({
258
235
  "src/utils/luhn.ts"() {
259
- "use strict";
260
236
  }
261
237
  });
262
238
 
@@ -264,7 +240,6 @@ var init_luhn = __esm({
264
240
  var creditCardRules;
265
241
  var init_creditCard = __esm({
266
242
  "src/rules/creditCard.ts"() {
267
- "use strict";
268
243
  init_luhn();
269
244
  creditCardRules = [
270
245
  {
@@ -283,7 +258,6 @@ var init_creditCard = __esm({
283
258
  var urlRules;
284
259
  var init_urls = __esm({
285
260
  "src/rules/urls.ts"() {
286
- "use strict";
287
261
  urlRules = [
288
262
  {
289
263
  name: "URL",
@@ -298,7 +272,6 @@ var init_urls = __esm({
298
272
  var customRules;
299
273
  var init_custom = __esm({
300
274
  "src/rules/custom.ts"() {
301
- "use strict";
302
275
  customRules = [
303
276
  {
304
277
  name: "GENERIC_SECRET_KV",
@@ -329,7 +302,6 @@ function normalize(rules) {
329
302
  var allRules;
330
303
  var init_rules = __esm({
331
304
  "src/rules/index.ts"() {
332
- "use strict";
333
305
  init_tokens();
334
306
  init_credentials();
335
307
  init_cloud();
@@ -358,39 +330,181 @@ function sanitizeLog(input, options) {
358
330
  return { output: "", matches: [] };
359
331
  }
360
332
  const ctx = {
361
- strict: Boolean(options?.strict)
333
+ strict: Boolean(options?.strict),
334
+ dryRun: Boolean(options?.dryRun)
362
335
  };
363
336
  const matches = [];
337
+ if (ctx.dryRun) {
338
+ applyRules(input, allRules, ctx, matches);
339
+ return { output: input, matches };
340
+ }
364
341
  const output = applyRules(input, allRules, ctx, matches);
365
342
  return { output, matches };
366
343
  }
367
344
  var init_sanitizeLog = __esm({
368
345
  "src/engine/sanitizeLog.ts"() {
369
- "use strict";
370
346
  init_applyRules();
371
347
  init_guard();
372
348
  init_rules();
373
349
  }
374
350
  });
375
351
 
352
+ // src/cli/summary.ts
353
+ var require_summary = __commonJS({
354
+ "src/cli/summary.ts"() {
355
+ "use strict";
356
+ var { readInput: readInput3 } = (init_readInput(), __toCommonJS(readInput_exports));
357
+ var { writeOutput: writeOutput3 } = (init_writeOutput(), __toCommonJS(writeOutput_exports));
358
+ var { printSummary: printSummary2 } = require_summary();
359
+ var { sanitizeLog: sanitizeLog3 } = (init_sanitizeLog(), __toCommonJS(sanitizeLog_exports));
360
+ var rawArgs2 = process.argv.slice(2).map((arg) => arg === "-h" ? "--help" : arg);
361
+ function getVersion2() {
362
+ return true ? "0.3.2" : "unknown";
363
+ }
364
+ function printHelp2() {
365
+ process.stdout.write(`Usage: logshield scan [file]
366
+
367
+ Behavior:
368
+ - If a file is provided, LogShield reads from that file
369
+ - If input is piped, LogShield reads from STDIN automatically
370
+ - --stdin is optional and only needed for explicitness
371
+
372
+ Options:
373
+ --strict Aggressive redaction
374
+ --dry-run Preview redactions without modifying output
375
+ --stdin Force read from STDIN
376
+ --fail-on-detect Exit with code 1 if any redaction occurs
377
+ --json JSON output
378
+ --summary Print summary
379
+ --version Print version
380
+ --help Show help
381
+ `);
382
+ }
383
+ function parseArgs2(args) {
384
+ const flags = /* @__PURE__ */ new Set();
385
+ const positionals = [];
386
+ for (const arg of args) {
387
+ if (arg.startsWith("--")) {
388
+ flags.add(arg);
389
+ } else {
390
+ positionals.push(arg);
391
+ }
392
+ }
393
+ return { flags, positionals };
394
+ }
395
+ function isStdinPiped2() {
396
+ return !process.stdin.isTTY;
397
+ }
398
+ function renderDryRunReport2(matches) {
399
+ if (matches.length === 0) {
400
+ process.stdout.write("[DRY RUN] No redactions detected.\n");
401
+ process.stdout.write("No output was modified.\n");
402
+ return;
403
+ }
404
+ const counts = /* @__PURE__ */ new Map();
405
+ for (const m of matches) {
406
+ counts.set(m.rule, (counts.get(m.rule) || 0) + 1);
407
+ }
408
+ process.stdout.write("[DRY RUN] Detected redactions:\n");
409
+ const maxLen = Math.max(
410
+ ...Array.from(counts.keys()).map((k) => k.length)
411
+ );
412
+ for (const [rule, count] of counts.entries()) {
413
+ process.stdout.write(
414
+ ` ${rule.padEnd(maxLen)} x${count}
415
+ `
416
+ );
417
+ }
418
+ process.stdout.write("\n");
419
+ process.stdout.write("No output was modified.\n");
420
+ process.stdout.write("Use without --dry-run to apply.\n");
421
+ }
422
+ async function main2() {
423
+ if (rawArgs2.length === 0 || rawArgs2.includes("--help")) {
424
+ printHelp2();
425
+ process.exit(0);
426
+ }
427
+ if (rawArgs2.includes("--version")) {
428
+ console.log(`logshield v${getVersion2()}`);
429
+ process.exit(0);
430
+ }
431
+ const { flags, positionals } = parseArgs2(rawArgs2);
432
+ const command = positionals[0];
433
+ if (command !== "scan") {
434
+ process.stdout.write("Unknown command\n");
435
+ process.exit(1);
436
+ }
437
+ const file = positionals[1];
438
+ const strict = flags.has("--strict");
439
+ const json = flags.has("--json");
440
+ const summary = flags.has("--summary");
441
+ const stdinFlag = flags.has("--stdin");
442
+ const failOnDetect = flags.has("--fail-on-detect");
443
+ const dryRun = flags.has("--dry-run");
444
+ const stdinAuto = isStdinPiped2();
445
+ const useStdin = stdinFlag || stdinAuto;
446
+ if (useStdin && file) {
447
+ process.stdout.write("Cannot read from both STDIN and file\n");
448
+ process.exit(1);
449
+ }
450
+ if (dryRun && json) {
451
+ process.stdout.write("--dry-run cannot be used with --json\n");
452
+ process.exit(1);
453
+ }
454
+ try {
455
+ const input = await readInput3(useStdin ? void 0 : file);
456
+ const result = sanitizeLog3(input, { strict });
457
+ if (dryRun) {
458
+ renderDryRunReport2(result.matches);
459
+ if (failOnDetect && result.matches.length > 0) {
460
+ process.exit(1);
461
+ }
462
+ process.exit(0);
463
+ }
464
+ writeOutput3(result, { json });
465
+ if (summary) {
466
+ printSummary2(result.matches);
467
+ }
468
+ if (failOnDetect && result.matches.length > 0) {
469
+ process.exit(1);
470
+ }
471
+ process.exit(0);
472
+ } catch (err) {
473
+ process.stdout.write(err?.message || "Unexpected error");
474
+ process.stdout.write("\n");
475
+ process.exit(2);
476
+ }
477
+ }
478
+ main2();
479
+ }
480
+ });
481
+
376
482
  // src/cli/index.ts
377
483
  var { readInput: readInput2 } = (init_readInput(), __toCommonJS(readInput_exports));
378
484
  var { writeOutput: writeOutput2 } = (init_writeOutput(), __toCommonJS(writeOutput_exports));
379
- var { printSummary: printSummary2 } = (init_summary(), __toCommonJS(summary_exports));
485
+ var { printSummary } = require_summary();
380
486
  var { sanitizeLog: sanitizeLog2 } = (init_sanitizeLog(), __toCommonJS(sanitizeLog_exports));
381
- var rawArgs = process.argv.slice(2);
487
+ var rawArgs = process.argv.slice(2).map((arg) => arg === "-h" ? "--help" : arg);
382
488
  function getVersion() {
383
- return true ? "0.3.0" : "unknown";
489
+ return true ? "0.3.2" : "unknown";
384
490
  }
385
491
  function printHelp() {
386
492
  process.stdout.write(`Usage: logshield scan [file]
387
493
 
494
+ Behavior:
495
+ - If a file is provided, LogShield reads from that file
496
+ - If input is piped, LogShield reads from STDIN automatically
497
+ - --stdin is optional and only needed for explicitness
498
+
388
499
  Options:
389
- --strict Aggressive redaction
390
- --json JSON output
391
- --summary Print summary
392
- --version Print version
393
- --help Show help
500
+ --strict Aggressive redaction
501
+ --dry-run Report detected redactions only
502
+ --stdin Force read from STDIN
503
+ --fail-on-detect Exit with code 1 if any redaction occurs
504
+ --json JSON output
505
+ --summary Print summary
506
+ --version Print version
507
+ --help Show help
394
508
  `);
395
509
  }
396
510
  function parseArgs(args) {
@@ -405,36 +519,94 @@ function parseArgs(args) {
405
519
  }
406
520
  return { flags, positionals };
407
521
  }
522
+ function isStdinPiped() {
523
+ return !process.stdin.isTTY;
524
+ }
525
+ function renderDryRunReport(matches) {
526
+ if (matches.length === 0) {
527
+ process.stdout.write("logshield (dry-run)\n");
528
+ process.stdout.write("Detected 0 redactions.\n");
529
+ process.stdout.write("No output was modified.\n");
530
+ return;
531
+ }
532
+ const counter = {};
533
+ for (const m of matches) {
534
+ counter[m.rule] = (counter[m.rule] || 0) + 1;
535
+ }
536
+ const entries = Object.entries(counter).map(([rule, count]) => ({ rule, count })).sort((a, b) => {
537
+ if (b.count !== a.count) return b.count - a.count;
538
+ return a.rule.localeCompare(b.rule);
539
+ });
540
+ const maxLen = Math.max(...entries.map((e) => e.rule.length));
541
+ const total = matches.length;
542
+ const label = total === 1 ? "redaction" : "redactions";
543
+ process.stdout.write("logshield (dry-run)\n");
544
+ process.stdout.write(`Detected ${total} ${label}:
545
+ `);
546
+ for (const { rule, count } of entries) {
547
+ process.stdout.write(
548
+ ` ${rule.padEnd(maxLen)} x${count}
549
+ `
550
+ );
551
+ }
552
+ process.stdout.write("\n");
553
+ process.stdout.write("No output was modified.\n");
554
+ process.stdout.write("Use without --dry-run to apply.\n");
555
+ }
408
556
  async function main() {
409
557
  if (rawArgs.length === 0 || rawArgs.includes("--help")) {
410
558
  printHelp();
411
559
  process.exit(0);
412
560
  }
413
561
  if (rawArgs.includes("--version")) {
414
- console.log(`logshield v${getVersion()}`);
562
+ process.stdout.write(`logshield v${getVersion()}
563
+ `);
415
564
  process.exit(0);
416
565
  }
417
566
  const { flags, positionals } = parseArgs(rawArgs);
418
567
  const command = positionals[0];
419
568
  if (command !== "scan") {
420
- process.stderr.write("Unknown command\n");
569
+ process.stdout.write("Unknown command\n");
421
570
  process.exit(1);
422
571
  }
423
572
  const file = positionals[1];
424
573
  const strict = flags.has("--strict");
425
574
  const json = flags.has("--json");
426
575
  const summary = flags.has("--summary");
576
+ const stdinFlag = flags.has("--stdin");
577
+ const failOnDetect = flags.has("--fail-on-detect");
578
+ const dryRun = flags.has("--dry-run");
579
+ const stdinAuto = isStdinPiped();
580
+ const useStdin = stdinFlag || stdinAuto;
581
+ if (useStdin && file) {
582
+ process.stdout.write("Cannot read from both STDIN and file\n");
583
+ process.exit(1);
584
+ }
585
+ if (dryRun && json) {
586
+ process.stdout.write("--dry-run cannot be used with --json\n");
587
+ process.exit(1);
588
+ }
427
589
  try {
428
- const input = await readInput2(file);
590
+ const input = await readInput2(useStdin ? void 0 : file);
429
591
  const result = sanitizeLog2(input, { strict });
592
+ if (dryRun) {
593
+ renderDryRunReport(result.matches);
594
+ if (failOnDetect && result.matches.length > 0) {
595
+ process.exit(1);
596
+ }
597
+ process.exit(0);
598
+ }
430
599
  writeOutput2(result, { json });
431
600
  if (summary) {
432
- printSummary2(result.matches);
601
+ printSummary(result.matches);
602
+ }
603
+ if (failOnDetect && result.matches.length > 0) {
604
+ process.exit(1);
433
605
  }
434
606
  process.exit(0);
435
607
  } catch (err) {
436
- process.stderr.write(err?.message || "Unexpected error");
437
- process.stderr.write("\n");
608
+ process.stdout.write(err?.message || "Unexpected error");
609
+ process.stdout.write("\n");
438
610
  process.exit(2);
439
611
  }
440
612
  }
package/package.json CHANGED
@@ -1,21 +1,29 @@
1
- {
2
- "name": "logshield-cli",
3
- "version": "0.3.1",
4
- "type": "commonjs",
5
- "bin": {
6
- "logshield": "dist/cli/index.cjs"
7
- },
8
- "files": [
9
- "dist",
10
- "README.md",
11
- "LICENSE"
12
- ],
13
- "scripts": {
14
- "build": "node scripts/build-cli.cjs",
15
- "test": "vitest"
16
- },
17
- "devDependencies": {
18
- "esbuild": "^0.25.0",
19
- "vitest": "^4.0.0"
20
- }
21
- }
1
+ {
2
+ "name": "logshield-cli",
3
+ "version": "0.3.3",
4
+ "license": "ISC",
5
+ "type": "commonjs",
6
+ "bin": {
7
+ "logshield": "dist/cli/index.cjs"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "README.md",
12
+ "LICENSE"
13
+ ],
14
+ "scripts": {
15
+ "build": "node scripts/build-cli.cjs",
16
+ "build:web": "vite build --outDir dist-web",
17
+ "dev:web": "vite",
18
+ "pretest": "npm run build",
19
+ "test": "vitest"
20
+ },
21
+ "devDependencies": {
22
+ "@types/node": "^25.0.3",
23
+ "autoprefixer": "^10.4.23",
24
+ "esbuild": "^0.25.0",
25
+ "postcss": "^8.5.6",
26
+ "tailwindcss": "^3.4.19",
27
+ "vitest": "^4.0.0"
28
+ }
29
+ }
package/dist/index.html DELETED
@@ -1,16 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <meta name="description"
7
- content="Privacy-first log sanitizer. Remove API keys, tokens, and PII instantly. 100% client-side." />
8
- <title>LogShield</title>
9
- <script defer data-domain="logshield.dev" src="https://plausible.io/js/script.js"></script>
10
- <script type="module" crossorigin src="/assets/index-DDJ1Wxio.js"></script>
11
- <link rel="stylesheet" crossorigin href="/assets/index-B3qxIuiz.css">
12
- </head>
13
- <body>
14
- <div id="root"></div>
15
- </body>
16
- </html>