privacy-brush 0.0.4 → 1.0.0
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/.editorconfig +19 -0
- package/package.json +1 -1
- package/src/cli.mjs +154 -154
- package/src/cli.test.mjs +160 -28
- package/src/index.mjs +239 -162
- package/src/lib/config.mjs +9 -8
- package/src/lib/parse-args.mjs +75 -0
- package/src/type.ts +19 -17
- package/test/fixtures/{terminal_log.txt → terminal_log.md} +24 -13
package/.editorconfig
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# http://editorconfig.org
|
|
2
|
+
root = true
|
|
3
|
+
|
|
4
|
+
[*]
|
|
5
|
+
indent_style = space
|
|
6
|
+
indent_size = 2
|
|
7
|
+
end_of_line = lf
|
|
8
|
+
charset = utf-8
|
|
9
|
+
trim_trailing_whitespace = true
|
|
10
|
+
insert_final_newline = true
|
|
11
|
+
|
|
12
|
+
[*.md]
|
|
13
|
+
trim_trailing_whitespace = false
|
|
14
|
+
|
|
15
|
+
[Makefile]
|
|
16
|
+
indent_style = tab
|
|
17
|
+
|
|
18
|
+
[.nvmrc]
|
|
19
|
+
insert_final_newline = false
|
package/package.json
CHANGED
package/src/cli.mjs
CHANGED
|
@@ -1,154 +1,154 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
// 检查 stdin 是否连接到管道(有数据输入)
|
|
93
|
-
const isPipedInput = !process.stdin.isTTY
|
|
94
|
-
|
|
95
|
-
// 如果不是管道输入(交互模式),才显示提示
|
|
96
|
-
if (!isPipedInput) {
|
|
97
|
-
process.stdout.write("Input (Press Ctrl+C to exit...):\n")
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
`)
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
async function parsePackageJSON() {
|
|
138
|
-
const fs = await import("node:fs")
|
|
139
|
-
const pkgPath = new URL("../package.json", import.meta.url)
|
|
140
|
-
|
|
141
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"))
|
|
142
|
-
|
|
143
|
-
return pkg
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
async function getNameVersion() {
|
|
147
|
-
const pkg = await parsePackageJSON()
|
|
148
|
-
|
|
149
|
-
return `${pkg.name}@${pkg.version}`
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
async function printVersion() {
|
|
153
|
-
console.log(await getNameVersion())
|
|
154
|
-
}
|
|
1
|
+
import { PrivacyBrush } from "./index.mjs"
|
|
2
|
+
import { parsedResult, verbose } from "./lib/parse-args.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
|
+
await main()
|
|
15
|
+
|
|
16
|
+
async function main() {
|
|
17
|
+
const [err, result] = parsedResult
|
|
18
|
+
|
|
19
|
+
if (err) {
|
|
20
|
+
console.error(verbose ? err : String(err))
|
|
21
|
+
console.error()
|
|
22
|
+
await printHelp()
|
|
23
|
+
|
|
24
|
+
process.exit(1)
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const { values } = result
|
|
29
|
+
|
|
30
|
+
if (values.help) {
|
|
31
|
+
await printHelp()
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (values.version) {
|
|
36
|
+
await printVersion()
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
verbose && console.log("values:", values)
|
|
41
|
+
// console.log("positionals:", positionals)
|
|
42
|
+
|
|
43
|
+
const config = values
|
|
44
|
+
|
|
45
|
+
const masker = new PrivacyBrush({
|
|
46
|
+
maskChar: config.mask,
|
|
47
|
+
preserveFirstPart: config["preserve-first"],
|
|
48
|
+
customPatterns: config.pattern,
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
// If an input file is provided, process it. If --output-file is provided write there, otherwise print to stdout.
|
|
52
|
+
if (config["input-file"]) {
|
|
53
|
+
try {
|
|
54
|
+
if (config["output-file"]) {
|
|
55
|
+
// maskFile will write to the output path
|
|
56
|
+
masker.maskFile(config["input-file"], config["output-file"])
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const masked = masker.maskFile(config["input-file"])
|
|
61
|
+
|
|
62
|
+
process.stdout.write(masked)
|
|
63
|
+
return
|
|
64
|
+
} catch {
|
|
65
|
+
process.exitCode = 1
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// If an output file was requested for piped or interactive stdin, collect all input, mask and write to file.
|
|
71
|
+
if (config["output-file"]) {
|
|
72
|
+
try {
|
|
73
|
+
const chunks = []
|
|
74
|
+
for await (const chunk of process.stdin) {
|
|
75
|
+
chunks.push(String(chunk))
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const inputText = chunks.join("")
|
|
79
|
+
console.log(`inputText: |${inputText}|`)
|
|
80
|
+
const masked = masker.maskText(inputText)
|
|
81
|
+
|
|
82
|
+
const fs = await import("node:fs/promises")
|
|
83
|
+
await fs.writeFile(config["output-file"], masked, "utf8")
|
|
84
|
+
return
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.error(error)
|
|
87
|
+
process.exitCode = 1
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 检查 stdin 是否连接到管道(有数据输入)
|
|
93
|
+
const isPipedInput = !process.stdin.isTTY
|
|
94
|
+
|
|
95
|
+
// 如果不是管道输入(交互模式),才显示提示
|
|
96
|
+
if (!isPipedInput) {
|
|
97
|
+
process.stdout.write("Input (Press Ctrl+C to exit...):\n")
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const maskStream = await masker.createMaskStream()
|
|
101
|
+
|
|
102
|
+
// Default: stream masking to stdout
|
|
103
|
+
process.stdin.pipe(maskStream).pipe(process.stdout)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function printHelp() {
|
|
107
|
+
console.log(`
|
|
108
|
+
# ${await getNameVersion()}
|
|
109
|
+
|
|
110
|
+
## Usage:
|
|
111
|
+
|
|
112
|
+
pnpx privacy-brush [options]
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
## Options:
|
|
116
|
+
--input-file, -i Path to input file to mask and print to stdout
|
|
117
|
+
--output-file, -o Path to write masked output (if given, write to this file)
|
|
118
|
+
--mask, -m Character to use for masking (default: "█")
|
|
119
|
+
--preserve-first, -p Whether to preserve the first part of version numbers (default: true, \`--no-preserve-first\` to false)
|
|
120
|
+
--help, -h Show this help message (default: false)
|
|
121
|
+
--verbose Enable verbose output (default: false)
|
|
122
|
+
--version, -v Show version information (default: false)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
## Examples:
|
|
126
|
+
|
|
127
|
+
flutter devices | pnpx privacy-brush
|
|
128
|
+
|
|
129
|
+
echo "Microsoft Windows [Version 10.0.12345.6785]" | pnpx privacy-brush
|
|
130
|
+
echo "Microsoft Windows [Version 10.0.12345.6785]" | pnpx privacy-brush --mask "X" --no-preserve-first
|
|
131
|
+
|
|
132
|
+
pnpx privacy-brush --input-file test/fixtures/terminal_log.md # mask and print to stdout
|
|
133
|
+
pnpx privacy-brush --input-file test/fixtures/terminal_log.md --output-file masked_log.md
|
|
134
|
+
`)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function parsePackageJSON() {
|
|
138
|
+
const fs = await import("node:fs")
|
|
139
|
+
const pkgPath = new URL("../package.json", import.meta.url)
|
|
140
|
+
|
|
141
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"))
|
|
142
|
+
|
|
143
|
+
return pkg
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function getNameVersion() {
|
|
147
|
+
const pkg = await parsePackageJSON()
|
|
148
|
+
|
|
149
|
+
return `${pkg.name}@${pkg.version}`
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function printVersion() {
|
|
153
|
+
console.log(await getNameVersion())
|
|
154
|
+
}
|
package/src/cli.test.mjs
CHANGED
|
@@ -1,28 +1,160 @@
|
|
|
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
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
1
|
+
// 流式处理
|
|
2
|
+
|
|
3
|
+
import { strict as assert } from "node:assert"
|
|
4
|
+
import { execSync, spawnSync } from "node:child_process"
|
|
5
|
+
import { test } from "node:test"
|
|
6
|
+
|
|
7
|
+
test("❯ echo | node src/cli.mjs", async () => {
|
|
8
|
+
const text = "Microsoft Windows [Version 10.0.12345.6785]"
|
|
9
|
+
|
|
10
|
+
// `echo "${text}"` will print text + newline to stdout
|
|
11
|
+
// `echo -n "${text}"` will print -n and text + newline to stdout
|
|
12
|
+
// So we use spawn with args [`-n`, text] to avoid the extra newline
|
|
13
|
+
function pipeCommands() {
|
|
14
|
+
const node = spawnSync("node", ["src/cli.mjs"], {
|
|
15
|
+
input: text,
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
return node.stdout.toString("utf8")
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const actual = pipeCommands()
|
|
22
|
+
|
|
23
|
+
const expected = "Microsoft Windows [Version 10.█.█████.████]"
|
|
24
|
+
assert.strictEqual(actual, expected)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test("--mask", async () => {
|
|
28
|
+
const actual = execSync(
|
|
29
|
+
'echo Microsoft Windows [Version 10.0.12345.6785] | node src/cli.mjs --mask "🔒" --no-preserve-first',
|
|
30
|
+
).toString("utf8")
|
|
31
|
+
|
|
32
|
+
// console.log(`actual:|${actual}|`)
|
|
33
|
+
|
|
34
|
+
const expected =
|
|
35
|
+
"Microsoft Windows [Version 🔒🔒.🔒.🔒🔒🔒🔒🔒.🔒🔒🔒🔒] \r\n"
|
|
36
|
+
assert.strictEqual(actual, expected)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test("--input-file outputs masked file content", async () => {
|
|
40
|
+
const { execSync } = await import("node:child_process")
|
|
41
|
+
|
|
42
|
+
const inputPath = "test/fixtures/terminal_log.md"
|
|
43
|
+
|
|
44
|
+
const actual = execSync(
|
|
45
|
+
`node src/cli.mjs --input-file ${inputPath}`,
|
|
46
|
+
).toString("utf8")
|
|
47
|
+
|
|
48
|
+
// compute expected via masker.maskFile and normalize EOLs
|
|
49
|
+
const expected = `# h1
|
|
50
|
+
|
|
51
|
+
## flutter devices
|
|
52
|
+
|
|
53
|
+
❯ flutter devices
|
|
54
|
+
Flutter assets will be downloaded from <https://storage.flutter-io.cn>. Make sure you trust this source!
|
|
55
|
+
Found 4 connected devices:
|
|
56
|
+
sdk gphone64 x86 64 (mobile) • emulator-5554 • android-x64 • Android 16 (API 36) (emulator)
|
|
57
|
+
Windows (desktop) • windows • windows-x64 • Microsoft Windows [版本 10.█.█████.████]
|
|
58
|
+
Chrome (web) • chrome • web-javascript • Google Chrome 144.█.████.██
|
|
59
|
+
Edge (web) • edge • web-javascript • Microsoft Edge 144.█.████.██
|
|
60
|
+
|
|
61
|
+
Run "flutter emulators" to list and start any available device emulators.
|
|
62
|
+
|
|
63
|
+
If you expected another device to be detected, please run "flutter doctor" to diagnose potential issues. You may also try
|
|
64
|
+
increasing the time to wait for connected devices with the "--device-timeout" flag. Visit <https://flutter.dev/setup/> for
|
|
65
|
+
troubleshooting tips.
|
|
66
|
+
|
|
67
|
+
## log
|
|
68
|
+
|
|
69
|
+
DEBUG: User login from IP 192.███.█.███
|
|
70
|
+
DEBUG: Session ID: abc123def456
|
|
71
|
+
DEBUG: Browser: Chrome/144.0.1234.60
|
|
72
|
+
DEBUG: OS: Windows 10.0.12345
|
|
73
|
+
`
|
|
74
|
+
|
|
75
|
+
assert.strictEqual(actual, expected)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test(`custom patterns`, () => {
|
|
79
|
+
const input = `DEEPSEEK_API_KEY=sk-af75149812524eb08eb302bf9604c8e8`
|
|
80
|
+
|
|
81
|
+
const actual = execSync(
|
|
82
|
+
`echo ${input} | node src/cli.mjs --pattern /sk-([a-z0-9]{20,})/`,
|
|
83
|
+
).toString("utf8")
|
|
84
|
+
|
|
85
|
+
const expected = "DEEPSEEK_API_KEY=sk-████████████████████████████████ \r\n"
|
|
86
|
+
assert.strictEqual(actual, expected)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
test(`custom patterns`, () => {
|
|
90
|
+
const input = `DEEPSEEK_API_KEY=sk-af75149812524eb08eb302bf9604c8e8`
|
|
91
|
+
|
|
92
|
+
const actual = execSync(
|
|
93
|
+
`echo ${input} | node src/cli.mjs --pattern /sk-[a-z0-9]{20,}/`,
|
|
94
|
+
).toString("utf8")
|
|
95
|
+
|
|
96
|
+
const expected = "DEEPSEEK_API_KEY=███████████████████████████████████ \r\n"
|
|
97
|
+
assert.strictEqual(actual, expected)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
test(`custom patterns`, () => {
|
|
101
|
+
const input = `DEEPSEEK_API_KEY=sk-af75149812524eb08eb302bf9604c8e8`
|
|
102
|
+
|
|
103
|
+
const actual = execSync(
|
|
104
|
+
`echo ${input} | node src/cli.mjs --pattern /([0-9]{2,})/`,
|
|
105
|
+
).toString("utf8")
|
|
106
|
+
|
|
107
|
+
const expected = "DEEPSEEK_API_KEY=sk-af███████████eb██eb███bf████c8e8 \r\n"
|
|
108
|
+
assert.strictEqual(actual, expected)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 将两个或多个命令通过管道连接起来执行,并返回最后一个命令的输出结果
|
|
113
|
+
* @param {string} cmdStrWithPipes
|
|
114
|
+
* @returns {string}
|
|
115
|
+
*/
|
|
116
|
+
function pipeCommands(cmdStrWithPipes) {
|
|
117
|
+
// const echo = spawnSync("echo", ["-n", text])
|
|
118
|
+
// const node = spawnSync("node", ["src/cli.mjs"], {
|
|
119
|
+
// input: echo.stdout,
|
|
120
|
+
// })
|
|
121
|
+
// 以下代码是以上代码的泛化版本
|
|
122
|
+
const commands = cmdStrWithPipes
|
|
123
|
+
.split("|")
|
|
124
|
+
.map(cmd => cmd.trim())
|
|
125
|
+
.filter(Boolean)
|
|
126
|
+
|
|
127
|
+
if (commands.length < 2) {
|
|
128
|
+
throw new Error("At least two commands are required for piping.")
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const [first, ...restCommand] = commands
|
|
132
|
+
const [cmdName, args] = splitCmdAndArgs(first)
|
|
133
|
+
|
|
134
|
+
const lastCommand = restCommand.reduce(
|
|
135
|
+
(prevProcess, cmdStr) => {
|
|
136
|
+
const [cmd, args] = splitCmdAndArgs(cmdStr)
|
|
137
|
+
|
|
138
|
+
const currentProcess = spawnSync(cmd, args, {
|
|
139
|
+
input: prevProcess.stdout,
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
return currentProcess
|
|
143
|
+
},
|
|
144
|
+
spawnSync(cmdName, args),
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
return lastCommand.stdout.toString("utf8")
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
*
|
|
152
|
+
* @param {string} cmd
|
|
153
|
+
* @returns {[cmdName: string, args: string[]]}
|
|
154
|
+
*/
|
|
155
|
+
function splitCmdAndArgs(cmd) {
|
|
156
|
+
const [cmdName, ...args] = cmd.split(" ").filter(Boolean)
|
|
157
|
+
// console.log(`cmdName:|${cmdName}|`)
|
|
158
|
+
// console.log("args:", args)
|
|
159
|
+
return [cmdName, args]
|
|
160
|
+
}
|
package/src/index.mjs
CHANGED
|
@@ -1,162 +1,239 @@
|
|
|
1
|
-
/** @import { IConfig, IPattern, IPatternName } from "./type.js" */
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
*
|
|
29
|
-
* @param {string}
|
|
30
|
-
* @param {string}
|
|
31
|
-
* @
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
*
|
|
47
|
-
* @param {string}
|
|
48
|
-
* @
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
*
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
*
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
.
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}
|
|
1
|
+
/** @import { IConfig, IPattern, IPatternName } from "./type.js" */
|
|
2
|
+
|
|
3
|
+
import { createRequire } from "node:module"
|
|
4
|
+
import { defaultConfig } from "./lib/config.mjs"
|
|
5
|
+
import { verbose } from "./lib/parse-args.mjs"
|
|
6
|
+
|
|
7
|
+
export class PrivacyBrush {
|
|
8
|
+
/**
|
|
9
|
+
* @param {IConfig} [config]
|
|
10
|
+
*/
|
|
11
|
+
constructor(config) {
|
|
12
|
+
this.config = {
|
|
13
|
+
...defaultConfig,
|
|
14
|
+
...config,
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
get defaultSensitivePatterns() {
|
|
19
|
+
/** @type {IPattern[]} */
|
|
20
|
+
const allPatterns = [
|
|
21
|
+
// 操作系统版本 (Windows 10.0.19045.6456)
|
|
22
|
+
{
|
|
23
|
+
/** @type {IPatternName} */
|
|
24
|
+
name: "windows_version",
|
|
25
|
+
// match both Chinese "版本" and English "Version"
|
|
26
|
+
regex: /(\[(?:版本|Version)\s+)(\d+\.\d+\.\d+\.\d+)(\])/i,
|
|
27
|
+
/**
|
|
28
|
+
* Handle windows version masking.
|
|
29
|
+
* @param {string} match
|
|
30
|
+
* @param {string} prefix
|
|
31
|
+
* @param {string} version
|
|
32
|
+
* @param {string} suffix
|
|
33
|
+
* @returns {string}
|
|
34
|
+
*/
|
|
35
|
+
replacer: (match, prefix, version, suffix) => {
|
|
36
|
+
return prefix + this.maskVersion(version) + suffix
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
// 浏览器版本 (Chrome 144.0.7559.60)
|
|
41
|
+
{
|
|
42
|
+
/** @type {IPatternName} */
|
|
43
|
+
name: "browser_version",
|
|
44
|
+
regex: /(Chrome|Edge)\s+(\d+\.\d+\.\d+\.\d+)/gi,
|
|
45
|
+
/**
|
|
46
|
+
* Handle browser version masking.
|
|
47
|
+
* @param {string} match
|
|
48
|
+
* @param {string} browser
|
|
49
|
+
* @param {string} version
|
|
50
|
+
* @returns {string}
|
|
51
|
+
*/
|
|
52
|
+
replacer: (match, browser, version) => {
|
|
53
|
+
return `${browser} ${this.maskVersion(version)}`
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
// IP地址
|
|
58
|
+
{
|
|
59
|
+
/** @type {IPatternName} */
|
|
60
|
+
name: "ip_address",
|
|
61
|
+
regex: /\b(\d{1,3}\.){3}\d{1,3}\b/g,
|
|
62
|
+
/**
|
|
63
|
+
* Handle IP address masking.
|
|
64
|
+
* @param {string} match
|
|
65
|
+
* @returns {string}
|
|
66
|
+
*/
|
|
67
|
+
replacer: match => {
|
|
68
|
+
return this.maskVersion(match)
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
return allPatterns
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Parse custom pattern inputs into maskable patterns.
|
|
78
|
+
* Accepts strings like '/sk-[a-z0-9]{20,}/i' or raw pattern bodies.
|
|
79
|
+
* @returns {IPattern[]}
|
|
80
|
+
*/
|
|
81
|
+
get customSensitivePatterns() {
|
|
82
|
+
const customPatterns = this.config.customPatterns || []
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Parse string pattern to RegExp.
|
|
86
|
+
* @param {string | RegExp} pattern
|
|
87
|
+
* @returns {RegExp} returns Parsed RegExp with global flag
|
|
88
|
+
* @example
|
|
89
|
+
* '/\\d{1,}/' => /\d{1,}/g
|
|
90
|
+
*/
|
|
91
|
+
const parse = pattern => {
|
|
92
|
+
if (pattern instanceof RegExp) {
|
|
93
|
+
// always replace globally
|
|
94
|
+
const flags = pattern.flags.includes("g")
|
|
95
|
+
? pattern.flags
|
|
96
|
+
: `${pattern.flags}g`
|
|
97
|
+
|
|
98
|
+
return new RegExp(pattern.source, flags)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const patternStr = pattern.trim()
|
|
102
|
+
// `/\d{1,}/g`.match(/^\/(.*)\/(\w*)$/) => [/\d{1,}/g, '\d{1,}', 'g']
|
|
103
|
+
const matches = patternStr.match(/^\/(.*)\/(\w*)$/)
|
|
104
|
+
|
|
105
|
+
if (matches) {
|
|
106
|
+
const body = matches[1]
|
|
107
|
+
let flags = matches[2] ?? ""
|
|
108
|
+
|
|
109
|
+
if (!flags.includes("g")) flags += "g"
|
|
110
|
+
|
|
111
|
+
return new RegExp(body, flags)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// treat as literal body
|
|
115
|
+
return new RegExp(patternStr, "g")
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return customPatterns.map((pattern, i) => {
|
|
119
|
+
const regex = parse(pattern)
|
|
120
|
+
return {
|
|
121
|
+
name: `custom_${i + 1}`,
|
|
122
|
+
regex,
|
|
123
|
+
replacer: (match, group) => {
|
|
124
|
+
// const groups = rest.slice(0, -2) // last two are offset and input string
|
|
125
|
+
verbose && console.log("custom pattern match:", { match, group })
|
|
126
|
+
|
|
127
|
+
if (typeof group === "string") {
|
|
128
|
+
return match.replace(group, this.maskChar.repeat(group.length))
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return this.maskChar.repeat(match.length)
|
|
132
|
+
},
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
get maskChar() {
|
|
138
|
+
return this.config.maskChar ?? defaultConfig.maskChar
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
*
|
|
143
|
+
* @returns {IPattern[]}
|
|
144
|
+
*/
|
|
145
|
+
get sensitivePatterns() {
|
|
146
|
+
const base = this.defaultSensitivePatterns.filter(({ name }) =>
|
|
147
|
+
this.config.maskPatternNames?.includes(name),
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
const customSensitivePatterns = this.customSensitivePatterns
|
|
151
|
+
verbose &&
|
|
152
|
+
console.log(
|
|
153
|
+
"🚀 ~ PrivacyBrush ~ sensitivePatterns ~ customSensitivePatterns:",
|
|
154
|
+
customSensitivePatterns,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
return base.concat(customSensitivePatterns)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Mask a version string.
|
|
162
|
+
* @param {string} version
|
|
163
|
+
* @returns {string}
|
|
164
|
+
*/
|
|
165
|
+
maskVersion(version) {
|
|
166
|
+
const parts = version.split(".")
|
|
167
|
+
|
|
168
|
+
if (!this.config.preserveFirstPart) {
|
|
169
|
+
return parts.map(part => this.maskChar.repeat(part.length)).join(".")
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return parts
|
|
173
|
+
.map((part, index) => {
|
|
174
|
+
return index === 0 ? part : this.maskChar.repeat(part.length)
|
|
175
|
+
})
|
|
176
|
+
.join(".")
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
*
|
|
181
|
+
* @param {string} text
|
|
182
|
+
* @returns
|
|
183
|
+
*/
|
|
184
|
+
maskText(text) {
|
|
185
|
+
verbose && console.log(`[PrivacyBrush] Masking text: |${text}|`)
|
|
186
|
+
|
|
187
|
+
let result = text
|
|
188
|
+
|
|
189
|
+
this.sensitivePatterns.forEach(pattern => {
|
|
190
|
+
result = result.replace(pattern.regex, pattern.replacer)
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
return result
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* 批量处理文件
|
|
198
|
+
* @param {string} inputPath
|
|
199
|
+
* @param {string} [outputPath]
|
|
200
|
+
* @returns
|
|
201
|
+
*/
|
|
202
|
+
maskFile(inputPath, outputPath) {
|
|
203
|
+
try {
|
|
204
|
+
// Delay-load fs to avoid startup cost when this method is not used
|
|
205
|
+
const require = createRequire(import.meta.url)
|
|
206
|
+
const fs = require("node:fs")
|
|
207
|
+
|
|
208
|
+
const content = fs.readFileSync(inputPath, "utf8")
|
|
209
|
+
const maskedContent = this.maskText(content)
|
|
210
|
+
|
|
211
|
+
if (outputPath) {
|
|
212
|
+
fs.writeFileSync(outputPath, maskedContent, "utf8")
|
|
213
|
+
verbose && console.log(`Masked file saved to: ${outputPath}`)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return maskedContent
|
|
217
|
+
} catch (error) {
|
|
218
|
+
console.error(
|
|
219
|
+
"Error processing file:",
|
|
220
|
+
error instanceof Error ? error.message : String(error),
|
|
221
|
+
)
|
|
222
|
+
throw error
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// 实时流处理
|
|
227
|
+
async createMaskStream() {
|
|
228
|
+
const { Transform } = await import("node:stream")
|
|
229
|
+
|
|
230
|
+
return new Transform({
|
|
231
|
+
transform: (chunk, encoding, callback) => {
|
|
232
|
+
const text = String(chunk)
|
|
233
|
+
|
|
234
|
+
const masked = this.maskText(text)
|
|
235
|
+
callback(null, masked)
|
|
236
|
+
},
|
|
237
|
+
})
|
|
238
|
+
}
|
|
239
|
+
}
|
package/src/lib/config.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @
|
|
3
|
-
*/
|
|
4
|
-
export const defaultConfig = {
|
|
5
|
-
maskChar: "█",
|
|
6
|
-
preserveFirstPart: true, // 是否保留版本号的第一部分
|
|
7
|
-
maskPatternNames: ["windows_version", "browser_version", "ip_address"],
|
|
8
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @satisfies {import('../type.js').IConfig}
|
|
3
|
+
*/
|
|
4
|
+
export const defaultConfig = {
|
|
5
|
+
maskChar: "█",
|
|
6
|
+
preserveFirstPart: true, // 是否保留版本号的第一部分
|
|
7
|
+
maskPatternNames: ["windows_version", "browser_version", "ip_address"],
|
|
8
|
+
customPatterns: [],
|
|
9
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { parseArgs } from "node:util"
|
|
2
|
+
|
|
3
|
+
// get config from command line arguments use native `parseArgs`
|
|
4
|
+
// node src/cli.mjs --mask X --preserve-first false
|
|
5
|
+
const args = process.argv.slice(2)
|
|
6
|
+
// console.log("args:", args) // args: [ '--mask', 'X', '--preserve-first', 'false' ]
|
|
7
|
+
|
|
8
|
+
export const verbose = args.includes("--verbose")
|
|
9
|
+
|
|
10
|
+
export const parsedResult = safeCall(() =>
|
|
11
|
+
parseArgs({
|
|
12
|
+
allowPositionals: true,
|
|
13
|
+
allowNegative: true,
|
|
14
|
+
|
|
15
|
+
args,
|
|
16
|
+
|
|
17
|
+
options: {
|
|
18
|
+
// input file to mask
|
|
19
|
+
"input-file": {
|
|
20
|
+
type: "string",
|
|
21
|
+
short: "i",
|
|
22
|
+
},
|
|
23
|
+
// custom regex pattern(s) to mask
|
|
24
|
+
pattern: {
|
|
25
|
+
type: "string",
|
|
26
|
+
short: "r",
|
|
27
|
+
multiple: true,
|
|
28
|
+
},
|
|
29
|
+
// output file to write masked content
|
|
30
|
+
"output-file": {
|
|
31
|
+
type: "string",
|
|
32
|
+
short: "o",
|
|
33
|
+
},
|
|
34
|
+
mask: {
|
|
35
|
+
type: "string",
|
|
36
|
+
short: "m",
|
|
37
|
+
default: "█",
|
|
38
|
+
},
|
|
39
|
+
"preserve-first": {
|
|
40
|
+
type: "boolean",
|
|
41
|
+
short: "p",
|
|
42
|
+
default: true,
|
|
43
|
+
},
|
|
44
|
+
// help
|
|
45
|
+
help: {
|
|
46
|
+
type: "boolean",
|
|
47
|
+
short: "h",
|
|
48
|
+
},
|
|
49
|
+
// verbose
|
|
50
|
+
verbose: {
|
|
51
|
+
type: "boolean",
|
|
52
|
+
},
|
|
53
|
+
// version
|
|
54
|
+
version: {
|
|
55
|
+
type: "boolean",
|
|
56
|
+
short: "v",
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
}),
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @template T
|
|
64
|
+
* @param {(...args: any[]) => T} fn
|
|
65
|
+
* @returns {[null, T] | [Error, null]}
|
|
66
|
+
*/
|
|
67
|
+
function safeCall(fn) {
|
|
68
|
+
try {
|
|
69
|
+
const result = fn()
|
|
70
|
+
return [null, result]
|
|
71
|
+
} catch (error) {
|
|
72
|
+
// @ts-expect-error
|
|
73
|
+
return [error, null]
|
|
74
|
+
}
|
|
75
|
+
}
|
package/src/type.ts
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
|
-
export type IConfig = {
|
|
2
|
-
maskChar?: string
|
|
3
|
-
preserveFirstPart?: boolean
|
|
4
|
-
maskPatternNames?: IPatternName[]
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
| "
|
|
10
|
-
| "
|
|
11
|
-
| "
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
1
|
+
export type IConfig = {
|
|
2
|
+
maskChar?: string
|
|
3
|
+
preserveFirstPart?: boolean
|
|
4
|
+
maskPatternNames?: IPatternName[]
|
|
5
|
+
customPatterns?: (string | RegExp)[]
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type IPatternName =
|
|
9
|
+
| "windows_version"
|
|
10
|
+
| "browser_version"
|
|
11
|
+
| "android_version"
|
|
12
|
+
| "ip_address"
|
|
13
|
+
| (string & {})
|
|
14
|
+
|
|
15
|
+
export type IPattern = {
|
|
16
|
+
name: IPatternName
|
|
17
|
+
regex: RegExp
|
|
18
|
+
replacer: (substring: string, ...args: any[]) => string
|
|
19
|
+
}
|
|
@@ -1,13 +1,24 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
1
|
+
# h1
|
|
2
|
+
|
|
3
|
+
## flutter devices
|
|
4
|
+
|
|
5
|
+
❯ flutter devices
|
|
6
|
+
Flutter assets will be downloaded from <https://storage.flutter-io.cn>. Make sure you trust this source!
|
|
7
|
+
Found 4 connected devices:
|
|
8
|
+
sdk gphone64 x86 64 (mobile) • emulator-5554 • android-x64 • Android 16 (API 36) (emulator)
|
|
9
|
+
Windows (desktop) • windows • windows-x64 • Microsoft Windows [版本 10.0.12345.6456]
|
|
10
|
+
Chrome (web) • chrome • web-javascript • Google Chrome 144.0.1234.60
|
|
11
|
+
Edge (web) • edge • web-javascript • Microsoft Edge 144.0.1234.82
|
|
12
|
+
|
|
13
|
+
Run "flutter emulators" to list and start any available device emulators.
|
|
14
|
+
|
|
15
|
+
If you expected another device to be detected, please run "flutter doctor" to diagnose potential issues. You may also try
|
|
16
|
+
increasing the time to wait for connected devices with the "--device-timeout" flag. Visit <https://flutter.dev/setup/> for
|
|
17
|
+
troubleshooting tips.
|
|
18
|
+
|
|
19
|
+
## log
|
|
20
|
+
|
|
21
|
+
DEBUG: User login from IP 192.168.1.100
|
|
22
|
+
DEBUG: Session ID: abc123def456
|
|
23
|
+
DEBUG: Browser: Chrome/144.0.1234.60
|
|
24
|
+
DEBUG: OS: Windows 10.0.12345
|