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 +11 -0
- package/README.md +60 -19
- package/dist/cli/index.cjs +112 -15
- package/package.json +1 -1
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
|
[](https://www.npmjs.com/package/logshield-cli)
|
|
5
4
|
[](https://www.npmjs.com/package/logshield-cli)
|
|
6
5
|
[](https://github.com/afria85/LogShield/actions/workflows/ci.yml)
|
|
7
6
|
|
|
8
|
-
|
|
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
|
-
#
|
|
14
|
-
|
|
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**
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
---
|
package/dist/cli/index.cjs
CHANGED
|
@@ -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
|
-
|
|
188
|
-
|
|
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
|
-
|
|
203
|
-
|
|
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
|
-
|
|
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
|
-
|
|
326
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
609
|
+
writeErr("Cannot read from both STDIN and file\n");
|
|
512
610
|
process.exit(1);
|
|
513
611
|
}
|
|
514
612
|
if (dryRun && json) {
|
|
515
|
-
|
|
613
|
+
writeErr("--dry-run cannot be used with --json\n");
|
|
516
614
|
process.exit(1);
|
|
517
615
|
}
|
|
518
616
|
if (json && summary) {
|
|
519
|
-
|
|
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
|
-
|
|
542
|
-
process.stdout.write("\n");
|
|
639
|
+
writeErr((err?.message || "Unexpected error") + "\n");
|
|
543
640
|
process.exit(2);
|
|
544
641
|
}
|
|
545
642
|
}
|