logshield-cli 0.4.1 → 0.4.2

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 CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.4.2
4
+
5
+ ### Fixed
6
+
7
+ - CLI errors are now written to stderr (CI-safe piping)
8
+ - JSON output is newline-terminated
9
+ - URL redaction is no longer overly aggressive; only credentials and sensitive parameters are redacted
10
+ - PASSWORD redaction preserves original delimiter and spacing
11
+ - Improved dry-run reporting consistency
12
+ - Added contract tests for CLI output and URL behavior
13
+
3
14
  ## v0.4.1
4
15
 
5
16
  ### Fixed
package/README.md CHANGED
@@ -1,18 +1,59 @@
1
- ---
2
1
  # LogShield
3
2
 
4
3
  [![npm version](https://img.shields.io/npm/v/logshield-cli)](https://www.npmjs.com/package/logshield-cli)
5
4
  [![npm downloads](https://img.shields.io/npm/dm/logshield-cli)](https://www.npmjs.com/package/logshield-cli)
6
5
  [![CI](https://github.com/afria85/LogShield/actions/workflows/ci.yml/badge.svg)](https://github.com/afria85/LogShield/actions/workflows/ci.yml)
7
6
 
8
- Deterministic log sanitization for developers.
7
+ Your logs already contain secrets. You just don’t see them.
8
+
9
+ LogShield is a small CLI that automatically redacts secrets from logs **before**
10
+ you paste them into CI, GitHub issues, Slack, or send them to third-party support.
11
+
12
+ No configuration. No cloud. Deterministic output.
13
+
14
+ ---
9
15
 
10
16
  ## Quick start (30 seconds)
11
17
 
12
18
  ```bash
13
- # Install (CLI command: logshield)
14
- npm install -g logshield-cli
19
+ # Sanitize logs before sharing them
20
+ cat app.log | logshield scan
21
+ ```
22
+
23
+ **Example input**
24
+
25
+ ```txt
26
+ POSTGRES_URL=postgres://user:supersecret@db.internal
27
+ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
28
+ ```
29
+
30
+ These are typical raw logs -- with secrets -- before you share them.
31
+
32
+ **Output**
33
+
34
+ ```txt
35
+ POSTGRES_URL=postgres://user:<REDACTED_PASSWORD>@db.internal
36
+ Authorization: Bearer <REDACTED_TOKEN>
37
+ ```
15
38
 
39
+ After LogShield, the same logs are safe to share.
40
+
41
+ ---
42
+
43
+ ## When should I use LogShield?
44
+
45
+ Use LogShield whenever logs leave your system:
46
+
47
+ - Before pasting logs into CI
48
+ - Before attaching logs to GitHub issues
49
+ - Before sending logs to third-party support
50
+ - Before sharing logs in Slack or email
51
+
52
+ ---
53
+
54
+ ## Preview before enforcing (dry-run)
55
+
56
+ ```bash
16
57
  # Preview what would be redacted (does not modify output)
17
58
  echo "email=test@example.com token=sk_live_123" | logshield scan --dry-run
18
59
  ```
@@ -35,7 +76,9 @@ echo "email=test@example.com token=sk_live_123" | logshield scan
35
76
  - Prefer `--dry-run` first in CI to verify you are not over-redacting.
36
77
  - Then switch to enforced mode once you are satisfied with the preview.
37
78
 
38
- 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.
79
+ LogShield is a CLI tool that scans logs and redacts **real secrets**
80
+ (API keys, tokens, credentials) before logs are shared with others,
81
+ AI tools, CI systems, or public channels.
39
82
 
40
83
  It is designed to be **predictable, conservative, and safe for production pipelines**.
41
84
 
@@ -148,28 +191,28 @@ If a file is not provided and input is piped, LogShield automatically reads from
148
191
 
149
192
  ## CLI Flags
150
193
 
151
- - `--strict`
194
+ - `--strict`
152
195
  Aggressive, security-first redaction
153
196
 
154
- - `--stdin`
197
+ - `--stdin`
155
198
  Explicitly force reading from STDIN
156
199
 
157
- - `--dry-run`
200
+ - `--dry-run`
158
201
  Detect sensitive data without modifying output
159
202
 
160
- - `--fail-on-detect`
203
+ - `--fail-on-detect`
161
204
  Exit with code `1` if any redaction is detected (CI-friendly)
162
205
 
163
- - `--summary`
206
+ - `--summary`
164
207
  Print a compact redaction summary
165
208
 
166
- - `--json`
209
+ - `--json`
167
210
  JSON output (cannot be combined with `--dry-run`)
168
211
 
169
- - `--version`
212
+ - `--version`
170
213
  Print CLI version
171
214
 
172
- - `--help`
215
+ - `--help`
173
216
  Show help
174
217
 
175
218
  ---
@@ -204,10 +247,10 @@ cat app.log | logshield scan --dry-run
204
247
 
205
248
  ```
206
249
  logshield (dry-run)
207
- Detected 3 redactions:
208
- OAUTH_ACCESS_TOKEN x1
250
+ Detected 5 redactions:
209
251
  AUTH_BEARER x2
210
252
  EMAIL x1
253
+ OAUTH_ACCESS_TOKEN x1
211
254
  PASSWORD x1
212
255
 
213
256
  No output was modified.
@@ -319,7 +362,7 @@ logshield scan --json < logs.txt
319
362
  Notes:
320
363
 
321
364
  - `--json` **cannot** be combined with `--dry-run`
322
- - Output schema is stable within v0.3.x
365
+ - Output schema is stable within v0.4.x
323
366
 
324
367
  ---
325
368
 
@@ -370,7 +413,7 @@ Depending on rules and mode:
370
413
  LogShield guarantees:
371
414
 
372
415
  - Deterministic output
373
- - Stable behavior within **v0.3.x**
416
+ - Stable behavior within **v0.4.x**
374
417
  - No runtime dependencies
375
418
  - Snapshot-tested and contract-tested
376
419
  - No telemetry
@@ -401,5 +444,3 @@ It is a **last-line safety net**, not a primary defense.
401
444
  ## License
402
445
 
403
446
  Apache-2.0
404
-
405
- ---
@@ -70,7 +70,8 @@ __export(writeOutput_exports, {
70
70
  });
71
71
  function writeOutput(result, opts) {
72
72
  if (opts.json) {
73
- process.stdout.write(JSON.stringify(result));
73
+ process.stdout.write(`${JSON.stringify(result)}
74
+ `);
74
75
  } else {
75
76
  process.stdout.write(result.output);
76
77
  }
@@ -184,8 +185,12 @@ var init_tokens = __esm({
184
185
  },
185
186
  {
186
187
  name: "EMAIL",
187
- pattern: /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi,
188
- replace: () => "<REDACTED_EMAIL>"
188
+ // Avoid corrupting URLs with embedded credentials like:
189
+ // https://user:pass@host
190
+ // In those cases, `pass@host` can look like an email.
191
+ // We therefore require a safe delimiter (whitespace/quotes/brackets/`=` or `: `) before the email.
192
+ pattern: /(^|[\s"'\(\[\{<>,;]|=|:\s)([A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,})/gim,
193
+ replace: (_match, _ctx, groups) => `${groups[0]}<REDACTED_EMAIL>`
189
194
  }
190
195
  ];
191
196
  }
@@ -199,8 +204,12 @@ var init_credentials = __esm({
199
204
  // password=... or password: ...
200
205
  {
201
206
  name: "PASSWORD",
202
- pattern: /\bpassword\s*[:=]\s*([^\s]+)/gi,
203
- replace: () => "password=<REDACTED_PASSWORD>"
207
+ // Preserve delimiter and spacing so logs remain readable and diff-friendly.
208
+ // Examples:
209
+ // password=secret -> password=<REDACTED_PASSWORD>
210
+ // Password : 123 -> Password : <REDACTED_PASSWORD>
211
+ pattern: /\b(password)(\s*[:=]\s*)([^\s]+)/gi,
212
+ replace: (_match, _ctx, groups) => `${groups[0]}${groups[1]}<REDACTED_PASSWORD>`
204
213
  },
205
214
  // DB URL credential: postgres://user:pass@host
206
215
  {
@@ -316,14 +325,100 @@ var init_creditCard = __esm({
316
325
  });
317
326
 
318
327
  // src/rules/urls.ts
319
- var urlRules;
328
+ function redactQueryLike(segment) {
329
+ if (segment.length < 2) return segment;
330
+ const prefix = segment[0];
331
+ const raw = segment.slice(1);
332
+ if (!raw.includes("=")) return segment;
333
+ const parts = raw.split("&");
334
+ const redacted = parts.map((p) => {
335
+ const eq = p.indexOf("=");
336
+ if (eq === -1) return p;
337
+ const key = p.slice(0, eq);
338
+ const value = p.slice(eq + 1);
339
+ const normalized = key.trim().toLowerCase();
340
+ if (!SENSITIVE_PARAM_KEYS.has(normalized)) return p;
341
+ if (value.length === 0) return `${key}=`;
342
+ return `${key}=<REDACTED_URL_PARAM>`;
343
+ });
344
+ return `${prefix}${redacted.join("&")}`;
345
+ }
346
+ function redactUrl(match) {
347
+ const schemeIdx = match.indexOf("://");
348
+ if (schemeIdx === -1) return match;
349
+ const scheme = match.slice(0, schemeIdx + 3);
350
+ const rest = match.slice(schemeIdx + 3);
351
+ const authorityEnd = (() => {
352
+ const slash = rest.indexOf("/");
353
+ const q = rest.indexOf("?");
354
+ const h = rest.indexOf("#");
355
+ const candidates = [slash, q, h].filter((i) => i !== -1);
356
+ return candidates.length === 0 ? rest.length : Math.min(...candidates);
357
+ })();
358
+ let authority = rest.slice(0, authorityEnd);
359
+ let tail = rest.slice(authorityEnd);
360
+ const at = authority.lastIndexOf("@");
361
+ if (at !== -1) {
362
+ const userinfo = authority.slice(0, at);
363
+ const host = authority.slice(at + 1);
364
+ const colon = userinfo.indexOf(":");
365
+ if (colon !== -1) {
366
+ const user = userinfo.slice(0, colon);
367
+ authority = `${user}:<REDACTED_PASSWORD>@${host}`;
368
+ } else {
369
+ authority = `<REDACTED_PASSWORD>@${host}`;
370
+ }
371
+ }
372
+ const hashIdx = tail.indexOf("#");
373
+ const queryIdx = tail.indexOf("?");
374
+ if (queryIdx !== -1 && (hashIdx === -1 || queryIdx < hashIdx)) {
375
+ const before = tail.slice(0, queryIdx);
376
+ const after = tail.slice(queryIdx);
377
+ const hashInside = after.indexOf("#");
378
+ if (hashInside === -1) {
379
+ tail = `${before}${redactQueryLike(after)}`;
380
+ } else {
381
+ const qPart = after.slice(0, hashInside);
382
+ const hPart = after.slice(hashInside);
383
+ tail = `${before}${redactQueryLike(qPart)}${redactQueryLike(hPart)}`;
384
+ }
385
+ } else if (hashIdx !== -1) {
386
+ const before = tail.slice(0, hashIdx);
387
+ const hPart = tail.slice(hashIdx);
388
+ tail = `${before}${redactQueryLike(hPart)}`;
389
+ }
390
+ return `${scheme}${authority}${tail}`;
391
+ }
392
+ var SENSITIVE_PARAM_KEYS, urlRules;
320
393
  var init_urls = __esm({
321
394
  "src/rules/urls.ts"() {
395
+ SENSITIVE_PARAM_KEYS = new Set(
396
+ [
397
+ "access_token",
398
+ "token",
399
+ "id_token",
400
+ "refresh_token",
401
+ "auth",
402
+ "authorization",
403
+ "api_key",
404
+ "apikey",
405
+ "api-key",
406
+ "key",
407
+ "secret",
408
+ "password",
409
+ "passwd",
410
+ "signature",
411
+ "sig",
412
+ "session"
413
+ ].map((k) => k.toLowerCase())
414
+ );
322
415
  urlRules = [
323
416
  {
324
417
  name: "URL",
325
- pattern: /\bhttps?:\/\/[^\s/$.?#].[^\s]*\b/gi,
326
- replace: () => "<REDACTED_URL>"
418
+ // Match HTTP(S) URLs, stopping at whitespace.
419
+ // (Conservative: avoids attempting to be a full RFC URL parser.)
420
+ pattern: /\bhttps?:\/\/[^\s]+/gi,
421
+ replace: (match) => redactUrl(match)
327
422
  }
328
423
  ];
329
424
  }
@@ -417,7 +512,7 @@ var { printSummary: printSummary2 } = (init_summary(), __toCommonJS(summary_expo
417
512
  var { sanitizeLog: sanitizeLog2 } = (init_sanitizeLog(), __toCommonJS(sanitizeLog_exports));
418
513
  var rawArgs = process.argv.slice(2).map((arg) => arg === "-h" ? "--help" : arg);
419
514
  function getVersion() {
420
- return true ? "0.4.1" : "unknown";
515
+ return true ? "0.4.2" : "unknown";
421
516
  }
422
517
  function printHelp() {
423
518
  process.stdout.write(`Usage: logshield scan [file]
@@ -438,6 +533,9 @@ Options:
438
533
  --help Show help
439
534
  `);
440
535
  }
536
+ function writeErr(message) {
537
+ process.stderr.write(message);
538
+ }
441
539
  function parseArgs(args) {
442
540
  const flags = /* @__PURE__ */ new Set();
443
541
  const positionals = [];
@@ -495,7 +593,7 @@ async function main() {
495
593
  const { flags, positionals } = parseArgs(rawArgs);
496
594
  const command = positionals[0];
497
595
  if (command !== "scan") {
498
- process.stdout.write("Unknown command\n");
596
+ writeErr("Unknown command\n");
499
597
  process.exit(1);
500
598
  }
501
599
  const file = positionals[1];
@@ -508,15 +606,15 @@ async function main() {
508
606
  const stdinAuto = isStdinPiped();
509
607
  const useStdin = stdinFlag || stdinAuto;
510
608
  if (useStdin && file) {
511
- process.stdout.write("Cannot read from both STDIN and file\n");
609
+ writeErr("Cannot read from both STDIN and file\n");
512
610
  process.exit(1);
513
611
  }
514
612
  if (dryRun && json) {
515
- process.stdout.write("--dry-run cannot be used with --json\n");
613
+ writeErr("--dry-run cannot be used with --json\n");
516
614
  process.exit(1);
517
615
  }
518
616
  if (json && summary) {
519
- process.stdout.write("--summary cannot be used with --json\n");
617
+ writeErr("--summary cannot be used with --json\n");
520
618
  process.exit(1);
521
619
  }
522
620
  try {
@@ -538,8 +636,7 @@ async function main() {
538
636
  }
539
637
  process.exit(0);
540
638
  } catch (err) {
541
- process.stdout.write(err?.message || "Unexpected error");
542
- process.stdout.write("\n");
639
+ writeErr((err?.message || "Unexpected error") + "\n");
543
640
  process.exit(2);
544
641
  }
545
642
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "logshield-cli",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "license": "Apache-2.0",
5
5
  "type": "commonjs",
6
6
  "bin": {