logshield-cli 0.4.2 → 0.4.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/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.4.3
4
+
5
+ ### Fixed
6
+
7
+ - Prevented API key redaction from corrupting header names (`x-api-key`)
8
+ - Preserved key labels when redacting `api_key=...` values
9
+ - Corrected CLI exit code for invalid flag combinations (`--json --dry-run` now exits with code 2)
10
+
11
+ ### Improved
12
+
13
+ - Deterministic and aligned `--summary` output (alphabetical, indented)
14
+ - Hardened CLI behavior with end-to-end golden tests
15
+ - Strengthened regression coverage for rule overlap and precedence
16
+
17
+ ### Notes
18
+
19
+ - No breaking changes
20
+ - No new features
21
+ - Hardening and correctness release
22
+
3
23
  ## v0.4.2
4
24
 
5
25
  ### Fixed
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  [![npm downloads](https://img.shields.io/npm/dm/logshield-cli)](https://www.npmjs.com/package/logshield-cli)
5
5
  [![CI](https://github.com/afria85/LogShield/actions/workflows/ci.yml/badge.svg)](https://github.com/afria85/LogShield/actions/workflows/ci.yml)
6
6
 
7
- Your logs already contain secrets. You just dont see them.
7
+ Your logs already contain secrets. You just don't see them.
8
8
 
9
9
  LogShield is a small CLI that automatically redacts secrets from logs **before**
10
10
  you paste them into CI, GitHub issues, Slack, or send them to third-party support.
@@ -55,22 +55,28 @@ Use LogShield whenever logs leave your system:
55
55
 
56
56
  ```bash
57
57
  # Preview what would be redacted (does not modify output)
58
- echo "email=test@example.com token=sk_live_123" | logshield scan --dry-run
58
+ echo "email=test@example.com Authorization: Bearer abcdefghijklmnop" | logshield scan --dry-run
59
59
  ```
60
60
 
61
61
  ```
62
62
  logshield (dry-run)
63
63
  Detected 2 redactions:
64
- EMAIL x1
65
- STRIPE_SECRET_KEY x1
64
+ AUTH_BEARER x1
65
+ EMAIL x1
66
66
 
67
67
  No output was modified.
68
68
  Use without --dry-run to apply.
69
69
  ```
70
70
 
71
+ Notes:
72
+
73
+ - The report is printed to stdout
74
+ - No log content is echoed
75
+ - Output is deterministic and CI-safe
76
+
71
77
  ```bash
72
78
  # Enforce redaction (sanitized output)
73
- echo "email=test@example.com token=sk_live_123" | logshield scan
79
+ echo "email=test@example.com Authorization: Bearer abcdefghijklmnop" | logshield scan
74
80
  ```
75
81
 
76
82
  - Prefer `--dry-run` first in CI to verify you are not over-redacting.
@@ -140,8 +146,8 @@ Examples:
140
146
 
141
147
  ```
142
148
  <REDACTED_PASSWORD>
143
- <REDACTED_API_KEY_HEADER>
144
- <REDACTED_AUTH_BEARER>
149
+ <REDACTED_API_KEY>
150
+ <REDACTED_TOKEN>
145
151
  <REDACTED_EMAIL>
146
152
  ```
147
153
 
@@ -247,8 +253,8 @@ cat app.log | logshield scan --dry-run
247
253
 
248
254
  ```
249
255
  logshield (dry-run)
250
- Detected 5 redactions:
251
- AUTH_BEARER x2
256
+ Detected 4 redactions:
257
+ AUTH_BEARER x1
252
258
  EMAIL x1
253
259
  OAUTH_ACCESS_TOKEN x1
254
260
  PASSWORD x1
@@ -345,10 +351,16 @@ Example:
345
351
 
346
352
  ```
347
353
  LogShield Summary
348
- PASSWORD: 2
349
- API_KEY_HEADER: 1
354
+ API_KEY_HEADER: 1
355
+ PASSWORD: 2
350
356
  ```
351
357
 
358
+ Notes:
359
+
360
+ - Sanitized log output is written to stdout
361
+ - The summary is written to stderr
362
+ - Rules are sorted alphabetically
363
+
352
364
  ---
353
365
 
354
366
  ## JSON output
@@ -362,7 +374,8 @@ logshield scan --json < logs.txt
362
374
  Notes:
363
375
 
364
376
  - `--json` **cannot** be combined with `--dry-run`
365
- - Output schema is stable within v0.4.x
377
+ - Usage errors exit with code `2`
378
+ - Output is always newline-terminated
366
379
 
367
380
  ---
368
381
 
@@ -87,33 +87,25 @@ __export(summary_exports, {
87
87
  printSummary: () => printSummary
88
88
  });
89
89
  function printSummary(matches) {
90
- if (!matches || matches.length === 0) {
91
- process.stderr.write("logshield: no redactions detected\n");
90
+ if (!matches.length) {
91
+ process.stderr.write("LogShield Summary\n(no redactions detected)\n");
92
92
  return;
93
93
  }
94
- const counter = {};
94
+ const counts = {};
95
95
  for (const m of matches) {
96
- counter[m.rule] = (counter[m.rule] || 0) + 1;
96
+ counts[m.rule] = (counts[m.rule] ?? 0) + 1;
97
97
  }
98
- const entries = Object.entries(counter).map(([rule, count]) => ({ rule, count })).sort((a, b) => {
99
- if (b.count !== a.count) return b.count - a.count;
100
- return a.rule.localeCompare(b.rule);
101
- });
102
- const maxLen = Math.max(...entries.map((e) => e.rule.length));
103
- const total = matches.length;
104
- const label = total === 1 ? "redaction" : "redactions";
105
- process.stderr.write(`logshield summary: ${total} ${label}
98
+ const rules = Object.keys(counts).sort((a, b) => a.localeCompare(b));
99
+ const maxNameLen = Math.max(...rules.map((r) => r.length));
100
+ process.stderr.write("LogShield Summary\n");
101
+ for (const rule of rules) {
102
+ const padded = rule.padEnd(maxNameLen, " ");
103
+ process.stderr.write(` ${padded} x${counts[rule]}
106
104
  `);
107
- for (const { rule, count } of entries) {
108
- process.stderr.write(
109
- ` ${rule.padEnd(maxLen)} x${count}
110
- `
111
- );
112
105
  }
113
106
  }
114
107
  var init_summary = __esm({
115
108
  "src/cli/summary.ts"() {
116
- "use strict";
117
109
  }
118
110
  });
119
111
 
@@ -217,6 +209,15 @@ var init_credentials = __esm({
217
209
  pattern: /\b(postgres|mysql|mongodb):\/\/([^:\s]+):([^@\s]+)@/gi,
218
210
  replace: (_match, _ctx, groups) => `${groups[0]}://${groups[1]}:<REDACTED_PASSWORD>@`
219
211
  },
212
+ // x-api-key: ....
213
+ // IMPORTANT: this must run BEFORE the generic API_KEY rule. Otherwise the
214
+ // generic API_KEY rule can match the "api-key: <value>" substring first and
215
+ // corrupt the header name (e.g. "x-api-key" -> "x-").
216
+ {
217
+ name: "API_KEY_HEADER",
218
+ pattern: /\bx-api-key\s*:\s*["']?[A-Za-z0-9_\-]{16,}["']?\b/gi,
219
+ replace: () => "x-api-key: <REDACTED_API_KEY>"
220
+ },
220
221
  /**
221
222
  * API key (common variants):
222
223
  * - apiKey=...
@@ -224,23 +225,19 @@ var init_credentials = __esm({
224
225
  * - api-key: ...
225
226
  * - apikey=...
226
227
  * Supports '=' or ':' and optional quotes/spaces.
228
+ *
229
+ * NOTE:
230
+ * Do NOT try to handle "Authorization: Bearer ..." here; that causes overlap
231
+ * with token rules. Token redaction is handled in tokens.ts.
227
232
  */
228
233
  {
229
234
  name: "API_KEY",
230
- pattern: /\bapi(?:[_-]?key)\s*[:=]\s*["']?([A-Za-z0-9_\-]{16,})["']?\b/gi,
231
- replace: () => "<REDACTED_API_KEY>"
232
- },
233
- // x-api-key: ....
234
- {
235
- name: "API_KEY_HEADER",
236
- pattern: /\bx-api-key\s*:\s*["']?[A-Za-z0-9_\-]{16,}["']?\b/gi,
237
- replace: () => "x-api-key: <REDACTED_API_KEY>"
238
- },
239
- // authorization: Bearer ...
240
- {
241
- name: "AUTHORIZATION_BEARER",
242
- pattern: /\bauthorization\s*:\s*bearer\s+([A-Za-z0-9._\-]{16,})\b/gi,
243
- replace: () => "authorization: Bearer <REDACTED_TOKEN>"
235
+ // Preserve the key label + delimiter, redact only the value.
236
+ // Examples:
237
+ // api_key=abcdef... -> api_key=<REDACTED_API_KEY>
238
+ // api-key: "abcdef..." -> api-key: "<REDACTED_API_KEY>"
239
+ pattern: /\b(api(?:[_-]?key)\s*[:=]\s*["']?)([A-Za-z0-9_\-]{16,})(["']?)\b/gi,
240
+ replace: (_match, _ctx, groups) => `${groups[0]}<REDACTED_API_KEY>${groups[2]}`
244
241
  }
245
242
  ];
246
243
  }
@@ -512,7 +509,7 @@ var { printSummary: printSummary2 } = (init_summary(), __toCommonJS(summary_expo
512
509
  var { sanitizeLog: sanitizeLog2 } = (init_sanitizeLog(), __toCommonJS(sanitizeLog_exports));
513
510
  var rawArgs = process.argv.slice(2).map((arg) => arg === "-h" ? "--help" : arg);
514
511
  function getVersion() {
515
- return true ? "0.4.2" : "unknown";
512
+ return true ? "0.4.3" : "unknown";
516
513
  }
517
514
  function printHelp() {
518
515
  process.stdout.write(`Usage: logshield scan [file]
@@ -580,6 +577,10 @@ function renderDryRunReport(matches) {
580
577
  process.stdout.write("No output was modified.\n");
581
578
  process.stdout.write("Use without --dry-run to apply.\n");
582
579
  }
580
+ function exitUsageError(message) {
581
+ writeErr(message.endsWith("\n") ? message : message + "\n");
582
+ process.exit(2);
583
+ }
583
584
  async function main() {
584
585
  if (rawArgs.length === 0 || rawArgs.includes("--help")) {
585
586
  printHelp();
@@ -593,8 +594,7 @@ async function main() {
593
594
  const { flags, positionals } = parseArgs(rawArgs);
594
595
  const command = positionals[0];
595
596
  if (command !== "scan") {
596
- writeErr("Unknown command\n");
597
- process.exit(1);
597
+ exitUsageError("Unknown command");
598
598
  }
599
599
  const file = positionals[1];
600
600
  const strict = flags.has("--strict");
@@ -606,16 +606,13 @@ async function main() {
606
606
  const stdinAuto = isStdinPiped();
607
607
  const useStdin = stdinFlag || stdinAuto;
608
608
  if (useStdin && file) {
609
- writeErr("Cannot read from both STDIN and file\n");
610
- process.exit(1);
609
+ exitUsageError("Cannot read from both STDIN and file");
611
610
  }
612
611
  if (dryRun && json) {
613
- writeErr("--dry-run cannot be used with --json\n");
614
- process.exit(1);
612
+ exitUsageError("--dry-run cannot be used with --json");
615
613
  }
616
614
  if (json && summary) {
617
- writeErr("--summary cannot be used with --json\n");
618
- process.exit(1);
615
+ exitUsageError("--summary cannot be used with --json");
619
616
  }
620
617
  try {
621
618
  const input = await readInput2(useStdin ? void 0 : file);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "logshield-cli",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "license": "Apache-2.0",
5
5
  "type": "commonjs",
6
6
  "bin": {
@@ -20,7 +20,8 @@
20
20
  "typecheck": "tsc -p tsconfig.core.json && tsc -p tsconfig.cli.json --noEmit",
21
21
  "pretest": "npm run build",
22
22
  "test": "vitest",
23
- "prepublishOnly": "npm run build"
23
+ "prepublish:check": "npm run typecheck && npm test",
24
+ "prepublishOnly": "npm run prepublish:check"
24
25
  },
25
26
  "devDependencies": {
26
27
  "@types/node": "^25.0.3",