logshield-cli 0.3.2 → 0.3.4

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
- # ?? LogShield
1
+ ---
2
+
3
+ # LogShield
4
+
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/afria85/LogShield/actions/workflows/ci.yml/badge.svg)](https://github.com/afria85/LogShield/actions/workflows/ci.yml)
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.
66
+
67
+ ---
2
68
 
3
- LogShield is a CLI tool to **redact real sensitive data from logs** before sharing them with others, AI tools, CI systems, or public channels.
69
+ ### 3. Explicit redaction markers
4
70
 
5
- It is designed to be **deterministic**, **safe by default**, and free of runtime dependencies.
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.
6
83
 
7
84
  ---
8
85
 
9
- ## Install
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,102 +113,244 @@ npm install -g logshield-cli
14
113
 
15
114
  ---
16
115
 
17
- ## Usage
116
+ ## CLI Usage
117
+
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
18
155
 
19
- Scan a log file:
156
+ ### Scan a file
20
157
 
21
158
  ```bash
22
159
  logshield scan app.log
23
160
  ```
24
161
 
25
- Scan from stdin (auto-detected):
162
+ ### Scan from STDIN (recommended for CI)
26
163
 
27
164
  ```bash
28
165
  cat app.log | logshield scan
29
166
  ```
30
167
 
31
- Explicit stdin mode:
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.
32
175
 
33
176
  ```bash
34
- cat app.log | logshield scan --stdin
177
+ cat app.log | logshield scan --dry-run
35
178
  ```
36
179
 
37
- Strict mode (more aggressive redaction):
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.
38
204
 
39
205
  ```bash
40
- logshield scan app.log --strict
206
+ cat app.log | logshield scan --dry-run --fail-on-detect
41
207
  ```
42
208
 
43
- JSON output (machine-readable):
209
+ Typical CI pattern:
44
210
 
45
211
  ```bash
46
- logshield scan app.log --json
212
+ logshield scan --dry-run --fail-on-detect < logs.txt
213
+ ```
214
+
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
47
243
  ```
48
244
 
49
- Print summary to stderr:
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`:
50
252
 
51
253
  ```bash
52
- logshield scan app.log --summary
254
+ cat app.log | logshield scan > sanitized.log
53
255
  ```
54
256
 
55
- Fail CI if any secret is detected:
257
+ ---
258
+
259
+ ## Strict mode
260
+
261
+ Enable more aggressive detection rules:
56
262
 
57
263
  ```bash
58
- logshield scan app.log --strict --fail-on-detect
264
+ logshield scan --strict < logs.txt
59
265
  ```
60
266
 
61
267
  ---
62
268
 
63
- ## What Gets Redacted
269
+ ## Summary output
270
+
271
+ Print a compact rule-based summary:
64
272
 
65
- - Passwords (`password=...`, DB URLs)
66
- - API keys (query params, headers)
67
- - JWT tokens
68
- - `Authorization: Bearer <TOKEN>`
69
- - Stripe secret keys
70
- - Cloud credentials (AWS access & secret keys)
71
- - OAuth access & refresh tokens
72
- - Credit cards (Luhn-validated, strict mode)
73
- - URLs (sanitized to avoid leaking endpoints)
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
+ ```
74
284
 
75
285
  ---
76
286
 
77
- ## Modes
287
+ ## JSON output
78
288
 
79
- ### Default (recommended)
289
+ Structured output for tooling and automation:
80
290
 
81
- - Conservative
82
- - Very low false positives
83
- - Preserves debugging context
291
+ ```bash
292
+ logshield scan --json < logs.txt
293
+ ```
84
294
 
85
- ### Strict
295
+ Notes:
86
296
 
87
- - Security-first
88
- - Redacts more aggressively
89
- - Suitable for CI, support bundles, and AI sharing
297
+ - `--json` **cannot** be combined with `--dry-run`
298
+ - Output schema is stable within v0.3.x
90
299
 
91
300
  ---
92
301
 
93
- ## Design Guarantees
302
+ ## Exit codes
94
303
 
95
- These guarantees are **locked for v0.3.x**:
304
+ | Code | Meaning |
305
+ | ---: | ------------------------------------ |
306
+ | 0 | Success / no detection |
307
+ | 1 | Detection found (`--fail-on-detect`) |
308
+ | 2 | Runtime or input error |
96
309
 
97
- - Deterministic output (same input ? same output)
98
- - Zero runtime dependencies
99
- - No network calls
100
- - No telemetry or tracking
101
- - Snapshot-tested & contract-tested
102
- - Fixed rule order (token ? credential ? cloud ? CC ? URL ? custom)
310
+ ---
103
311
 
104
- When in doubt, LogShield prefers **not** to redact.
312
+ ## What gets redacted
313
+
314
+ Depending on rules and mode:
315
+
316
+ - Passwords
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)
105
325
 
106
326
  ---
107
327
 
108
- ## Example
328
+ ## Modes
109
329
 
110
- ```bash
111
- cat server.log | logshield scan --strict --summary
112
- ```
330
+ ### Default mode (recommended)
331
+
332
+ - Conservative
333
+ - Low false positives
334
+ - Safe for sharing logs publicly
335
+
336
+ ### Strict mode
337
+
338
+ - Aggressive
339
+ - Security-first
340
+ - May redact more than necessary
341
+
342
+ ---
343
+
344
+ ## Guarantees
345
+
346
+ LogShield guarantees:
347
+
348
+ - Deterministic output
349
+ - Stable behavior within **v0.3.x**
350
+ - No runtime dependencies
351
+ - Snapshot-tested and contract-tested
352
+ - No telemetry
353
+ - No network access
113
354
 
114
355
  ---
115
356
 
@@ -119,12 +360,22 @@ LogShield is **not**:
119
360
 
120
361
  - A DLP system
121
362
  - A runtime security monitor
122
- - A replacement for secret rotation
363
+ - A secret rotation solution
123
364
 
124
365
  It is a **last-line safety net**, not a primary defense.
125
366
 
126
367
  ---
127
368
 
369
+ ## Status
370
+
371
+ - Engine behavior locked
372
+ - Rule set evolving conservatively
373
+ - Open and free by design
374
+
375
+ ---
376
+
128
377
  ## License
129
378
 
130
379
  ISC
380
+
381
+ ---
@@ -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,36 +330,176 @@ 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
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
500
  --strict Aggressive redaction
390
- --stdin Read input from STDIN explicitly
501
+ --dry-run Report detected redactions only
502
+ --stdin Force read from STDIN
391
503
  --fail-on-detect Exit with code 1 if any redaction occurs
392
504
  --json JSON output
393
505
  --summary Print summary
@@ -407,45 +519,94 @@ function parseArgs(args) {
407
519
  }
408
520
  return { flags, positionals };
409
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
+ }
410
556
  async function main() {
411
557
  if (rawArgs.length === 0 || rawArgs.includes("--help")) {
412
558
  printHelp();
413
559
  process.exit(0);
414
560
  }
415
561
  if (rawArgs.includes("--version")) {
416
- console.log(`logshield v${getVersion()}`);
562
+ process.stdout.write(`logshield v${getVersion()}
563
+ `);
417
564
  process.exit(0);
418
565
  }
419
566
  const { flags, positionals } = parseArgs(rawArgs);
420
567
  const command = positionals[0];
421
568
  if (command !== "scan") {
422
- process.stderr.write("Unknown command\n");
569
+ process.stdout.write("Unknown command\n");
423
570
  process.exit(1);
424
571
  }
425
572
  const file = positionals[1];
426
573
  const strict = flags.has("--strict");
427
574
  const json = flags.has("--json");
428
575
  const summary = flags.has("--summary");
429
- const stdin = flags.has("--stdin");
576
+ const stdinFlag = flags.has("--stdin");
430
577
  const failOnDetect = flags.has("--fail-on-detect");
431
- if (stdin && file) {
432
- process.stderr.write("Cannot use --stdin with file argument\n");
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");
433
587
  process.exit(1);
434
588
  }
435
589
  try {
436
- const input = await readInput2(stdin ? void 0 : file);
590
+ const input = await readInput2(useStdin ? void 0 : file);
437
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
+ }
438
599
  writeOutput2(result, { json });
439
600
  if (summary) {
440
- printSummary2(result.matches);
601
+ printSummary(result.matches);
441
602
  }
442
603
  if (failOnDetect && result.matches.length > 0) {
443
604
  process.exit(1);
444
605
  }
445
606
  process.exit(0);
446
607
  } catch (err) {
447
- process.stderr.write(err?.message || "Unexpected error");
448
- process.stderr.write("\n");
608
+ process.stdout.write(err?.message || "Unexpected error");
609
+ process.stdout.write("\n");
449
610
  process.exit(2);
450
611
  }
451
612
  }
package/package.json CHANGED
@@ -1,21 +1,29 @@
1
- {
2
- "name": "logshield-cli",
3
- "version": "0.3.2",
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.4",
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>