privacy-brush 0.0.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/.github/workflows/typecheck.yml +31 -0
- package/LICENSE +21 -0
- package/README-zh.md +496 -0
- package/README.md +302 -0
- package/biome.json +36 -0
- package/package.json +26 -0
- package/src/cli.mjs +116 -0
- package/src/cli.test.mjs +28 -0
- package/src/index.mjs +162 -0
- package/src/index.test.mjs +161 -0
- package/src/lib/config.mjs +8 -0
- package/src/type.ts +17 -0
- package/test/fixtures/terminal_log.txt +13 -0
- package/tsconfig.json +10 -0
package/README.md
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
<h1 align="center" title="PrivacyBrush">๐ก๏ธ PrivโcyBrโsh ๐๏ธ</h1>
|
|
2
|
+
|
|
3
|
+
> **Terminal Output Masking Tool | Safely Share Logs by Hiding Sensitive Information**
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/privacy-brush)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
[](https://github.com/legend80s/privacy-brush/actions/workflows/typecheck.yml)
|
|
8
|
+
|
|
9
|
+
<p align="center">
|
|
10
|
+
<img src="https://raw.githubusercontent.com/legend80s/privacy-brush/main/docs/demo.gif" alt="PrivacyBrush Demo" width="800">
|
|
11
|
+
</p>
|
|
12
|
+
|
|
13
|
+
## โจ Features
|
|
14
|
+
|
|
15
|
+
- ๐ฏ **Smart Detection** - Auto-detects 20+ sensitive information patterns
|
|
16
|
+
- ๐ง **Highly Configurable** - Custom masking rules and characters
|
|
17
|
+
- โก **High Performance** - Stream processing for large files
|
|
18
|
+
- ๐ก๏ธ **Privacy First** - Local processing only, no data leaves your machine
|
|
19
|
+
- ๐ฆ **Multiple Formats** - CLI, API, Stream, File processing
|
|
20
|
+
- ๐ **Multi-language** - Supports English, Chinese, and other log formats
|
|
21
|
+
- ๐จ **Customizable** - Add your own sensitive patterns
|
|
22
|
+
|
|
23
|
+
## ๐ Quick Start
|
|
24
|
+
|
|
25
|
+
### Basic Usage
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# Direct terminal output processing
|
|
29
|
+
flutter devices | pnpx privacy-brush
|
|
30
|
+
flutter doctor | pnpx privacy-brush
|
|
31
|
+
|
|
32
|
+
# Process files
|
|
33
|
+
privacy-brush input.log -o masked.log
|
|
34
|
+
|
|
35
|
+
# Real-time command output
|
|
36
|
+
echo 'Microsoft Windows [Version 10.0.12345.6785]' | privacy-brush
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### In Your Node.js Project
|
|
40
|
+
|
|
41
|
+
```javascript
|
|
42
|
+
// Or ES Module
|
|
43
|
+
import { PrivacyBrush } from 'privacy-brush';
|
|
44
|
+
|
|
45
|
+
// Create instance
|
|
46
|
+
const brush = new PrivacyBrush();
|
|
47
|
+
|
|
48
|
+
// Process text
|
|
49
|
+
const sensitiveText = `Windows [Version 10.0.12345.1234]
|
|
50
|
+
Chrome 144.0.1234.12
|
|
51
|
+
User IP: 192.123.1.123`;
|
|
52
|
+
|
|
53
|
+
const safeText = brush.maskText(sensitiveText);
|
|
54
|
+
console.log(safeText);
|
|
55
|
+
|
|
56
|
+
// Output:
|
|
57
|
+
// Windows [Version 10.โ.โโโโโ.โโโโ]
|
|
58
|
+
// Chrome 144.โ.โโโโ.โโ
|
|
59
|
+
// User IP: 192.โโโ.โ.โโโ
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## ๐ Examples
|
|
63
|
+
|
|
64
|
+
### Example 1: Process Flutter Output
|
|
65
|
+
|
|
66
|
+
**Original:**
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
โฏ flutter devices
|
|
70
|
+
Found 4 connected devices:
|
|
71
|
+
Windows (desktop) โข windows โข windows-x64 โข Microsoft Windows [Version 10.0.12345.1234]
|
|
72
|
+
Chrome (web) โข chrome โข web-javascript โข Google Chrome 144.0.1234.60
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**After PrivacyBrush:**
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
โฏ flutter devices | privacy-brush
|
|
79
|
+
Found 4 connected devices:
|
|
80
|
+
Windows (desktop) โข windows โข windows-x64 โข Microsoft Windows [Version 10.โ.โโโโโ.โโโโ]
|
|
81
|
+
Chrome (web) โข chrome โข web-javascript โข Google Chrome 144.โ.โโโโ.โโ
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Example 2: Process Node.js Debug Logs
|
|
85
|
+
|
|
86
|
+
```javascript
|
|
87
|
+
const masker = new PrivacyBrush({
|
|
88
|
+
maskChar: '*',
|
|
89
|
+
preserveFirstPart: false
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const debugLog = `
|
|
93
|
+
DEBUG: User login from IP 192.168.1.100
|
|
94
|
+
DEBUG: Session ID: abc123def456
|
|
95
|
+
DEBUG: Browser: Chrome/144.0.1234.60
|
|
96
|
+
DEBUG: OS: Windows 10.0.12345
|
|
97
|
+
`;
|
|
98
|
+
|
|
99
|
+
console.log(masker.mask(debugLog));
|
|
100
|
+
// Output:
|
|
101
|
+
// DEBUG: User login from IP ***.***.*.***
|
|
102
|
+
// DEBUG: Session ID: ************
|
|
103
|
+
// DEBUG: Browser: Chrome/***.*.***.**
|
|
104
|
+
// DEBUG: OS: Windows **.*.*****
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## โ๏ธ Configuration
|
|
108
|
+
|
|
109
|
+
### CLI Options
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
# Basic usage
|
|
113
|
+
privacy-brush [input-file] [options]
|
|
114
|
+
|
|
115
|
+
# Options
|
|
116
|
+
--output, -o <file> Output to file
|
|
117
|
+
--char, -c <char> Mask character (default: โ)
|
|
118
|
+
--preserve-first Keep first part of version numbers
|
|
119
|
+
--strict Strict mode (mask more info)
|
|
120
|
+
--config <file> Use config file
|
|
121
|
+
--list-patterns List all built-in patterns
|
|
122
|
+
--add-pattern <regex> Add custom regex pattern
|
|
123
|
+
--version Show version
|
|
124
|
+
--help Show help
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### JavaScript API Options
|
|
128
|
+
|
|
129
|
+
```javascript
|
|
130
|
+
const masker = new PrivacyBrush({
|
|
131
|
+
// Basic config
|
|
132
|
+
maskChar: 'โ', // Mask character
|
|
133
|
+
preserveFirstPart: true, // Keep first part of versions
|
|
134
|
+
|
|
135
|
+
// Pattern config
|
|
136
|
+
patterns: {
|
|
137
|
+
ipAddress: true,
|
|
138
|
+
macAddress: true,
|
|
139
|
+
email: true,
|
|
140
|
+
phone: true,
|
|
141
|
+
creditCard: true,
|
|
142
|
+
jwtToken: true,
|
|
143
|
+
apiKey: true,
|
|
144
|
+
|
|
145
|
+
osVersion: true,
|
|
146
|
+
browserVersion: true,
|
|
147
|
+
appVersion: true,
|
|
148
|
+
|
|
149
|
+
deviceId: true,
|
|
150
|
+
serialNumber: true,
|
|
151
|
+
|
|
152
|
+
filePaths: false, // Don't mask file paths
|
|
153
|
+
localhost: false // Don't mask localhost
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
// Custom patterns
|
|
157
|
+
customPatterns: [
|
|
158
|
+
{
|
|
159
|
+
name: 'custom-id',
|
|
160
|
+
regex: /ID-\d{6}/g,
|
|
161
|
+
mask: 'ID-******'
|
|
162
|
+
}
|
|
163
|
+
]
|
|
164
|
+
});
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## ๐ง Built-in Patterns
|
|
168
|
+
|
|
169
|
+
PrivacyBrush includes 20+ pre-configured sensitive information patterns:
|
|
170
|
+
|
|
171
|
+
### ๐ Personal Information
|
|
172
|
+
|
|
173
|
+
- Email addresses `user@example.com` โ `***@example.com`
|
|
174
|
+
- Phone numbers `13800138000` โ `138****8000`
|
|
175
|
+
- ID numbers `110101199001011234` โ `110101********1234`
|
|
176
|
+
|
|
177
|
+
### ๐ป Technical Information
|
|
178
|
+
|
|
179
|
+
- IP addresses `192.168.1.100` โ `192.168.*.*`
|
|
180
|
+
- MAC addresses `00:1A:2B:3C:4D:5E` โ `00:**:**:**:**:**`
|
|
181
|
+
- Port numbers `:8080` โ `:****`
|
|
182
|
+
- API keys `sk_live_1234567890` โ `sk_live_********`
|
|
183
|
+
|
|
184
|
+
### ๐ฅ๏ธ System & Browser
|
|
185
|
+
|
|
186
|
+
- Windows versions `10.0.12345.1234` โ `10.โโโ.โโโ.โโโ`
|
|
187
|
+
- Chrome versions `144.0.1234.60` โ `144.โโโ.โโโ.โโโ`
|
|
188
|
+
- Android versions `Android 16` โ `Android โโ`
|
|
189
|
+
|
|
190
|
+
### ๐ข Business Data
|
|
191
|
+
|
|
192
|
+
- Credit cards `4111 1111 1111 1111` โ `4111 **** **** 1111`
|
|
193
|
+
- JWT tokens `eyJhbGciOiJIUzI1...` โ `eyJ********...`
|
|
194
|
+
- Session IDs `session-abc123def456` โ `session-************`
|
|
195
|
+
|
|
196
|
+
## ๐ ๏ธ Advanced Usage
|
|
197
|
+
|
|
198
|
+
### Stream Processing for Large Files
|
|
199
|
+
|
|
200
|
+
```javascript
|
|
201
|
+
const fs = require('fs');
|
|
202
|
+
const { createMaskStream } = require('privacy-brush');
|
|
203
|
+
|
|
204
|
+
const inputStream = fs.createReadStream('huge.log');
|
|
205
|
+
const maskStream = createMaskStream();
|
|
206
|
+
|
|
207
|
+
inputStream
|
|
208
|
+
.pipe(maskStream)
|
|
209
|
+
.pipe(fs.createWriteStream('masked-huge.log'))
|
|
210
|
+
.on('finish', () => {
|
|
211
|
+
console.log('Large file processing completed!');
|
|
212
|
+
});
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Express.js Integration
|
|
216
|
+
|
|
217
|
+
```javascript
|
|
218
|
+
const express = require('express');
|
|
219
|
+
const { PrivacyBrush } = require('privacy-brush');
|
|
220
|
+
const app = express();
|
|
221
|
+
const masker = new PrivacyBrush();
|
|
222
|
+
|
|
223
|
+
// Middleware: auto-mask sensitive info in responses
|
|
224
|
+
app.use((req, res, next) => {
|
|
225
|
+
const originalSend = res.send;
|
|
226
|
+
res.send = function(body) {
|
|
227
|
+
if (typeof body === 'string' && body.includes('sensitive')) {
|
|
228
|
+
body = masker.mask(body);
|
|
229
|
+
}
|
|
230
|
+
originalSend.call(this, body);
|
|
231
|
+
};
|
|
232
|
+
next();
|
|
233
|
+
});
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Git Hook Integration
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
#!/bin/bash
|
|
240
|
+
# .git/hooks/pre-commit
|
|
241
|
+
|
|
242
|
+
for file in $(git diff --cached --name-only | grep -E '\.(log|txt|json)$'); do
|
|
243
|
+
if privacy-brush --check "$file"; then
|
|
244
|
+
echo "โ File $file contains unmasked sensitive information"
|
|
245
|
+
echo "Use: privacy-brush $file -o $file && git add $file"
|
|
246
|
+
exit 1
|
|
247
|
+
fi
|
|
248
|
+
done
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## ๐ Configuration File
|
|
252
|
+
|
|
253
|
+
Create `privacy-brush.config.json`:
|
|
254
|
+
|
|
255
|
+
```json
|
|
256
|
+
{
|
|
257
|
+
"maskChar": "โ",
|
|
258
|
+
"preserveFirstPart": true,
|
|
259
|
+
"patterns": {
|
|
260
|
+
"ipAddress": true,
|
|
261
|
+
"email": true,
|
|
262
|
+
"phone": true,
|
|
263
|
+
"osVersion": true,
|
|
264
|
+
"browserVersion": true
|
|
265
|
+
},
|
|
266
|
+
"customPatterns": [
|
|
267
|
+
{
|
|
268
|
+
"name": "project-api-key",
|
|
269
|
+
"regex": "PROJECT_API_KEY=\\w{32}",
|
|
270
|
+
"mask": "PROJECT_API_KEY=******************************"
|
|
271
|
+
}
|
|
272
|
+
]
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
## ๐ค Contributing
|
|
277
|
+
|
|
278
|
+
We welcome contributions! See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
|
|
279
|
+
|
|
280
|
+
1. Fork the repository
|
|
281
|
+
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
|
282
|
+
3. Commit changes (`git commit -m 'Add amazing feature'`)
|
|
283
|
+
4. Push to branch (`git push origin feature/amazing-feature`)
|
|
284
|
+
5. Open a Pull Request
|
|
285
|
+
|
|
286
|
+
## ๐ License
|
|
287
|
+
|
|
288
|
+
MIT License ยฉ 2024 PrivacyBrush Contributors
|
|
289
|
+
|
|
290
|
+
## ๐ Support
|
|
291
|
+
|
|
292
|
+
- ๐ง Email: <support@privacy-brush.dev>
|
|
293
|
+
- ๐ [Issue Tracker](https://github.com/legend80s/privacy-brush/issues)
|
|
294
|
+
- ๐ฌ [Discussions](https://github.com/legend80s/privacy-brush/discussions)
|
|
295
|
+
- ๐ [Documentation](https://privacy-brush.dev/docs)
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
<p align="center">
|
|
300
|
+
<strong>Share Safely, Start with PrivacyBrush</strong><br>
|
|
301
|
+
<sub>Protect privacy, communicate with confidence</sub>
|
|
302
|
+
</p>
|
package/biome.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://biomejs.dev/schemas/2.0.4/schema.json",
|
|
3
|
+
"vcs": {
|
|
4
|
+
"enabled": false,
|
|
5
|
+
"clientKind": "git",
|
|
6
|
+
"useIgnoreFile": false
|
|
7
|
+
},
|
|
8
|
+
"files": {
|
|
9
|
+
"ignoreUnknown": false
|
|
10
|
+
},
|
|
11
|
+
"formatter": {
|
|
12
|
+
"enabled": true,
|
|
13
|
+
"indentStyle": "space"
|
|
14
|
+
},
|
|
15
|
+
"linter": {
|
|
16
|
+
"enabled": true,
|
|
17
|
+
"rules": {
|
|
18
|
+
"recommended": true
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"javascript": {
|
|
22
|
+
"formatter": {
|
|
23
|
+
"arrowParentheses": "asNeeded",
|
|
24
|
+
"semicolons": "asNeeded",
|
|
25
|
+
"quoteStyle": "double"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"assist": {
|
|
29
|
+
"enabled": true,
|
|
30
|
+
"actions": {
|
|
31
|
+
"source": {
|
|
32
|
+
"organizeImports": "on"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "privacy-brush",
|
|
3
|
+
"version": "0.0.3",
|
|
4
|
+
"description": "Automatically mask sensitive information in terminal outputs and logs. Keep your data safe when sharing.",
|
|
5
|
+
"main": "src/index.mjs",
|
|
6
|
+
"module": "src/index.mjs",
|
|
7
|
+
"bin": "src/cli.mjs",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"test": "node --test",
|
|
10
|
+
"typecheck": "tsgo --noEmit",
|
|
11
|
+
"check": "npm run typecheck && npm run test",
|
|
12
|
+
"========== Publishing ==========": "",
|
|
13
|
+
"pub:patch": "npm version patch",
|
|
14
|
+
"pub:minor": "npm version minor",
|
|
15
|
+
"pub:major": "npm version major",
|
|
16
|
+
"preversion": "git pull && git push origin HEAD && pnpm i && npm run check",
|
|
17
|
+
"postversion": "npm publish && git push origin HEAD && git push --tags",
|
|
18
|
+
"publishd": "npm publish --dry-run"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [],
|
|
21
|
+
"author": "",
|
|
22
|
+
"license": "ISC",
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/node": "^25.0.10"
|
|
25
|
+
}
|
|
26
|
+
}
|
package/src/cli.mjs
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { parseArgs } from "node:util"
|
|
2
|
+
import { PrivacyBrush } from "./index.mjs"
|
|
3
|
+
|
|
4
|
+
// ๆตๅผๅค็๏ผๅฐ stdin ้่ฟ masker ็ Transform ๆตๅค็ๅ่พๅบๅฐ stdout
|
|
5
|
+
// Usage: some_command | node src/cli.mjs
|
|
6
|
+
// Example 1: echo "password" | node src/cli.mjs
|
|
7
|
+
// Example 2: flutter devices | node src/cli.mjs
|
|
8
|
+
// Example 3: โฏ echo 'Microsoft Windows [็ๆฌ 10.0.12345.6785]' | node src/cli.mjs
|
|
9
|
+
// => Microsoft Windows [็ๆฌ 10.โ.โโโโโ.โโโโ]
|
|
10
|
+
// Example 4: โฏ node src/cli.mjs
|
|
11
|
+
// Input: Microsoft Windows [็ๆฌ 10.0.12345.6785]
|
|
12
|
+
// Output: Microsoft Windows [็ๆฌ 10.โ.โโโโโ.โโโโ]
|
|
13
|
+
|
|
14
|
+
// get config from command line arguments use native `parseArgs`
|
|
15
|
+
// node src/cli.mjs --mask X --preserve-first false
|
|
16
|
+
const args = process.argv.slice(2)
|
|
17
|
+
// console.log("args:", args) // args: [ '--mask', 'X', '--preserve-first', 'false' ]
|
|
18
|
+
|
|
19
|
+
const verbose = args.includes("--verbose")
|
|
20
|
+
|
|
21
|
+
const [err, result] = safeCall(() =>
|
|
22
|
+
parseArgs({
|
|
23
|
+
allowPositionals: true,
|
|
24
|
+
allowNegative: true,
|
|
25
|
+
|
|
26
|
+
args,
|
|
27
|
+
|
|
28
|
+
options: {
|
|
29
|
+
mask: {
|
|
30
|
+
type: "string",
|
|
31
|
+
short: "m",
|
|
32
|
+
default: "โ",
|
|
33
|
+
},
|
|
34
|
+
"preserve-first": {
|
|
35
|
+
type: "boolean",
|
|
36
|
+
short: "p",
|
|
37
|
+
default: true,
|
|
38
|
+
},
|
|
39
|
+
// help
|
|
40
|
+
help: {
|
|
41
|
+
type: "boolean",
|
|
42
|
+
short: "h",
|
|
43
|
+
},
|
|
44
|
+
// verbose
|
|
45
|
+
verbose: {
|
|
46
|
+
type: "boolean",
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
}),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
await main()
|
|
53
|
+
|
|
54
|
+
async function main() {
|
|
55
|
+
if (err) {
|
|
56
|
+
console.error(verbose ? err : String(err))
|
|
57
|
+
console.error()
|
|
58
|
+
printHelp()
|
|
59
|
+
|
|
60
|
+
process.exit(1)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const { values } = result
|
|
64
|
+
|
|
65
|
+
if (values.help) {
|
|
66
|
+
printHelp()
|
|
67
|
+
process.exit(0)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// console.log("values:", values)
|
|
71
|
+
// console.log("positionals:", positionals)
|
|
72
|
+
|
|
73
|
+
const config = values
|
|
74
|
+
|
|
75
|
+
const masker = new PrivacyBrush({
|
|
76
|
+
maskChar: config.mask,
|
|
77
|
+
preserveFirstPart: config["preserve-first"],
|
|
78
|
+
})
|
|
79
|
+
const maskStream = await masker.createMaskStream()
|
|
80
|
+
|
|
81
|
+
// ๆฃๆฅ stdin ๆฏๅฆ่ฟๆฅๅฐ็ฎก้๏ผๆๆฐๆฎ่พๅ
ฅ๏ผ
|
|
82
|
+
const isPipedInput = !process.stdin.isTTY
|
|
83
|
+
|
|
84
|
+
// ๅฆๆไธๆฏ็ฎก้่พๅ
ฅ๏ผไบคไบๆจกๅผ๏ผ๏ผๆๆพ็คบๆ็คบ
|
|
85
|
+
if (!isPipedInput) {
|
|
86
|
+
process.stdout.write("Input (Press Ctrl+C to exit...):\n")
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ๅค็ๆๆๆฐๆฎ
|
|
90
|
+
process.stdin.pipe(maskStream).pipe(process.stdout)
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* @template T
|
|
94
|
+
* @param {(...args: any[]) => T} fn
|
|
95
|
+
* @returns {[null, T] | [Error, null]}
|
|
96
|
+
*/
|
|
97
|
+
function safeCall(fn) {
|
|
98
|
+
try {
|
|
99
|
+
const result = fn()
|
|
100
|
+
return [null, result]
|
|
101
|
+
} catch (error) {
|
|
102
|
+
// @ts-expect-error
|
|
103
|
+
return [error, null]
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function printHelp() {
|
|
108
|
+
console.log(`Usage: node src/cli.mjs [options]
|
|
109
|
+
|
|
110
|
+
Options:
|
|
111
|
+
--mask, -m Character to use for masking (default: "โ")
|
|
112
|
+
--preserve-first, -p Whether to preserve the first part of version numbers (default: true, \`--no-preserve-first\` to false)
|
|
113
|
+
--help, -h Show this help message (default: false)
|
|
114
|
+
--verbose Enable verbose output (default: false)
|
|
115
|
+
`)
|
|
116
|
+
}
|
package/src/cli.test.mjs
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// ๆตๅผๅค็
|
|
2
|
+
|
|
3
|
+
import { strict as assert } from "node:assert"
|
|
4
|
+
import { execSync } from "node:child_process"
|
|
5
|
+
import { test } from "node:test"
|
|
6
|
+
|
|
7
|
+
test("โฏ echo 'Microsoft Windows [Version 10.0.12345.6785]' | node src/cli.mjs", async () => {
|
|
8
|
+
const actual = execSync(
|
|
9
|
+
"echo Microsoft Windows [Version 10.0.12345.6785] | node src/cli.mjs",
|
|
10
|
+
).toString("utf8")
|
|
11
|
+
|
|
12
|
+
// console.log(`actual:|${actual}|`)
|
|
13
|
+
|
|
14
|
+
const expected = "Microsoft Windows [Version 10.โ.โโโโโ.โโโโ] \r\n"
|
|
15
|
+
assert.strictEqual(actual, expected)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test("--mask", async () => {
|
|
19
|
+
const actual = execSync(
|
|
20
|
+
'echo Microsoft Windows [Version 10.0.12345.6785] | node src/cli.mjs --mask "๐" --no-preserve-first',
|
|
21
|
+
).toString("utf8")
|
|
22
|
+
|
|
23
|
+
// console.log(`actual:|${actual}|`)
|
|
24
|
+
|
|
25
|
+
const expected =
|
|
26
|
+
"Microsoft Windows [Version ๐๐.๐.๐๐๐๐๐.๐๐๐๐] \r\n"
|
|
27
|
+
assert.strictEqual(actual, expected)
|
|
28
|
+
})
|
package/src/index.mjs
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/** @import { IConfig, IPattern, IPatternName } from "./type.js" */
|
|
2
|
+
|
|
3
|
+
import { defaultConfig } from "./lib/config.mjs"
|
|
4
|
+
|
|
5
|
+
export class PrivacyBrush {
|
|
6
|
+
/**
|
|
7
|
+
* @param {IConfig} [config]
|
|
8
|
+
*/
|
|
9
|
+
constructor(config) {
|
|
10
|
+
this.config = {
|
|
11
|
+
...defaultConfig,
|
|
12
|
+
...config,
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
get defaultSensitivePatterns() {
|
|
17
|
+
/** @type {IPattern[]} */
|
|
18
|
+
const allPatterns = [
|
|
19
|
+
// ๆไฝ็ณป็ป็ๆฌ (Windows 10.0.19045.6456)
|
|
20
|
+
{
|
|
21
|
+
/** @type {IPatternName} */
|
|
22
|
+
name: "windows_version",
|
|
23
|
+
// match both Chinese "็ๆฌ" and English "Version"
|
|
24
|
+
regex: /(\[(?:็ๆฌ|Version)\s+)(\d+\.\d+\.\d+\.\d+)(\])/i,
|
|
25
|
+
/**
|
|
26
|
+
* Handle windows version masking.
|
|
27
|
+
* @param {string} match
|
|
28
|
+
* @param {string} prefix
|
|
29
|
+
* @param {string} version
|
|
30
|
+
* @param {string} suffix
|
|
31
|
+
* @returns {string}
|
|
32
|
+
*/
|
|
33
|
+
replacer: (match, prefix, version, suffix) => {
|
|
34
|
+
return prefix + this.maskVersion(version) + suffix
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
// ๆต่งๅจ็ๆฌ (Chrome 144.0.7559.60)
|
|
39
|
+
{
|
|
40
|
+
/** @type {IPatternName} */
|
|
41
|
+
name: "browser_version",
|
|
42
|
+
regex: /(Chrome|Edge)\s+(\d+\.\d+\.\d+\.\d+)/gi,
|
|
43
|
+
/**
|
|
44
|
+
* Handle browser version masking.
|
|
45
|
+
* @param {string} match
|
|
46
|
+
* @param {string} browser
|
|
47
|
+
* @param {string} version
|
|
48
|
+
* @returns {string}
|
|
49
|
+
*/
|
|
50
|
+
replacer: (match, browser, version) => {
|
|
51
|
+
return `${browser} ${this.maskVersion(version)}`
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
// IPๅฐๅ
|
|
56
|
+
{
|
|
57
|
+
/** @type {IPatternName} */
|
|
58
|
+
name: "ip_address",
|
|
59
|
+
regex: /\b(\d{1,3}\.){3}\d{1,3}\b/g,
|
|
60
|
+
/**
|
|
61
|
+
* Handle IP address masking.
|
|
62
|
+
* @param {string} match
|
|
63
|
+
* @returns {string}
|
|
64
|
+
*/
|
|
65
|
+
replacer: match => {
|
|
66
|
+
return this.maskVersion(match)
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
return allPatterns
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
get maskChar() {
|
|
75
|
+
return this.config.maskChar ?? defaultConfig.maskChar
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
*
|
|
80
|
+
* @returns {IPattern[]}
|
|
81
|
+
*/
|
|
82
|
+
get sensitivePatterns() {
|
|
83
|
+
return this.defaultSensitivePatterns.filter(({ name }) =>
|
|
84
|
+
this.config.maskPatternNames?.includes(name),
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Mask a version string.
|
|
90
|
+
* @param {string} version
|
|
91
|
+
* @returns {string}
|
|
92
|
+
*/
|
|
93
|
+
maskVersion(version) {
|
|
94
|
+
const parts = version.split(".")
|
|
95
|
+
|
|
96
|
+
if (!this.config.preserveFirstPart) {
|
|
97
|
+
return parts.map(part => this.maskChar.repeat(part.length)).join(".")
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return parts
|
|
101
|
+
.map((part, index) => {
|
|
102
|
+
return index === 0 ? part : this.maskChar.repeat(part.length)
|
|
103
|
+
})
|
|
104
|
+
.join(".")
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
*
|
|
109
|
+
* @param {string} text
|
|
110
|
+
* @returns
|
|
111
|
+
*/
|
|
112
|
+
maskText(text) {
|
|
113
|
+
let result = text
|
|
114
|
+
|
|
115
|
+
this.sensitivePatterns.forEach(pattern => {
|
|
116
|
+
result = result.replace(pattern.regex, pattern.replacer)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
return result
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* ๆน้ๅค็ๆไปถ
|
|
124
|
+
* @param {string} inputPath
|
|
125
|
+
* @param {string} outputPath
|
|
126
|
+
* @returns
|
|
127
|
+
*/
|
|
128
|
+
maskFile(inputPath, outputPath) {
|
|
129
|
+
try {
|
|
130
|
+
const fs = require("node:fs")
|
|
131
|
+
|
|
132
|
+
const content = fs.readFileSync(inputPath, "utf8")
|
|
133
|
+
const maskedContent = this.maskText(content)
|
|
134
|
+
|
|
135
|
+
if (outputPath) {
|
|
136
|
+
fs.writeFileSync(outputPath, maskedContent, "utf8")
|
|
137
|
+
console.log(`Masked file saved to: ${outputPath}`)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return maskedContent
|
|
141
|
+
} catch (error) {
|
|
142
|
+
console.error(
|
|
143
|
+
"Error processing file:",
|
|
144
|
+
error instanceof Error ? error.message : String(error),
|
|
145
|
+
)
|
|
146
|
+
throw error
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ๅฎๆถๆตๅค็
|
|
151
|
+
async createMaskStream() {
|
|
152
|
+
const { Transform } = await import("node:stream")
|
|
153
|
+
|
|
154
|
+
return new Transform({
|
|
155
|
+
transform: (chunk, encoding, callback) => {
|
|
156
|
+
const text = String(chunk)
|
|
157
|
+
const masked = this.maskText(text)
|
|
158
|
+
callback(null, masked)
|
|
159
|
+
},
|
|
160
|
+
})
|
|
161
|
+
}
|
|
162
|
+
}
|