pi-webdav-backup 1.0.0 → 1.0.1
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/README.md
CHANGED
|
@@ -200,7 +200,11 @@ export function redactSensitive(relPath, buf, opts = {}) {
|
|
|
200
200
|
const aggressive = opts.aggressive !== false;
|
|
201
201
|
const base = relPath.split("/").pop() ?? "";
|
|
202
202
|
|
|
203
|
-
const SECRET_KEY_RE = /key|token|secret|password|passwd|credential|api[-_]?key|auth|cookie|session|bearer|private/i;
|
|
203
|
+
const SECRET_KEY_RE = /key|token|secret|password|passwd|credential|api[-_]?key|auth(?!or)|cookie|session[-_]?(token|key|secret|id)|bearer|private/i;
|
|
204
|
+
// 密钥引用($ENV / !cmd / dpapi: / file:)是“密钥放在哪”的指针,不是秘密本身,应保留
|
|
205
|
+
const SECRET_REF_SCHEMES = ["$", "!", "dpapi:", "file:"];
|
|
206
|
+
const isRef = (v) =>
|
|
207
|
+
typeof v === "string" && SECRET_REF_SCHEMES.some((p) => v.trim().startsWith(p));
|
|
204
208
|
// 常见凭据前缀,以及“长且像随机串”的值
|
|
205
209
|
const SECRET_VALUE_RE =
|
|
206
210
|
/^(sk-[A-Za-z0-9_-]{8,}|ghp_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_-]{20,}|xox[baprs]-[A-Za-z0-9-]{10,}|AIza[0-9A-Za-z_-]{30,}|eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}|[A-Za-z0-9_+/=-]{32,})$/;
|
|
@@ -214,8 +218,15 @@ export function redactSensitive(relPath, buf, opts = {}) {
|
|
|
214
218
|
const out = {};
|
|
215
219
|
for (const [k, val] of Object.entries(v)) {
|
|
216
220
|
if (SECRET_KEY_RE.test(k)) {
|
|
217
|
-
//
|
|
218
|
-
|
|
221
|
+
// 保留结构,抹掉值。
|
|
222
|
+
// 只替换字符串:布尔/数字不可能是凭据,
|
|
223
|
+
// 把 false 替换成字符串会破坏恢复后的配置语义(回归:includeSessions 曾被误伤)
|
|
224
|
+
out[k] =
|
|
225
|
+
typeof val === "object" && val !== null
|
|
226
|
+
? walk(val, k)
|
|
227
|
+
: typeof val === "string"
|
|
228
|
+
? "__REDACTED__"
|
|
229
|
+
: val;
|
|
219
230
|
} else {
|
|
220
231
|
out[k] = walk(val, k);
|
|
221
232
|
}
|
|
@@ -236,10 +247,6 @@ export function redactSensitive(relPath, buf, opts = {}) {
|
|
|
236
247
|
if (base.startsWith("auth.json")) {
|
|
237
248
|
try {
|
|
238
249
|
const obj = JSON.parse(buf.toString("utf8"));
|
|
239
|
-
const SECRET_REF_SCHEMES = ["$", "!", "dpapi:", "file:"];
|
|
240
|
-
const isRef = (v) =>
|
|
241
|
-
typeof v === "string" && SECRET_REF_SCHEMES.some((p) => v.trim().startsWith(p));
|
|
242
|
-
|
|
243
250
|
const walkCredential = (cred) => {
|
|
244
251
|
if (!cred || typeof cred !== "object") return redactJson(cred, { aggressiveValues: aggressive });
|
|
245
252
|
const out = {};
|
|
@@ -270,7 +277,11 @@ export function redactSensitive(relPath, buf, opts = {}) {
|
|
|
270
277
|
try {
|
|
271
278
|
const obj = JSON.parse(buf.toString("utf8"));
|
|
272
279
|
const cleaned = redactJson(obj, { aggressiveValues: false });
|
|
273
|
-
|
|
280
|
+
// 与 auth.json 同一约定:密钥引用保留(指针非秘密),
|
|
281
|
+
// 明文由字段名规则抹掉,这里只需把被误抹的引用还原
|
|
282
|
+
if (typeof obj.encryptKey === "string" && isRef(obj.encryptKey)) cleaned.encryptKey = obj.encryptKey;
|
|
283
|
+
const pw = obj.remote && typeof obj.remote === "object" ? obj.remote.password : undefined;
|
|
284
|
+
if (typeof pw === "string" && isRef(pw) && cleaned.remote) cleaned.remote.password = pw;
|
|
274
285
|
return Buffer.from(JSON.stringify(cleaned, null, 2), "utf8");
|
|
275
286
|
} catch {
|
|
276
287
|
return buf;
|
|
@@ -107,6 +107,30 @@ check("嵌套路径 auth.json 也被脱敏", !r12.includes("sk-nested-canary"),
|
|
|
107
107
|
check("空对象不报错", (() => { try { redact("auth.json", "{}"); return true; } catch { return false; } })());
|
|
108
108
|
check("null 值不报错", (() => { try { redact("auth.json", '{"a":null,"b":1}'); return true; } catch { return false; } })());
|
|
109
109
|
|
|
110
|
+
// ── 10b. 回归:布尔/数字不被误伤,密钥引用保留(本地实测发现)
|
|
111
|
+
const rBool = redact(
|
|
112
|
+
"webdav-backup.json",
|
|
113
|
+
JSON.stringify({ includeSessions: false, snapshot: true, keepVersions: 10, sessionDir: "C:/sessions" }),
|
|
114
|
+
);
|
|
115
|
+
check("includeSessions:false 不被误伤(回归)", !rBool.includes("__REDACTED__"), rBool);
|
|
116
|
+
check("sessionDir 不被误伤", rBool.includes("C:/sessions"), rBool);
|
|
117
|
+
|
|
118
|
+
const rTokFlag = redact("webdav-backup.json", JSON.stringify({ tokenEnabled: false, keyCount: 3 }));
|
|
119
|
+
check("布尔/数字在敏感字段名下也不误替换", !rTokFlag.includes("__REDACTED__"), rTokFlag);
|
|
120
|
+
|
|
121
|
+
const rSessTok = redact("webdav-backup.json", JSON.stringify({ sessionToken: "tok-123456789012" }));
|
|
122
|
+
check("sessionToken 仍被脱敏", !rSessTok.includes("tok-123456789012"), rSessTok);
|
|
123
|
+
|
|
124
|
+
const rRef2 = redact(
|
|
125
|
+
"webdav-backup.json",
|
|
126
|
+
JSON.stringify({ remote: { password: "dpapi:webdav-password" }, encryptKey: "$PI_WEBDAV_BACKUP_KEY" }),
|
|
127
|
+
);
|
|
128
|
+
check("password 的 dpapi: 引用保留", rRef2.includes("dpapi:webdav-password"), rRef2);
|
|
129
|
+
check("encryptKey 的 $ENV 引用保留", rRef2.includes("$PI_WEBDAV_BACKUP_KEY"), rRef2);
|
|
130
|
+
|
|
131
|
+
const rPlain2 = redact("webdav-backup.json", JSON.stringify({ remote: { password: "MyPassword123" } }));
|
|
132
|
+
check("password 明文仍被脱敏", !rPlain2.includes("MyPassword123"), rPlain2);
|
|
133
|
+
|
|
110
134
|
// ── 12. 明文审计(防止备份副本把密钥带上云)
|
|
111
135
|
{
|
|
112
136
|
const { auditForPlaintext, looksCredentialFile } = await import("./backup.mjs");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-webdav-backup",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "pi 配置自动备份到 WebDAV(坚果云 / Nextcloud / 群晖 / 自建):归档与快照两种模式、AES-256-GCM 加密、DPAPI 密钥管理、敏感字段脱敏、退出自动备份与每日定时备份。",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -33,7 +33,9 @@
|
|
|
33
33
|
"test": "node extensions/webdav-backup/run-tests.mjs"
|
|
34
34
|
},
|
|
35
35
|
"pi": {
|
|
36
|
-
"extensions": [
|
|
36
|
+
"extensions": [
|
|
37
|
+
"./extensions/webdav-backup.ts"
|
|
38
|
+
]
|
|
37
39
|
},
|
|
38
40
|
"peerDependencies": {
|
|
39
41
|
"@earendil-works/pi-coding-agent": "*"
|