dsh-config-manager 0.1.39 → 0.1.41
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/lib/client.d.ts +90 -8
- package/lib/client.js +1904 -1086
- package/lib/client.js.map +1 -1
- package/lib/core/cache-cleaner.d.ts +39 -0
- package/lib/core/cache-cleaner.js +164 -0
- package/lib/core/cache-cleaner.js.map +1 -0
- package/lib/index.js +172 -3
- package/lib/index.js.map +1 -1
- package/lib/market/github-repos.d.ts +19 -8
- package/lib/market/github-repos.js +62 -33
- package/lib/market/github-repos.js.map +1 -1
- package/lib/market/my-repo.d.ts +111 -3
- package/lib/market/my-repo.js +309 -26
- package/lib/market/my-repo.js.map +1 -1
- package/lib/market/prepare.js +3 -2
- package/lib/market/prepare.js.map +1 -1
- package/lib/market/types.d.ts +2 -0
- package/lib/security/secret-scanner.d.ts +18 -3
- package/lib/security/secret-scanner.js +144 -20
- package/lib/security/secret-scanner.js.map +1 -1
- package/package.json +1 -1
- package/src/client/common/ConfirmDialog.tsx +135 -0
- package/src/client/config-manager.module.css +101 -0
- package/src/client/market/MarketPanel.tsx +138 -45
- package/src/client/market/MyConfigsView.tsx +515 -132
- package/src/client/market/disclaimer.test.ts +57 -0
- package/src/client/market/disclaimer.ts +43 -0
- package/src/client/market/market-api.test.ts +3 -0
- package/src/client/market/market-locales.ts +37 -13
- package/src/client/market/my-configs-api.ts +20 -2
- package/src/client/market/my-configs-view.test.ts +137 -2
- package/src/client/market/my-configs-view.ts +174 -2
- package/src/client/run-store.test.ts +40 -2
- package/src/client/run-store.ts +70 -10
- package/src/client/sync/SyncConfirmView.tsx +28 -22
- package/src/client/sync/SyncSettingsView.tsx +175 -65
- package/src/client/sync/sync-api.ts +19 -0
- package/src/client/sync/sync-locales.ts +14 -2
- package/src/core/cache-cleaner.test.ts +333 -0
- package/src/core/cache-cleaner.ts +196 -0
- package/src/index.ts +165 -5
- package/src/market/github-repos.test.ts +99 -8
- package/src/market/github-repos.ts +67 -35
- package/src/market/my-repo.test.ts +218 -27
- package/src/market/my-repo.ts +403 -30
- package/src/market/prepare.test.ts +71 -1
- package/src/market/prepare.ts +3 -2
- package/src/market/types.ts +2 -0
- package/src/security/secret-scanner.ts +139 -22
- package/src/security/security.test.ts +111 -2
package/src/market/types.ts
CHANGED
|
@@ -194,6 +194,8 @@ export interface MarketStatusResponse {
|
|
|
194
194
|
/** 是否已添加过至少一个市场 */
|
|
195
195
|
configured: boolean;
|
|
196
196
|
markets: MarketSummary[];
|
|
197
|
+
/** 本次 dsh 启动后市场是否已刷新过(首次打开市场页自动更新一次的依据;进程内存,dsh 重启后重置) */
|
|
198
|
+
bootAutoRefreshed?: boolean;
|
|
197
199
|
}
|
|
198
200
|
|
|
199
201
|
/** POST /market/add | /market/remove 响应:{ ok, markets } */
|
|
@@ -7,10 +7,19 @@
|
|
|
7
7
|
* 判定策略(两级,避免误伤/漏报):
|
|
8
8
|
* - 字段名:规范化(小写、去 `_-. ` 分隔符)后做「精确命中 / 敏感后缀 / 敏感前缀」三级匹配;
|
|
9
9
|
* 只剥离**字符串叶值**(对象/数组整体递归,不整块剥离——保证 `credentials: [...]` 状态数组存活)。
|
|
10
|
-
* - 引用豁免:`apiKeyEnv`
|
|
11
|
-
* (`isEnvVarName
|
|
10
|
+
* - 引用豁免:`apiKeyEnv` 等「只存引用名」字段、形如 `DEEPSEEK_API_KEY` 的全大写环境变量名
|
|
11
|
+
* (`isEnvVarName`)以及 `${VAR}` / `{{VAR}}` / `%VAR%` / `$VAR` 模板引用(`isTemplateReference`)
|
|
12
|
+
* 一律保留——值是名字不是秘密。
|
|
12
13
|
* - 值形状启发式(规范 §6「不能只根据固定字段判断」):字段名未命中但值长得像 secret
|
|
13
|
-
* (sk- / JWT / AKIA / GitHub PAT / PEM 私钥 / Bearer
|
|
14
|
+
* (sk- / JWT / AKIA / GitHub PAT / PEM 私钥 / Bearer)也剥离;**示例/占位形态**(含
|
|
15
|
+
* your/example/xxx/test/<...> 等占位词的 `sk-your-*`、`Bearer example-*`、JWT 教学串)放行。
|
|
16
|
+
* - 两种判定档位(2026-08-21 优化):
|
|
17
|
+
* * 保守档(默认,导出/同步脱敏用):字段名敏感**即**剥离(值形状优先、引用/env 名豁免);
|
|
18
|
+
* * 宽松档(`literalValueOnly`,市场发布扫描用):字段名敏感**且**值像真实字面量凭据
|
|
19
|
+
* (非占位符 / 非类型词 / 非代码表达式 / 非模板引用 / 非短标识符)才命中——消除
|
|
20
|
+
* `"token": "your-token"`、`"token": "${OPENAI_API_KEY}"`、`"Authorization": "Bearer <token>"`
|
|
21
|
+
* 等「值不是秘密」的误报。
|
|
22
|
+
* - 高熵 base64 默认关(误伤率高)。
|
|
14
23
|
*
|
|
15
24
|
* 实现不变量:返回 sanitized 为**全新对象**(不改原数据);迭代式显式栈(防爆栈)+ 深度上限;
|
|
16
25
|
* 命中记录只含路径与字段名,**值永不外泄**。
|
|
@@ -65,6 +74,42 @@ export const SECRET_VALUE_PATTERNS: readonly ValuePattern[] = [
|
|
|
65
74
|
{ name: 'bearer-token', re: /Bearer [A-Za-z0-9._~+/=-]{8,}/ },
|
|
66
75
|
];
|
|
67
76
|
|
|
77
|
+
/**
|
|
78
|
+
* 值形状命中片段中的「示例/占位」词:匹配到的片段含其中任一 → 视为示例形态放行。
|
|
79
|
+
* 覆盖 `sk-your-*`、`sk-example-*`、`sk-test-*`、`Bearer example-*`、`AKIA...EXAMPLE`(AWS 官方示例)、
|
|
80
|
+
* JWT 教学串(第三段 signature)等;真实随机密钥串几乎不可能包含这些完整单词,安全不回退。
|
|
81
|
+
*/
|
|
82
|
+
const SECRET_SHAPE_PLACEHOLDER_WORDS = [
|
|
83
|
+
'your', 'their', 'example', 'sample', 'dummy', 'placeholder',
|
|
84
|
+
'test', 'demo', 'signature', 'secret', 'value', 'xxxx',
|
|
85
|
+
] as const;
|
|
86
|
+
|
|
87
|
+
/** 值形状命中是否「示例/占位」形态(含占位词或尖括号包裹)→ 放行 */
|
|
88
|
+
function isExampleSecretShape(matched: string): boolean {
|
|
89
|
+
if (/<[^>]+>/.test(matched)) return true;
|
|
90
|
+
const lower = matched.toLowerCase();
|
|
91
|
+
return SECRET_SHAPE_PLACEHOLDER_WORDS.some((w) => lower.includes(w));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** 值形状命中详情(模式名 + 实际匹配片段;示例降噪用) */
|
|
95
|
+
interface ValuePatternHit { name: string; matched: string }
|
|
96
|
+
|
|
97
|
+
function matchSecretValuePatternHit(value: string): ValuePatternHit | null {
|
|
98
|
+
for (const p of SECRET_VALUE_PATTERNS) {
|
|
99
|
+
const m = p.re.exec(value);
|
|
100
|
+
if (m !== null) return { name: p.name, matched: m[0] };
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** 值是否命中强模式 secret 形状(示例/占位形态 → null;返回命中模式名;未命中返回 null) */
|
|
106
|
+
export function matchSecretValuePattern(value: string): string | null {
|
|
107
|
+
const hit = matchSecretValuePatternHit(value);
|
|
108
|
+
if (hit === null) return null;
|
|
109
|
+
if (isExampleSecretShape(hit.matched)) return null;
|
|
110
|
+
return hit.name;
|
|
111
|
+
}
|
|
112
|
+
|
|
68
113
|
export interface SecretScannerOptions {
|
|
69
114
|
/** 附加敏感字段名(任意原始形态,内部会规范化) */
|
|
70
115
|
extraFieldNames?: string[];
|
|
@@ -74,6 +119,13 @@ export interface SecretScannerOptions {
|
|
|
74
119
|
valuePatterns?: boolean;
|
|
75
120
|
/** 高熵长串启发式(默认 false:误伤风险高,需显式开启) */
|
|
76
121
|
highEntropy?: boolean;
|
|
122
|
+
/**
|
|
123
|
+
* 宽松档(默认 false = 保守档)。
|
|
124
|
+
* 保守档(导出/同步脱敏):字段名敏感即命中,值形状优先、引用/env 名豁免;
|
|
125
|
+
* 宽松档(市场发布扫描):字段名敏感**且**值像真实字面量凭据才命中——占位符 /
|
|
126
|
+
* 模板引用 / 代码表达式 / 类型词 / 短标识符放行,消除「值不是秘密」的误报。
|
|
127
|
+
*/
|
|
128
|
+
literalValueOnly?: boolean;
|
|
77
129
|
/** 最大递归深度(默认 64,与 JSON 深度上限一致) */
|
|
78
130
|
maxDepth?: number;
|
|
79
131
|
}
|
|
@@ -88,6 +140,23 @@ export function isEnvVarName(value: string): boolean {
|
|
|
88
140
|
return /^[A-Z][A-Z0-9_]{2,}$/.test(value);
|
|
89
141
|
}
|
|
90
142
|
|
|
143
|
+
/** 是否为模板/环境变量引用形态(${VAR} / {{VAR}} / %VAR% / $VAR)→ 值是名字不是秘密。
|
|
144
|
+
* 注意:文本扫描的 `field: value` 正则用 `[^\s,;}]` 取值,`${...}` / `{{...}}` 的右花括号
|
|
145
|
+
* 会被截断,因此除完整闭合形态外,也接受「以 ${ / {{ / % 开头、内容全为变量名字符」的截断形态。 */
|
|
146
|
+
export function isTemplateReference(value: string): boolean {
|
|
147
|
+
const v = value.trim();
|
|
148
|
+
// 完整闭合形态
|
|
149
|
+
if (/^\$\{[A-Za-z0-9_.]+\}$/.test(v)) return true;
|
|
150
|
+
if (/^\{\{[A-Za-z0-9_.]+\}\}$/.test(v)) return true;
|
|
151
|
+
if (/^%[A-Za-z0-9_.]+%$/.test(v)) return true;
|
|
152
|
+
if (/^\$[A-Z][A-Z0-9_]{2,}$/.test(v)) return true;
|
|
153
|
+
// 截断形态(右花括号被取值正则排除):真实凭据不会以 ${ / {{ / % 开头,放行安全
|
|
154
|
+
if (/^\$\{[A-Za-z0-9_.]*$/.test(v)) return true;
|
|
155
|
+
if (/^\{\{[A-Za-z0-9_.]*$/.test(v)) return true;
|
|
156
|
+
if (/^%[A-Za-z0-9_.]*$/.test(v)) return true;
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
|
|
91
160
|
/** 字段名是否命中敏感名单(规范化后精确 / 敏感后缀 / 敏感前缀) */
|
|
92
161
|
export function isSensitiveFieldName(
|
|
93
162
|
field: string,
|
|
@@ -109,14 +178,6 @@ export function isReferenceField(
|
|
|
109
178
|
return [...DEFAULT_REFERENCE_FIELDS, ...extra].includes(norm);
|
|
110
179
|
}
|
|
111
180
|
|
|
112
|
-
/** 值是否命中强模式 secret 形状(返回命中模式名;未命中返回 null) */
|
|
113
|
-
export function matchSecretValuePattern(value: string): string | null {
|
|
114
|
-
for (const p of SECRET_VALUE_PATTERNS) {
|
|
115
|
-
if (p.re.test(value)) return p.name;
|
|
116
|
-
}
|
|
117
|
-
return null;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
181
|
/** 高熵启发式:长 base64/hex 串且 Shannon 熵高(默认关) */
|
|
121
182
|
function isHighEntropySecret(value: string): boolean {
|
|
122
183
|
if (!/^[A-Za-z0-9+/=_-]{32,}$/.test(value)) return false;
|
|
@@ -131,10 +192,65 @@ function isHighEntropySecret(value: string): boolean {
|
|
|
131
192
|
return entropy > 4.2;
|
|
132
193
|
}
|
|
133
194
|
|
|
195
|
+
/* ---------------- 值形态判定(结构化扫描与文本扫描共用) ---------------- */
|
|
196
|
+
|
|
197
|
+
/** 占位符 / 示例形态:xxx、your-token、<...>、example、sample、dummy、placeholder、... */
|
|
198
|
+
const PLACEHOLDER_RE = /^(x{3,}|your[-_ ]?[\w]+|their[-_ ]?[\w]+|<[^>]+>|example|examples?|sample|dummy|placeholder|som[e]thing|^\u2026)/i;
|
|
199
|
+
|
|
200
|
+
/** 类型/关键字形态(代码声明,如 `authToken: string`、`token: null`) */
|
|
201
|
+
const TYPE_KEYWORD_RE = /^(string|number|boolean|object|array|any|unknown|null|undefined|void|true|false|required|optional)$/i;
|
|
202
|
+
|
|
203
|
+
/** 代码表达式/引用形态:值里出现调用/成员访问/引号/运算符/空白等 → 是代码而非字面量密钥 */
|
|
204
|
+
function isCodeExpressionValue(value: string): boolean {
|
|
205
|
+
// 方法调用 / 成员访问:data.token、process.env.X、wx.getStorageSync(...)、a.b(...)
|
|
206
|
+
if (/[.([\]'"`]/.test(value)) return true;
|
|
207
|
+
// 赋值/比较/模板:a=b、a&&b 等
|
|
208
|
+
if (/[=+*/%<>!&|?]+/.test(value)) return true;
|
|
209
|
+
// 含空白 → 短语/语句而非单个字面量
|
|
210
|
+
if (/\s/.test(value)) return true;
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** 混合字符长串是否「疑似真实凭据」:含数字、或含字母数字连字符下划线以外的符号、或大小写混合
|
|
215
|
+
* (纯小写字母+连字符的示例值如 `abc-def-ghi-jkl` → 放行;`s3cretP@ssw0rd` / `abc123-def456` → 拦截) */
|
|
216
|
+
function looksLikeCredential(value: string): boolean {
|
|
217
|
+
if (value.length < 8) return false;
|
|
218
|
+
if (/[0-9]/.test(value)) return true;
|
|
219
|
+
if (/[^A-Za-z0-9_-]/.test(value)) return true;
|
|
220
|
+
if (/[a-z]/.test(value) && /[A-Z]/.test(value)) return true;
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** 值是否为「字面量密钥」:既不是代码表达式,也不是占位符/类型词/短标识符/环境引用。
|
|
225
|
+
* 只有字段名敏感 **且** 值像真实字面量凭据时才报告 —— 消除技能文档中
|
|
226
|
+
* `authToken: string`、`password: process.env.X`、`token: data.token` 等代码示例误报。 */
|
|
227
|
+
function isLiteralSecretValue(value: string): boolean {
|
|
228
|
+
if (value === '') return false;
|
|
229
|
+
// 值形状强信号(sk- / ghp_ / AKIA / JWT / PEM / Bearer,示例/占位形态已降噪)—— 任何字段都视为真凭据
|
|
230
|
+
if (matchSecretValuePattern(value) !== null) return true;
|
|
231
|
+
if (PLACEHOLDER_RE.test(value)) return false;
|
|
232
|
+
if (TYPE_KEYWORD_RE.test(value)) return false;
|
|
233
|
+
if (isCodeExpressionValue(value)) return false;
|
|
234
|
+
if (isEnvVarName(value)) return false;
|
|
235
|
+
if (isTemplateReference(value)) return false;
|
|
236
|
+
// 纯字母数字短标识符(变量名/普通词,如 authToken、data、key)→ 非密钥
|
|
237
|
+
if (/^[A-Za-z0-9_]+$/.test(value) && value.length < 20) return false;
|
|
238
|
+
// 其余:混合字符串,仅当像真实凭据(含数字/符号/大小写混合)才疑似
|
|
239
|
+
return looksLikeCredential(value);
|
|
240
|
+
}
|
|
241
|
+
|
|
134
242
|
/** 判定单个「字段名 + 字符串值」:strip | keep(含引用豁免与值形状)
|
|
135
|
-
* 顺序:值形状优先(AKIA 等全大写 secret 形状不能被 env 名豁免误放)→ 引用字段 → env 名 → 字段名。
|
|
136
|
-
|
|
243
|
+
* 顺序:值形状优先(AKIA 等全大写 secret 形状不能被 env 名豁免误放)→ 引用字段 → env 名 → 字段名。
|
|
244
|
+
* 宽松档(literalValueOnly,市场发布):字段名命中还要求值像真实字面量凭据(非占位/引用/代码)。 */
|
|
245
|
+
function judgeFieldValue(field: string, value: string, opts: Required<Pick<SecretScannerOptions, 'extraFieldNames' | 'extraReferenceFields' | 'valuePatterns' | 'highEntropy' | 'literalValueOnly'>>): boolean {
|
|
137
246
|
if (value === '') return false;
|
|
247
|
+
if (opts.literalValueOnly) {
|
|
248
|
+
// 宽松档(市场发布扫描):值形状强信号字段无关必拦;字段名敏感 + 值像真实字面量凭据才拦
|
|
249
|
+
if (opts.valuePatterns && matchSecretValuePattern(value) !== null) return true;
|
|
250
|
+
if (isSensitiveFieldName(field, opts.extraFieldNames) && isLiteralSecretValue(value)) return true;
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
// 保守档(导出/同步脱敏):值形状优先 → 引用字段/env 名豁免 → 字段名敏感即剥
|
|
138
254
|
if (opts.valuePatterns && matchSecretValuePattern(value) !== null) return true;
|
|
139
255
|
if (isReferenceField(field, opts.extraReferenceFields)) return false;
|
|
140
256
|
if (isEnvVarName(value)) return false; // 值是 env 变量名 → 引用
|
|
@@ -146,11 +262,12 @@ function judgeFieldValue(field: string, value: string, opts: Required<Pick<Secre
|
|
|
146
262
|
/** 递归扫描 + 剥离(纯函数,不改原数据;深度保护;返回清洗后数据与命中清单) */
|
|
147
263
|
export function scanAndRedact(data: unknown, opts: SecretScannerOptions = {}): { sanitized: unknown; hits: SensitiveHit[] } {
|
|
148
264
|
const maxDepth = opts.maxDepth ?? 64;
|
|
149
|
-
const cfg: Required<Pick<SecretScannerOptions, 'extraFieldNames' | 'extraReferenceFields' | 'valuePatterns' | 'highEntropy'>> = {
|
|
265
|
+
const cfg: Required<Pick<SecretScannerOptions, 'extraFieldNames' | 'extraReferenceFields' | 'valuePatterns' | 'highEntropy' | 'literalValueOnly'>> = {
|
|
150
266
|
extraFieldNames: opts.extraFieldNames ?? [],
|
|
151
267
|
extraReferenceFields: opts.extraReferenceFields ?? [],
|
|
152
268
|
valuePatterns: opts.valuePatterns ?? true,
|
|
153
269
|
highEntropy: opts.highEntropy ?? false,
|
|
270
|
+
literalValueOnly: opts.literalValueOnly ?? false,
|
|
154
271
|
};
|
|
155
272
|
const hits: SensitiveHit[] = [];
|
|
156
273
|
|
|
@@ -225,7 +342,11 @@ const FIELD_VALUE_RE = [
|
|
|
225
342
|
{ re: /([A-Za-z0-9_.\-]+)\s*:\s*([^\s,;}]+)/g, json: false },
|
|
226
343
|
] as const;
|
|
227
344
|
|
|
228
|
-
/** 文本级扫描:逐行找「敏感字段名
|
|
345
|
+
/** 文本级扫描:逐行找「敏感字段名 + 疑似真实字面量凭据」与「值形状」命中(只报告,不修改文本)。
|
|
346
|
+
* 2026-08-20 优化:字段名命中不再是充分条件 —— 值必须是「非代码引用 / 非占位符 / 非类型词 /
|
|
347
|
+
* 非短标识符」的字面量,消除技能/代码文档中 `token:`/`password:` 等示例命名的误报;
|
|
348
|
+
* 2026-08-21 优化:值形状(sk-/ghp_/AKIA/JWT/PEM/Bearer)对示例/占位形态(含 your/example/
|
|
349
|
+
* xxx/test/<...> 等占位词)放行;混合字符串需像真实凭据(含数字/符号/大小写混合)才报告。 */
|
|
229
350
|
export function scanText(text: string, opts: SecretScannerOptions = {}): SensitiveHit[] {
|
|
230
351
|
const hits: SensitiveHit[] = [];
|
|
231
352
|
const extra = opts.extraFieldNames ?? [];
|
|
@@ -243,16 +364,12 @@ export function scanText(text: string, opts: SecretScannerOptions = {}): Sensiti
|
|
|
243
364
|
const field = m[1]!;
|
|
244
365
|
const value = m[2]!;
|
|
245
366
|
if (!isSensitiveFieldName(field, extra)) continue;
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
hits.push({ path: linePath, field });
|
|
249
|
-
continue;
|
|
250
|
-
}
|
|
251
|
-
if (isEnvVarName(value)) continue; // 引用名不报告
|
|
367
|
+
// 值必须是「疑似字面量凭据」才报告(消除代码示例/占位符误报)
|
|
368
|
+
if (!isLiteralSecretValue(value)) continue;
|
|
252
369
|
hits.push({ path: linePath, field });
|
|
253
370
|
}
|
|
254
371
|
}
|
|
255
|
-
//
|
|
372
|
+
// 值形状(字段名无关;整行 trim 后匹配 sk-/ghp_/JWT/PEM/Bearer 等强信号,示例形态已降噪)
|
|
256
373
|
if (valuePatterns) {
|
|
257
374
|
const trimmed = line.trim();
|
|
258
375
|
const name = matchSecretValuePattern(trimmed);
|
|
@@ -619,7 +619,7 @@ test('scanner: 引用豁免(apiKeyEnv 等只存名字的字段、env 名形态
|
|
|
619
619
|
apiKeyEnv: 'DEEPSEEK_API_KEY',
|
|
620
620
|
api_key_env: 'GITHUB_TOKEN',
|
|
621
621
|
apiKey: 'DEEPSEEK_API_KEY', // 值是 env 变量名形态 → 视为引用
|
|
622
|
-
realKey: 'sk-
|
|
622
|
+
realKey: 'sk-proj-9f8e7d6c5b4a3210abcdef', // 真实形态密钥 → 仍拦截
|
|
623
623
|
tokenRef: 'MY_TOKEN_REF',
|
|
624
624
|
};
|
|
625
625
|
const { sanitized } = scanAndRedact(input);
|
|
@@ -635,7 +635,7 @@ test('scanner: 值形状启发式(字段名无关的 secret 形状也剥离)
|
|
|
635
635
|
const input = {
|
|
636
636
|
note: 'use sk-abc1234567890abc for api',
|
|
637
637
|
jwt: 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U',
|
|
638
|
-
aws: '
|
|
638
|
+
aws: 'AKIAIOSFODNN7A1B2C3D4',
|
|
639
639
|
github: 'ghp_abcdefghijklmnopqrstuvwxyz',
|
|
640
640
|
pem: '-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA...',
|
|
641
641
|
fine: 'just a note',
|
|
@@ -653,6 +653,26 @@ test('scanner: 值形状启发式(字段名无关的 secret 形状也剥离)
|
|
|
653
653
|
assert.equal(matchSecretValuePattern('plain text'), null);
|
|
654
654
|
});
|
|
655
655
|
|
|
656
|
+
test('scanner: 值形状示例/占位形态放行(sk-your-*、AKIA...EXAMPLE、JWT 教学串)', () => {
|
|
657
|
+
const input = {
|
|
658
|
+
yourKey: 'sk-your-api-key-here', // 占位词 your
|
|
659
|
+
exampleKey: 'sk-example-key-12345', // 占位词 example
|
|
660
|
+
testKey: 'sk-test-key-1234567890', // 占位词 test
|
|
661
|
+
awsSample: 'AKIAIOSFODNN7EXAMPLE', // AWS 官方文档示例 key
|
|
662
|
+
jwtSample: 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.signature', // 教学串第三段 signature
|
|
663
|
+
note: 'use sk-your-demo-key for api', // 文本叙述中的示例
|
|
664
|
+
};
|
|
665
|
+
const { sanitized, hits } = scanAndRedact(input);
|
|
666
|
+
const s = sanitized as Record<string, unknown>;
|
|
667
|
+
assert.equal(s['yourKey'], 'sk-your-api-key-here', 'sk-your-* 示例应放行');
|
|
668
|
+
assert.equal(s['exampleKey'], 'sk-example-key-12345', 'sk-example-* 示例应放行');
|
|
669
|
+
assert.equal(s['testKey'], 'sk-test-key-1234567890', 'sk-test-* 示例应放行');
|
|
670
|
+
assert.equal(s['awsSample'], 'AKIAIOSFODNN7EXAMPLE', 'AWS 官方示例应放行');
|
|
671
|
+
assert.equal(s['jwtSample'], 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.signature', 'JWT 教学串应放行');
|
|
672
|
+
assert.equal(s['note'], 'use sk-your-demo-key for api', '叙述中的示例应放行');
|
|
673
|
+
assert.equal(hits.length, 0);
|
|
674
|
+
});
|
|
675
|
+
|
|
656
676
|
test('scanner: 深层嵌套上限(≤64 通过,超限抛错)与循环引用防护', () => {
|
|
657
677
|
const deep = (n: number): unknown => {
|
|
658
678
|
let v: unknown = 'leaf';
|
|
@@ -700,6 +720,95 @@ test('scanner: isSensitiveFieldName / createSecretScanner(core 契约)', ()
|
|
|
700
720
|
assert.ok(textHits.some((h) => h.path.startsWith('line:')));
|
|
701
721
|
});
|
|
702
722
|
|
|
723
|
+
test('scanText: 技能/代码文档中的示例命名不再误报(2026-08-20 优化)', () => {
|
|
724
|
+
// 真实技能文件(api-tester.md / frontend-developer.md 等)里出现过的代码示例形态 —— 全部应放行
|
|
725
|
+
const skillDoc = [
|
|
726
|
+
'let authToken: string;', // 类型声明(值=string 类型词)
|
|
727
|
+
'password: process.env.TEST_USER_PASSWORD', // 代码引用(含 . 与 ()—— 注意 field: value 正则截取 value 为 process.env.TEST_USER_PASSWORD
|
|
728
|
+
'authToken = data.token;', // 赋值 + 成员访问
|
|
729
|
+
'const token = loginResponse.json(\'token\');', // 方法调用
|
|
730
|
+
'key={virtualItem.key}', // React key prop(key 不入敏感名单)
|
|
731
|
+
'queryKey: [\'products\'],', // 数组字面量
|
|
732
|
+
'token: your-token-here', // 占位符
|
|
733
|
+
'api_key: <your-key>', // 占位符
|
|
734
|
+
'max_tokens: 100', // 值=数字…… 但正则只匹配 \w+;"100" 是短标识符?100 长3 <20 → 放行 ✓
|
|
735
|
+
].join('\n');
|
|
736
|
+
const hits = scanText(skillDoc);
|
|
737
|
+
assert.equal(hits.length, 0, `技能文档示例不应误报,实际: ${JSON.stringify(hits)}`);
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
test('scanText: 真实字面量密钥仍然拦截(优化不回退安全)', () => {
|
|
741
|
+
// 字段名敏感 + 真正的字面量长串 → 报
|
|
742
|
+
const real = [
|
|
743
|
+
'password: s3cretP@ssw0rdXyZ2024', // 字段 password + 混合字面量长串
|
|
744
|
+
'client_secret: abcdef1234567890abcdef12345678', // 字段敏感 + 长混合串
|
|
745
|
+
'"apiKey": "sk-proj-abcdef1234567890"', // JSON 形态 + sk- 值形状(强信号)
|
|
746
|
+
'token=ghp_abcdefghijklmnopqrstuvwxyzABCDEF', // GitHub PAT 值形状
|
|
747
|
+
].join('\n');
|
|
748
|
+
const hits = scanText(real);
|
|
749
|
+
assert.ok(hits.length >= 4, `真实密钥应全部拦截,实际: ${JSON.stringify(hits)} (${hits.length})`);
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
test('scanText: 占位/示例/模板引用/普通示例值放行(2026-08-21 优化)', () => {
|
|
753
|
+
const doc = [
|
|
754
|
+
'token: ${OPENAI_API_KEY}', // 模板引用
|
|
755
|
+
'api_key: {{MY_KEY}}', // 模板引用(双花括号)
|
|
756
|
+
'secret: $ENV_SECRET', // $VAR 引用
|
|
757
|
+
'password: your-token-here', // 占位符
|
|
758
|
+
'token: abc-def-ghi-jkl', // 纯小写字母+连字符示例值(无数字/符号/大小写混合)
|
|
759
|
+
'Bearer example-token-here', // 值形状示例形态
|
|
760
|
+
'sk-your-api-key-here', // 值形状示例形态
|
|
761
|
+
'use sk-demo-key-1234567890 in this doc', // 叙述中的示例
|
|
762
|
+
].join('\n');
|
|
763
|
+
const hits = scanText(doc);
|
|
764
|
+
assert.equal(hits.length, 0, `示例文档不应误报,实际: ${JSON.stringify(hits)}`);
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
test('scanText: 含数字/符号的混合串仍是真实凭据特征 → 拦截(不回退)', () => {
|
|
768
|
+
const real = [
|
|
769
|
+
'token: abc123-def456', // 含数字的混合串
|
|
770
|
+
'password: AbCdEfGh-1234', // 大小写混合 + 数字 + 连字符(非纯字母数字,不落短标识符豁免)
|
|
771
|
+
'apiKey: sk-proj-9f8e7d6c5b4a3210abcdef', // 真实形态 sk-(无占位词)
|
|
772
|
+
].join('\n');
|
|
773
|
+
const hits = scanText(real);
|
|
774
|
+
assert.ok(hits.length >= 3, `真实凭据特征应拦截,实际: ${JSON.stringify(hits)} (${hits.length})`);
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
test('scanAndRedact: 宽松档(literalValueOnly,市场发布)占位/模板/代码引用放行,真凭据仍拦', () => {
|
|
778
|
+
// 宽松档:字段名敏感但值不是秘密 → 全部放行(市场发布场景,消除误报)
|
|
779
|
+
const benign = {
|
|
780
|
+
token: 'your-token',
|
|
781
|
+
apiKey: '${OPENAI_API_KEY}',
|
|
782
|
+
secret: '{{MY_SECRET}}',
|
|
783
|
+
password: 'process.env.PASSWORD',
|
|
784
|
+
Authorization: 'Bearer <token>',
|
|
785
|
+
clientSecret: 'client-secret-value', // 混合字符但纯小写+连字符(无数字/符号/大小写混合)→ 非凭据
|
|
786
|
+
max_tokens: 100,
|
|
787
|
+
note: 'just a note',
|
|
788
|
+
};
|
|
789
|
+
const r = scanAndRedact(benign, { literalValueOnly: true });
|
|
790
|
+
assert.equal(r.hits.length, 0, `宽松档不应误报,实际: ${JSON.stringify(r.hits)}`);
|
|
791
|
+
assert.deepEqual(r.sanitized, benign);
|
|
792
|
+
|
|
793
|
+
// 宽松档:真实凭据仍然拦截(安全不回退)
|
|
794
|
+
const real = {
|
|
795
|
+
token: 'sk-proj-9f8e7d6c5b4a3210abcdef',
|
|
796
|
+
Authorization: 'Bearer xyz1234567890abcdef',
|
|
797
|
+
clientSecret: 's3cretP@ssw0rdXyZ2024',
|
|
798
|
+
};
|
|
799
|
+
const rr = scanAndRedact(real, { literalValueOnly: true });
|
|
800
|
+
assert.equal(rr.hits.length, 3, `宽松档真凭据仍应拦截,实际: ${JSON.stringify(rr.hits)}`);
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
test('scanAndRedact: 保守档(默认,导出/同步)字段名敏感即剥,不受宽松档影响', () => {
|
|
804
|
+
// 默认模式:字段名敏感 + 任意值 → 剥离(导出脱敏保持保守,行为不变)
|
|
805
|
+
const input = { token: 'your-token', apiKey: '${OPENAI_API_KEY}' };
|
|
806
|
+
const { sanitized } = scanAndRedact(input);
|
|
807
|
+
const s = sanitized as Record<string, unknown>;
|
|
808
|
+
assert.equal(s['token'], REDACTED_PLACEHOLDER, '保守档字段名敏感即剥(占位符值也剥)');
|
|
809
|
+
assert.equal(s['apiKey'], REDACTED_PLACEHOLDER, '保守档模板引用值也剥');
|
|
810
|
+
});
|
|
811
|
+
|
|
703
812
|
/* ================= redaction ================= */
|
|
704
813
|
|
|
705
814
|
test('redaction: 结构化字段三形态 + 值形状 + URL query', () => {
|