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 +15 -0
- package/README.md +309 -29
- package/dist/cli/index.cjs +222 -50
- package/package.json +29 -21
- package/dist/index.html +0 -16
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
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/logshield-cli)
|
|
6
|
+
[](https://www.npmjs.com/package/logshield-cli)
|
|
7
|
+
[](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
|
-
|
|
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
|
-
|
|
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
|
|
162
|
+
### Scan from STDIN (recommended for CI)
|
|
26
163
|
|
|
27
164
|
```bash
|
|
28
165
|
cat app.log | logshield scan
|
|
29
166
|
```
|
|
30
167
|
|
|
31
|
-
|
|
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
|
|
212
|
+
logshield scan --dry-run --fail-on-detect < logs.txt
|
|
35
213
|
```
|
|
36
214
|
|
|
37
|
-
|
|
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
|
|
254
|
+
cat app.log | logshield scan > sanitized.log
|
|
41
255
|
```
|
|
42
256
|
|
|
43
|
-
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## Strict mode
|
|
260
|
+
|
|
261
|
+
Enable more aggressive detection rules:
|
|
44
262
|
|
|
45
263
|
```bash
|
|
46
|
-
logshield scan
|
|
264
|
+
logshield scan --strict < logs.txt
|
|
47
265
|
```
|
|
48
266
|
|
|
49
267
|
---
|
|
50
268
|
|
|
51
|
-
##
|
|
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
|
-
-
|
|
56
|
-
-
|
|
57
|
-
-
|
|
58
|
-
-
|
|
59
|
-
-
|
|
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
|
-
##
|
|
344
|
+
## Guarantees
|
|
345
|
+
|
|
346
|
+
LogShield guarantees:
|
|
80
347
|
|
|
81
348
|
- Deterministic output
|
|
82
|
-
-
|
|
83
|
-
-
|
|
84
|
-
-
|
|
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
|
-
##
|
|
357
|
+
## Non-goals
|
|
90
358
|
|
|
91
|
-
|
|
92
|
-
|
|
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
|
|
package/dist/cli/index.cjs
CHANGED
|
@@ -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
|
|
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.
|
|
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
|
|
390
|
-
--
|
|
391
|
-
--
|
|
392
|
-
--
|
|
393
|
-
--
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
437
|
-
process.
|
|
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.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"
|
|
19
|
-
"
|
|
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>
|