@modusensus/dsh-mneme 0.8.14 → 0.8.15
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 +21 -21
- package/README.md +655 -654
- package/assets/icon.svg +3 -3
- package/bin/cli.mjs +689 -689
- package/bin/dsh-mneme-mcp.mjs +510 -510
- package/bin/dsh-mneme-serve.mjs +151 -151
- package/cordis.patch.yml +21 -21
- package/docs/CONFIGURATION.md +263 -0
- package/docs/CONTINUITY.md +124 -0
- package/docs/DAEMON.md +46 -0
- package/docs/ENTITIES.md +245 -0
- package/docs/LOCAL_MODEL.md +222 -0
- package/docs/SEMANTIC.md +256 -0
- package/docs/SLEEP.md +164 -0
- package/docs/STORAGE.md +89 -0
- package/lib/api-standalone.js +673 -673
- package/lib/api.js +1437 -1433
- package/lib/bootstrap.js +211 -211
- package/lib/client.js +5444 -5295
- package/lib/commands.js +95 -95
- package/lib/config.js +880 -843
- package/lib/content-hash.js +54 -54
- package/lib/continuity.js +220 -220
- package/lib/document-index.js +94 -94
- package/lib/document.js +303 -303
- package/lib/dream/clustering.js +118 -118
- package/lib/dream/decisions.js +674 -674
- package/lib/dream/narratives.js +44 -44
- package/lib/dream/sleep.js +819 -819
- package/lib/dream.js +1501 -1501
- package/lib/embedding.js +177 -177
- package/lib/entities/extractor.js +252 -252
- package/lib/graph/anchoring.js +179 -179
- package/lib/guide.js +74 -74
- package/lib/heat.js +144 -144
- package/lib/hot-memory.js +46 -46
- package/lib/index.js +535 -503
- package/lib/inject.js +464 -450
- package/lib/lang.js +248 -229
- package/lib/local-embedder.js +358 -358
- package/lib/maintenance.js +142 -142
- package/lib/mirror.js +330 -330
- package/lib/organize.js +450 -450
- package/lib/peak-hours.js +117 -117
- package/lib/placement.js +13 -0
- package/lib/pre-inject-gate.js +340 -0
- package/lib/quality-filter.js +126 -126
- package/lib/recall-stats.js +175 -175
- package/lib/reranker.js +269 -269
- package/lib/runtime/adopt.js +176 -176
- package/lib/runtime/closure.js +172 -172
- package/lib/runtime/download.js +247 -247
- package/lib/runtime/layout.js +265 -265
- package/lib/runtime/loader.js +252 -252
- package/lib/runtime/model-fetch.js +361 -361
- package/lib/runtime/provision.js +237 -237
- package/lib/runtime/tarball.js +130 -130
- package/lib/runtime/verify.js +223 -204
- package/lib/scope.js +146 -146
- package/lib/search/adaptive.js +60 -60
- package/lib/search/bm25.js +96 -96
- package/lib/semantic.js +229 -229
- package/lib/sensitive-scan.js +209 -209
- package/lib/serve.js +215 -215
- package/lib/service.js +2663 -2605
- package/lib/settings.js +517 -511
- package/lib/store.js +3007 -3007
- package/lib/summarize.js +891 -856
- package/lib/tools.js +743 -737
- package/lib/vector-index.js +110 -110
- package/lib/version-check.js +122 -122
- package/lib/write-admission.js +402 -402
- package/locale/en.json +6 -6
- package/locale/zh.json +6 -6
- package/package.json +118 -118
- package/runtime-manifest.json +526 -526
- package/scripts/benchmark-embed.js +201 -201
- package/scripts/benchmark-recall.js +249 -249
- package/scripts/benchmark-rerank.js +166 -166
- package/scripts/build-runtime-manifest.mjs +224 -224
- package/scripts/changelog-prep.mjs +36 -36
- package/scripts/check-sync.js +42 -42
- package/scripts/e2e-dsh.js +220 -220
- package/scripts/mneme-runtime.mjs +277 -277
- package/scripts/stress-dsh.js +255 -255
- package/scripts/sync-lib.js +47 -47
- package/scripts/sync-test-badge.mjs +69 -69
- package/scripts/test-count-sync.mjs +44 -44
- package/src/api-standalone.js +673 -673
- package/src/api.js +1437 -1433
- package/src/bootstrap.js +211 -211
- package/src/commands.js +95 -95
- package/src/config.js +880 -843
- package/src/content-hash.js +54 -54
- package/src/continuity.js +220 -220
- package/src/document-index.js +94 -94
- package/src/document.js +303 -303
- package/src/dream/clustering.js +118 -118
- package/src/dream/decisions.js +674 -674
- package/src/dream/narratives.js +44 -44
- package/src/dream/sleep.js +819 -819
- package/src/dream.js +1501 -1501
- package/src/embedding.js +177 -177
- package/src/entities/extractor.js +252 -252
- package/src/graph/anchoring.js +179 -179
- package/src/guide.js +74 -74
- package/src/heat.js +144 -144
- package/src/hot-memory.js +46 -46
- package/src/index.js +535 -503
- package/src/inject.js +464 -450
- package/src/lang.js +248 -229
- package/src/local-embedder.js +358 -358
- package/src/maintenance.js +142 -142
- package/src/mirror.js +330 -330
- package/src/organize.js +450 -450
- package/src/peak-hours.js +117 -117
- package/src/placement.js +13 -0
- package/src/pre-inject-gate.js +340 -0
- package/src/quality-filter.js +126 -126
- package/src/recall-stats.js +175 -175
- package/src/reranker.js +269 -269
- package/src/runtime/adopt.js +176 -176
- package/src/runtime/closure.js +172 -172
- package/src/runtime/download.js +247 -247
- package/src/runtime/layout.js +265 -265
- package/src/runtime/loader.js +252 -252
- package/src/runtime/model-fetch.js +361 -361
- package/src/runtime/provision.js +237 -237
- package/src/runtime/tarball.js +130 -130
- package/src/runtime/verify.js +223 -204
- package/src/scope.js +146 -146
- package/src/search/adaptive.js +60 -60
- package/src/search/bm25.js +96 -96
- package/src/semantic.js +229 -229
- package/src/sensitive-scan.js +209 -209
- package/src/serve.js +215 -215
- package/src/service.js +2663 -2605
- package/src/settings.js +517 -511
- package/src/store.js +3007 -3007
- package/src/summarize.js +891 -856
- package/src/tools.js +743 -737
- package/src/vector-index.js +110 -110
- package/src/version-check.js +122 -122
- package/src/write-admission.js +402 -402
- package/test/adaptive-inject-budget.test.js +85 -85
- package/test/api-routes-gaps.test.js +218 -218
- package/test/api.test.js +1702 -1690
- package/test/audit.test.js +448 -448
- package/test/baseline-persist.test.js +248 -248
- package/test/benchmark.test.js +94 -94
- package/test/bootstrap.test.js +220 -220
- package/test/changelog-prep.test.js +60 -60
- package/test/client.test.js +1230 -1169
- package/test/clustering.test.js +100 -100
- package/test/commands.test.js +106 -106
- package/test/config.test.js +66 -66
- package/test/conflict-freeze.test.js +300 -300
- package/test/content-hash.test.js +204 -204
- package/test/continuity.test.js +333 -333
- package/test/document-dir.test.js +233 -233
- package/test/document.test.js +671 -671
- package/test/dream-archive-guardrail.test.js +116 -116
- package/test/dream-candidate-hybrid.test.js +281 -281
- package/test/dream-coverage-degrade.test.js +86 -86
- package/test/dream-failure-backoff.test.js +173 -173
- package/test/dream-peak-hours.test.js +194 -194
- package/test/dream.test.js +1385 -1383
- package/test/embedding-ready.test.js +196 -196
- package/test/entities.test.js +627 -547
- package/test/entity-recall.test.js +179 -179
- package/test/epistemic.test.js +325 -325
- package/test/fnew-0112.test.js +311 -311
- package/test/fnew-03.test.js +422 -422
- package/test/graph-anchoring.test.js +184 -184
- package/test/graph-api.test.js +175 -175
- package/test/graph-inject-budget.test.js +103 -103
- package/test/graph-passive-confirm.test.js +91 -91
- package/test/graph-weight-evolution.test.js +104 -104
- package/test/heat.test.js +175 -175
- package/test/helpers/dream-mock.js +82 -82
- package/test/helpers/peer-worker.mjs +25 -25
- package/test/helpers/runtime-source.js +60 -60
- package/test/helpers/tar-builder.js +80 -80
- package/test/helpers/write-admission-samples.js +308 -308
- package/test/hot-memory.test.js +145 -145
- package/test/inject-content-cap.test.js +91 -91
- package/test/inject-first-round-bm25.test.js +89 -89
- package/test/inject-heat.test.js +77 -77
- package/test/inject-parent-gate.test.js +106 -106
- package/test/inject-pools.test.js +194 -194
- package/test/inject-recall.test.js +105 -105
- package/test/inject-rotation.test.js +161 -161
- package/test/inject.test.js +420 -420
- package/test/lang.test.js +87 -87
- package/test/lastrunat-persist.test.js +101 -101
- package/test/lib-smoke.test.js +96 -96
- package/test/llm-audit.test.js +598 -598
- package/test/llm-message-source.test.js +207 -207
- package/test/local-embedder.test.js +292 -292
- package/test/maintenance.test.js +391 -391
- package/test/mcp-stdio.test.js +385 -385
- package/test/mirror-dirty.test.js +424 -424
- package/test/mirror-edit-digest.test.js +187 -187
- package/test/mirror-generation.test.js +499 -499
- package/test/mirror-types.test.js +208 -208
- package/test/mirror.test.js +249 -249
- package/test/narrative-bars.test.js +162 -162
- package/test/organize.test.js +418 -418
- package/test/peer-blockers.test.js +193 -193
- package/test/placement.test.js +36 -0
- package/test/policy-epoch.test.js +259 -259
- package/test/pre-inject-gate.test.js +434 -0
- package/test/quality-filter.test.js +186 -186
- package/test/reasoning-effort.test.js +752 -752
- package/test/recall-anti-update.test.js +82 -82
- package/test/recall-evals.test.js +253 -253
- package/test/recall-layer.test.js +326 -326
- package/test/recall-stats.test.js +189 -189
- package/test/receipt-chain.test.js +451 -451
- package/test/reflection.test.js +226 -226
- package/test/reindex-backfill.test.js +62 -62
- package/test/reranker-single-logit.test.js +73 -73
- package/test/reranker.test.js +197 -197
- package/test/resident-status.test.js +83 -83
- package/test/runtime-adopt.test.js +206 -206
- package/test/runtime-cli.test.js +89 -89
- package/test/runtime-closure.test.js +193 -193
- package/test/runtime-download.test.js +302 -302
- package/test/runtime-layout.test.js +163 -163
- package/test/runtime-loader.test.js +255 -255
- package/test/runtime-manifest.test.js +159 -159
- package/test/runtime-model-fetch.test.js +348 -348
- package/test/runtime-provision.test.js +101 -101
- package/test/runtime-tarball.test.js +81 -81
- package/test/runtime-verify.test.js +319 -279
- package/test/scope-candidates.test.js +239 -239
- package/test/scope-explicit.test.js +452 -452
- package/test/scope-inject-soft.test.js +106 -106
- package/test/scope-retrieval.test.js +197 -197
- package/test/scope-storage.test.js +255 -255
- package/test/scope-strict.test.js +367 -367
- package/test/search-fusion.test.js +90 -90
- package/test/semantic.test.js +124 -124
- package/test/sensitive-scan.test.js +207 -207
- package/test/serve-bin.test.js +121 -121
- package/test/serve.test.js +196 -196
- package/test/service-search.test.js +174 -174
- package/test/service.test.js +405 -405
- package/test/settings.test.js +261 -261
- package/test/skip-invalid-restore.test.js +262 -262
- package/test/sleep-action-set.test.js +422 -422
- package/test/sleep-heat.test.js +126 -126
- package/test/sleep-scheduler-rearm.test.js +89 -89
- package/test/sleep.test.js +527 -527
- package/test/standalone-api.test.js +751 -751
- package/test/store-vector-cache.test.js +98 -98
- package/test/store.test.js +396 -396
- package/test/stress.test.js +209 -209
- package/test/summarize-occurred-at.test.js +127 -0
- package/test/summarize-reasoning-effort.test.js +189 -189
- package/test/summarize.test.js +1465 -1465
- package/test/supersede-state-tracking.test.js +394 -0
- package/test/test-count-sync.test.js +60 -60
- package/test/tools-dto-schema-matrix.test.js +325 -325
- package/test/tools-memory-get-schema.test.js +65 -65
- package/test/tools.test.js +428 -374
- package/test/updated-at-semantics.test.js +113 -113
- package/test/vector-index.test.js +221 -221
- package/test/version-check.test.js +152 -152
- package/test/webServer-optional.test.js +82 -82
- package/test/write-admission.test.js +668 -668
|
@@ -1,308 +1,308 @@
|
|
|
1
|
-
// #254 第 1 级「密钥 / PII」判据的回归样本集。
|
|
2
|
-
//
|
|
3
|
-
// 这份语料是给**判据**用的,不是给闸门用的:闸门(src/write-admission.js)只消费
|
|
4
|
-
// `sensitiveScan` 返回的 {kind,label},判据本身是 #164 A2 的落地范围(记在维护者
|
|
5
|
-
// 排期里)。语料放在这里有两个用途——判据落地时有一份现成的正负样本可跑;现在就能
|
|
6
|
-
// 把「接线通不通」钉住(下面导出的 referenceScan 是只给测试用的参考实现)。
|
|
7
|
-
//
|
|
8
|
-
// 三条口径:
|
|
9
|
-
//
|
|
10
|
-
// 1. 正样本分两类。SECRET_SAMPLES(密钥凭据)与 PII_SAMPLES(个人信息)都收,但
|
|
11
|
-
// 两者的误杀面差一个量级:密钥串在正常项目记忆里几乎不出现(出现了就是事故),
|
|
12
|
-
// 而邮箱 / 电话 / 订单号在项目记忆里完全可能是正当内容。所以 `kind` 必须落进
|
|
13
|
-
// 审计行(见 write-admission.js 的 record)——enforce 打开前先按 kind 看命中
|
|
14
|
-
// 分布,将来要按类放行也只改策略、不动判据。
|
|
15
|
-
//
|
|
16
|
-
// 2. 负样本专挑两类最容易误杀的文本:**含敏感词但没有值**("把 API key 放进环境
|
|
17
|
-
// 变量")、**高熵但不是凭据**(sha256 校验和、UUID、Luhn 合法的示例卡号之外的
|
|
18
|
-
// 长数字)。一个只按关键词命中的扫描器会在这组里全军覆没,这正是这组样本的
|
|
19
|
-
// 用处——它量的不是「抓得住多少」,而是「抓错多少」。
|
|
20
|
-
//
|
|
21
|
-
// 3. 样本里的凭据都是公开占位值(AWS 官方文档的 AKIAIOSFODNN7EXAMPLE、GitHub /
|
|
22
|
-
// OpenAI / Stripe 的文档格式、Stripe 与银行的官方测试卡号),不是真凭据。
|
|
23
|
-
//
|
|
24
|
-
// 用法(测试侧):
|
|
25
|
-
// import { allSamples, NEGATIVE_SAMPLES, referenceScan } from "./helpers/write-admission-samples.js";
|
|
26
|
-
|
|
27
|
-
// 这几条样本的**值**在密钥扫描器眼里就是真凭据的形状。字面量直接写进仓库有两重代价:
|
|
28
|
-
// GitHub 侧 push protection 会直接拒推(GH013),CI 的 gitleaks 全历史扫描会红掉
|
|
29
|
-
// (.github/workflows/security.yml)。拆成拼接后仓库文本里不再含完整形状,而运行时拼
|
|
30
|
-
// 出来的值仍然是判据要面对的那个形状——语料测的东西一点没变。
|
|
31
|
-
//
|
|
32
|
-
// 两套扫描器的规则**并不重合**:push protection 拦的是 Slack / Stripe,gitleaks 拦的是
|
|
33
|
-
// GitHub PAT / JWT。所以这几条统一走拼接,别按「上一次哪条红了」逐条打补丁——扫描器
|
|
34
|
-
// 升级一次规则就又得回来补。
|
|
35
|
-
//
|
|
36
|
-
// 别为了绕开扫描器把它们简化成一眼假的占位串:正样本的价值恰恰在于压在检测器的形状
|
|
37
|
-
// 边界上,改成 `xoxb-xxx` 就等于把这条样本删了。AWS 那条用的是官方文档自己的示例值
|
|
38
|
-
// AKIAIOSFODNN7EXAMPLE,两套扫描器本来就认它是样例,保持字面量。
|
|
39
|
-
const assemble = (...parts) => parts.join("");
|
|
40
|
-
|
|
41
|
-
/** 语料里那几条被扫描器盯着形状的凭据值。测试侧要拼句子时从这里取,别另抄一份。 */
|
|
42
|
-
export const SAMPLE_VALUES = {
|
|
43
|
-
githubPat: assemble("ghp_", "16C7e42F292c6912E7710c838347Ae178B4a"),
|
|
44
|
-
slackToken: assemble("xoxb-", "123456789012-1234567890123-AbCdEfGhIjKlMnOpQrStUvWx"),
|
|
45
|
-
jwt: assemble(
|
|
46
|
-
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9",
|
|
47
|
-
".eyJzdWIiOiIxMjM0NTY3ODkwIn0",
|
|
48
|
-
".dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U"
|
|
49
|
-
),
|
|
50
|
-
stripeKey: assemble("sk_", "live_", "51H8Qw2KZvN3mPqRsTuVwXyZ0123456789abcdef")
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
/** 密钥凭据类正样本。kind 与 label 是判据该报回来的东西。 */
|
|
54
|
-
export const SECRET_SAMPLES = [
|
|
55
|
-
{
|
|
56
|
-
id: "aws-access-key",
|
|
57
|
-
kind: "aws_access_key",
|
|
58
|
-
label: "AWS access key id",
|
|
59
|
-
title: "部署脚本",
|
|
60
|
-
content: "部署脚本里写死了 AKIAIOSFODNN7EXAMPLE,回头要换掉",
|
|
61
|
-
why: "AKIA + 16 位大写字母数字,长度与字符集都固定"
|
|
62
|
-
},
|
|
63
|
-
{
|
|
64
|
-
id: "github-pat",
|
|
65
|
-
kind: "github_token",
|
|
66
|
-
label: "GitHub token",
|
|
67
|
-
title: "CI 配置",
|
|
68
|
-
content: `CI 的 token 是 ${SAMPLE_VALUES.githubPat},别再贴出来`,
|
|
69
|
-
why: "ghp_ 前缀 + 36 位,GitHub PAT 的固定形状"
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
id: "openai-key",
|
|
73
|
-
kind: "openai_key",
|
|
74
|
-
label: "OpenAI API key",
|
|
75
|
-
title: "调试记录",
|
|
76
|
-
content: "临时用 sk-proj-abcdefghijklmnopqrstuvwxyz0123456789ABCD 调了一次",
|
|
77
|
-
why: "sk-/sk-proj- 前缀 + 长串"
|
|
78
|
-
},
|
|
79
|
-
{
|
|
80
|
-
id: "slack-token",
|
|
81
|
-
kind: "slack_token",
|
|
82
|
-
label: "Slack token",
|
|
83
|
-
title: "webhook",
|
|
84
|
-
content: `webhook 配的是 ${SAMPLE_VALUES.slackToken}`,
|
|
85
|
-
why: "xoxb- 前缀 + 分段数字 + 混合串"
|
|
86
|
-
},
|
|
87
|
-
{
|
|
88
|
-
id: "pem-private-key",
|
|
89
|
-
kind: "private_key",
|
|
90
|
-
label: "PEM private key",
|
|
91
|
-
title: "issue 正文",
|
|
92
|
-
content: "把 -----BEGIN RSA PRIVATE KEY----- 整段贴到了 issue 里",
|
|
93
|
-
why: "PEM 头是明文私钥的确定标志,有没有正文都不影响判定"
|
|
94
|
-
},
|
|
95
|
-
{
|
|
96
|
-
id: "assigned-literal",
|
|
97
|
-
kind: "assigned_secret",
|
|
98
|
-
label: "assigned credential literal",
|
|
99
|
-
title: "本地配置",
|
|
100
|
-
content: 'config 里 password = "Tr0ub4dor&3xKcd" 这一行得删掉',
|
|
101
|
-
why: "赋值号右边是字面量凭据——这是最常见的一类,也是最需要防误杀的一类"
|
|
102
|
-
},
|
|
103
|
-
{
|
|
104
|
-
id: "jwt",
|
|
105
|
-
kind: "jwt",
|
|
106
|
-
label: "JSON Web Token",
|
|
107
|
-
title: "抓包",
|
|
108
|
-
content: `抓到的 Authorization 是 ${SAMPLE_VALUES.jwt}`,
|
|
109
|
-
why: "三段 base64url,header 恒以 eyJ 开头"
|
|
110
|
-
},
|
|
111
|
-
{
|
|
112
|
-
id: "connection-string",
|
|
113
|
-
kind: "connection_string",
|
|
114
|
-
label: "credentials in URL",
|
|
115
|
-
title: "连不上",
|
|
116
|
-
content: "postgres://admin:S3cr3tPa55w0rd@db.internal:5432/prod 连不上",
|
|
117
|
-
why: "URL 里带 user:password@host 就是明文凭据"
|
|
118
|
-
},
|
|
119
|
-
{
|
|
120
|
-
id: "stripe-key",
|
|
121
|
-
kind: "stripe_key",
|
|
122
|
-
label: "Stripe secret key",
|
|
123
|
-
title: "支付联调",
|
|
124
|
-
content: `联调用的 ${SAMPLE_VALUES.stripeKey} 忘了轮换`,
|
|
125
|
-
why: "sk_live_ 前缀是生产密钥(测试密钥是 sk_test_,不该报)"
|
|
126
|
-
},
|
|
127
|
-
{
|
|
128
|
-
id: "npm-auth-token",
|
|
129
|
-
kind: "npm_token",
|
|
130
|
-
label: "npm auth token",
|
|
131
|
-
title: ".npmrc",
|
|
132
|
-
content: "//registry.npmjs.org/:_authToken=npm_abcdefghijklmnopqrstuvwxyz0123456789",
|
|
133
|
-
why: "按 _authToken= 这个固定键定位,不必猜 token 的形状"
|
|
134
|
-
}
|
|
135
|
-
];
|
|
136
|
-
|
|
137
|
-
/** PII 类正样本。误杀面比密钥大一档,见文件头第 1 条。 */
|
|
138
|
-
export const PII_SAMPLES = [
|
|
139
|
-
{
|
|
140
|
-
id: "email-phone",
|
|
141
|
-
kind: "email",
|
|
142
|
-
label: "email address",
|
|
143
|
-
title: "客户联系方式",
|
|
144
|
-
content: "客户的联系方式是 zhang.wei@example.com,别外传",
|
|
145
|
-
why: "RFC 形状的邮箱;项目记忆里出现它是可能的,所以更适合先告警"
|
|
146
|
-
},
|
|
147
|
-
{
|
|
148
|
-
id: "cn-mobile",
|
|
149
|
-
kind: "cn_mobile",
|
|
150
|
-
label: "mainland mobile number",
|
|
151
|
-
title: "值班表",
|
|
152
|
-
content: "值班手机 13800138000,紧急情况打这个",
|
|
153
|
-
why: "1[3-9] + 9 位,中国手机号的固定形状"
|
|
154
|
-
},
|
|
155
|
-
{
|
|
156
|
-
id: "cn-id-card",
|
|
157
|
-
kind: "cn_id_card",
|
|
158
|
-
label: "mainland ID number",
|
|
159
|
-
title: "实名核验",
|
|
160
|
-
content: "身份证 11010519491231002X 已经核验过",
|
|
161
|
-
why: "17 位数字 + 校验位(数字或 X)"
|
|
162
|
-
},
|
|
163
|
-
{
|
|
164
|
-
id: "bank-card",
|
|
165
|
-
kind: "bank_card",
|
|
166
|
-
label: "payment card number",
|
|
167
|
-
title: "对公账户",
|
|
168
|
-
content: "银行卡 4111111111111111 是对公账户",
|
|
169
|
-
why: "4 开头的 16 位;这里用 Visa 的公开测试号,不用真卡号"
|
|
170
|
-
}
|
|
171
|
-
];
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* 负样本:必须**一条都不命中**。每条都写清它为什么容易误杀。
|
|
175
|
-
* 这是本文件里最值钱的部分——正样本只证明「抓得住」,负样本才约束「抓错多少」。
|
|
176
|
-
*/
|
|
177
|
-
export const NEGATIVE_SAMPLES = [
|
|
178
|
-
{
|
|
179
|
-
id: "keyword-no-value",
|
|
180
|
-
title: "密钥管理",
|
|
181
|
-
content: "把 API key 放进环境变量,别写进代码——轮换流程见 docs/handbook/12-secrets.md",
|
|
182
|
-
why: "整句在讨论凭据管理,没有一个字面量值"
|
|
183
|
-
},
|
|
184
|
-
{
|
|
185
|
-
id: "token-as-noun",
|
|
186
|
-
title: "重试策略",
|
|
187
|
-
content: "token 刷新失败时退避重试三次,第三次仍失败就报错",
|
|
188
|
-
why: "token 是普通名词,后面没有赋值号"
|
|
189
|
-
},
|
|
190
|
-
{
|
|
191
|
-
id: "redaction-placeholder",
|
|
192
|
-
title: "脱敏示例",
|
|
193
|
-
content: "password: <redacted>",
|
|
194
|
-
why: "占位符不是值——纯关键词扫描器在这里必错"
|
|
195
|
-
},
|
|
196
|
-
{
|
|
197
|
-
id: "env-var-indirection",
|
|
198
|
-
title: "配置读取",
|
|
199
|
-
content: "secret = process.env.MNEME_SECRET",
|
|
200
|
-
why: "赋值右边是环境变量读取,不是字面量"
|
|
201
|
-
},
|
|
202
|
-
{
|
|
203
|
-
id: "template-var",
|
|
204
|
-
title: "模板",
|
|
205
|
-
// 这条样本**故意**就是一个模板占位符——它量的正是「看到 = 或 : 就报」的误杀。
|
|
206
|
-
// biome-ignore lint/suspicious/noTemplateCurlyInString: 样本内容本身就是待测文本,不是要插值的模板
|
|
207
|
-
content: "token: ${TOKEN}",
|
|
208
|
-
why: "模板变量同理——看到 = 或 : 就报会误杀这一整类"
|
|
209
|
-
},
|
|
210
|
-
{
|
|
211
|
-
id: "hash-not-secret",
|
|
212
|
-
title: "校验和",
|
|
213
|
-
content: "sha256 摘要 3f786850e387550fdab836ed7e6dc881de23001b 不是密钥,只是一个校验和",
|
|
214
|
-
why: "高熵十六进制串,但不是凭据;按熵阈值判会误杀全部哈希"
|
|
215
|
-
},
|
|
216
|
-
{
|
|
217
|
-
id: "uuid",
|
|
218
|
-
title: "订单号",
|
|
219
|
-
content: "订单号 550e8400-e29b-41d4-a716-446655440000 是 UUID v4",
|
|
220
|
-
why: "同样是高熵串;UUID 在项目记忆里到处都是"
|
|
221
|
-
},
|
|
222
|
-
{
|
|
223
|
-
id: "prefix-only",
|
|
224
|
-
title: "格式说明",
|
|
225
|
-
content: "GitHub PAT 的前缀是 ghp_,新格式是 github_pat_",
|
|
226
|
-
why: "只提到前缀,后面没有 token 正文"
|
|
227
|
-
},
|
|
228
|
-
{
|
|
229
|
-
id: "sk-prefix-only",
|
|
230
|
-
title: "key 前缀",
|
|
231
|
-
content: "sk- 开头的是 OpenAI 的 key 前缀",
|
|
232
|
-
why: "同上——前缀本身不是凭据"
|
|
233
|
-
},
|
|
234
|
-
{
|
|
235
|
-
id: "discuss-rotation",
|
|
236
|
-
title: "轮换周期",
|
|
237
|
-
content: "把密钥轮换周期定成 90 天,到期前一周提醒",
|
|
238
|
-
why: "在讨论密钥的生命周期"
|
|
239
|
-
},
|
|
240
|
-
{
|
|
241
|
-
id: "discuss-hashing",
|
|
242
|
-
title: "密码处理",
|
|
243
|
-
content: "用户在设置页输入密码后调用 bcrypt.hash 存哈希,明文不落盘",
|
|
244
|
-
why: "讨论密码处理方式,不含任何密码"
|
|
245
|
-
},
|
|
246
|
-
{
|
|
247
|
-
id: "discuss-pii",
|
|
248
|
-
title: "脱敏要求",
|
|
249
|
-
content: "手机号必须脱敏后再入库,日志里也不能出现",
|
|
250
|
-
why: "在讨论 PII 策略,不含具体号码"
|
|
251
|
-
}
|
|
252
|
-
];
|
|
253
|
-
|
|
254
|
-
/** 全部正样本(密钥 + PII),每条都带期望的 kind。 */
|
|
255
|
-
export const POSITIVE_SAMPLES = [...SECRET_SAMPLES, ...PII_SAMPLES];
|
|
256
|
-
|
|
257
|
-
/** 正负样本一起,便于整组遍历。 */
|
|
258
|
-
export const allSamples = [...POSITIVE_SAMPLES, ...NEGATIVE_SAMPLES];
|
|
259
|
-
|
|
260
|
-
// --- 参考扫描器(仅供测试) ---------------------------------------------------
|
|
261
|
-
// 这不是要发货的实现,也**不是** #164 A2 的实现——A2 归维护者排期。它的作用只有
|
|
262
|
-
// 两个:证明闸门侧 `sensitiveScan` 的接线真的能把命中变成拒绝;给上面的语料一个
|
|
263
|
-
// 可执行的对照。A2 落地时把同一个语料原样跑在真扫描器上,这个函数就可以删掉。
|
|
264
|
-
//
|
|
265
|
-
// 写法上刻意体现一件事:**先看形状,再看关键词**。纯关键词匹配在负样本组里会
|
|
266
|
-
// 全军覆没(见 NEGATIVE_SAMPLES 的 why),所以这里每条模式都带长度或字符集约束,
|
|
267
|
-
// 赋值型那条还要多一道占位符守卫。
|
|
268
|
-
const SECRET_PATTERNS = [
|
|
269
|
-
{ kind: "aws_access_key", label: "AWS access key id", re: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/ },
|
|
270
|
-
{ kind: "github_token", label: "GitHub token", re: /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{36}\b/ },
|
|
271
|
-
{ kind: "slack_token", label: "Slack token", re: /\bxox[abprs]-[A-Za-z0-9-]{10,}\b/ },
|
|
272
|
-
{ kind: "private_key", label: "PEM private key", re: /-----BEGIN [A-Z ]*PRIVATE KEY-----/ },
|
|
273
|
-
{ kind: "jwt", label: "JSON Web Token", re: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/ },
|
|
274
|
-
{ kind: "connection_string", label: "credentials in URL", re: /\b[a-z][a-z0-9+.-]*:\/\/[^\s/:@]+:[^\s/:@]{6,}@/ },
|
|
275
|
-
{ kind: "stripe_key", label: "Stripe secret key", re: /\bsk_live_[A-Za-z0-9]{16,}\b/ },
|
|
276
|
-
{ kind: "npm_token", label: "npm auth token", re: /_authToken\s*=\s*[A-Za-z0-9_-]{20,}/ },
|
|
277
|
-
{ kind: "openai_key", label: "OpenAI API key", re: /\bsk-(?:proj-)?[A-Za-z0-9_-]{20,}\b/ },
|
|
278
|
-
{
|
|
279
|
-
kind: "assigned_secret",
|
|
280
|
-
label: "assigned credential literal",
|
|
281
|
-
re: /\b(?:password|passwd|pwd|secret|api[_-]?key|token)\b\s*[:=]\s*["']?([^\s"']{8,})/i,
|
|
282
|
-
// 占位符 / 间接引用守卫:命中关键词但没有字面量值时不算命中。
|
|
283
|
-
guard: (value) => !/^(?:<[^>]*>|\$\{|\$[A-Z_]+$|process\.env|redacted|xx+|\*+|\u2026)/i.test(value)
|
|
284
|
-
}
|
|
285
|
-
];
|
|
286
|
-
|
|
287
|
-
const PII_PATTERNS = [
|
|
288
|
-
{ kind: "email", label: "email address", re: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/ },
|
|
289
|
-
{ kind: "cn_mobile", label: "mainland mobile number", re: /\b1[3-9]\d{9}\b/ },
|
|
290
|
-
{ kind: "cn_id_card", label: "mainland ID number", re: /\b\d{17}[\dXx]\b/ },
|
|
291
|
-
{ kind: "bank_card", label: "payment card number", re: /\b(?:4\d{15}|5[1-5]\d{14}|62\d{14,17})\b/ }
|
|
292
|
-
];
|
|
293
|
-
|
|
294
|
-
/**
|
|
295
|
-
* 参考扫描器:命中返回 `{kind, label}`,未命中返回 null——与
|
|
296
|
-
* `createWriteAdmission` 的 `sensitiveScan` 契约一致(不抛)。
|
|
297
|
-
* 先密钥后 PII:一条文本同时像两类时(如带凭据的 URL 也像邮箱),报更严重的那类。
|
|
298
|
-
*/
|
|
299
|
-
export function referenceScan(memory) {
|
|
300
|
-
const text = [memory?.title, memory?.content].filter((s) => typeof s === "string").join("\n");
|
|
301
|
-
for (const p of [...SECRET_PATTERNS, ...PII_PATTERNS]) {
|
|
302
|
-
const m = text.match(p.re);
|
|
303
|
-
if (!m) continue;
|
|
304
|
-
if (p.guard && !p.guard(m[1] ?? "")) continue;
|
|
305
|
-
return { kind: p.kind, label: p.label };
|
|
306
|
-
}
|
|
307
|
-
return null;
|
|
308
|
-
}
|
|
1
|
+
// #254 第 1 级「密钥 / PII」判据的回归样本集。
|
|
2
|
+
//
|
|
3
|
+
// 这份语料是给**判据**用的,不是给闸门用的:闸门(src/write-admission.js)只消费
|
|
4
|
+
// `sensitiveScan` 返回的 {kind,label},判据本身是 #164 A2 的落地范围(记在维护者
|
|
5
|
+
// 排期里)。语料放在这里有两个用途——判据落地时有一份现成的正负样本可跑;现在就能
|
|
6
|
+
// 把「接线通不通」钉住(下面导出的 referenceScan 是只给测试用的参考实现)。
|
|
7
|
+
//
|
|
8
|
+
// 三条口径:
|
|
9
|
+
//
|
|
10
|
+
// 1. 正样本分两类。SECRET_SAMPLES(密钥凭据)与 PII_SAMPLES(个人信息)都收,但
|
|
11
|
+
// 两者的误杀面差一个量级:密钥串在正常项目记忆里几乎不出现(出现了就是事故),
|
|
12
|
+
// 而邮箱 / 电话 / 订单号在项目记忆里完全可能是正当内容。所以 `kind` 必须落进
|
|
13
|
+
// 审计行(见 write-admission.js 的 record)——enforce 打开前先按 kind 看命中
|
|
14
|
+
// 分布,将来要按类放行也只改策略、不动判据。
|
|
15
|
+
//
|
|
16
|
+
// 2. 负样本专挑两类最容易误杀的文本:**含敏感词但没有值**("把 API key 放进环境
|
|
17
|
+
// 变量")、**高熵但不是凭据**(sha256 校验和、UUID、Luhn 合法的示例卡号之外的
|
|
18
|
+
// 长数字)。一个只按关键词命中的扫描器会在这组里全军覆没,这正是这组样本的
|
|
19
|
+
// 用处——它量的不是「抓得住多少」,而是「抓错多少」。
|
|
20
|
+
//
|
|
21
|
+
// 3. 样本里的凭据都是公开占位值(AWS 官方文档的 AKIAIOSFODNN7EXAMPLE、GitHub /
|
|
22
|
+
// OpenAI / Stripe 的文档格式、Stripe 与银行的官方测试卡号),不是真凭据。
|
|
23
|
+
//
|
|
24
|
+
// 用法(测试侧):
|
|
25
|
+
// import { allSamples, NEGATIVE_SAMPLES, referenceScan } from "./helpers/write-admission-samples.js";
|
|
26
|
+
|
|
27
|
+
// 这几条样本的**值**在密钥扫描器眼里就是真凭据的形状。字面量直接写进仓库有两重代价:
|
|
28
|
+
// GitHub 侧 push protection 会直接拒推(GH013),CI 的 gitleaks 全历史扫描会红掉
|
|
29
|
+
// (.github/workflows/security.yml)。拆成拼接后仓库文本里不再含完整形状,而运行时拼
|
|
30
|
+
// 出来的值仍然是判据要面对的那个形状——语料测的东西一点没变。
|
|
31
|
+
//
|
|
32
|
+
// 两套扫描器的规则**并不重合**:push protection 拦的是 Slack / Stripe,gitleaks 拦的是
|
|
33
|
+
// GitHub PAT / JWT。所以这几条统一走拼接,别按「上一次哪条红了」逐条打补丁——扫描器
|
|
34
|
+
// 升级一次规则就又得回来补。
|
|
35
|
+
//
|
|
36
|
+
// 别为了绕开扫描器把它们简化成一眼假的占位串:正样本的价值恰恰在于压在检测器的形状
|
|
37
|
+
// 边界上,改成 `xoxb-xxx` 就等于把这条样本删了。AWS 那条用的是官方文档自己的示例值
|
|
38
|
+
// AKIAIOSFODNN7EXAMPLE,两套扫描器本来就认它是样例,保持字面量。
|
|
39
|
+
const assemble = (...parts) => parts.join("");
|
|
40
|
+
|
|
41
|
+
/** 语料里那几条被扫描器盯着形状的凭据值。测试侧要拼句子时从这里取,别另抄一份。 */
|
|
42
|
+
export const SAMPLE_VALUES = {
|
|
43
|
+
githubPat: assemble("ghp_", "16C7e42F292c6912E7710c838347Ae178B4a"),
|
|
44
|
+
slackToken: assemble("xoxb-", "123456789012-1234567890123-AbCdEfGhIjKlMnOpQrStUvWx"),
|
|
45
|
+
jwt: assemble(
|
|
46
|
+
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9",
|
|
47
|
+
".eyJzdWIiOiIxMjM0NTY3ODkwIn0",
|
|
48
|
+
".dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U"
|
|
49
|
+
),
|
|
50
|
+
stripeKey: assemble("sk_", "live_", "51H8Qw2KZvN3mPqRsTuVwXyZ0123456789abcdef")
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/** 密钥凭据类正样本。kind 与 label 是判据该报回来的东西。 */
|
|
54
|
+
export const SECRET_SAMPLES = [
|
|
55
|
+
{
|
|
56
|
+
id: "aws-access-key",
|
|
57
|
+
kind: "aws_access_key",
|
|
58
|
+
label: "AWS access key id",
|
|
59
|
+
title: "部署脚本",
|
|
60
|
+
content: "部署脚本里写死了 AKIAIOSFODNN7EXAMPLE,回头要换掉",
|
|
61
|
+
why: "AKIA + 16 位大写字母数字,长度与字符集都固定"
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: "github-pat",
|
|
65
|
+
kind: "github_token",
|
|
66
|
+
label: "GitHub token",
|
|
67
|
+
title: "CI 配置",
|
|
68
|
+
content: `CI 的 token 是 ${SAMPLE_VALUES.githubPat},别再贴出来`,
|
|
69
|
+
why: "ghp_ 前缀 + 36 位,GitHub PAT 的固定形状"
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: "openai-key",
|
|
73
|
+
kind: "openai_key",
|
|
74
|
+
label: "OpenAI API key",
|
|
75
|
+
title: "调试记录",
|
|
76
|
+
content: "临时用 sk-proj-abcdefghijklmnopqrstuvwxyz0123456789ABCD 调了一次",
|
|
77
|
+
why: "sk-/sk-proj- 前缀 + 长串"
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: "slack-token",
|
|
81
|
+
kind: "slack_token",
|
|
82
|
+
label: "Slack token",
|
|
83
|
+
title: "webhook",
|
|
84
|
+
content: `webhook 配的是 ${SAMPLE_VALUES.slackToken}`,
|
|
85
|
+
why: "xoxb- 前缀 + 分段数字 + 混合串"
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
id: "pem-private-key",
|
|
89
|
+
kind: "private_key",
|
|
90
|
+
label: "PEM private key",
|
|
91
|
+
title: "issue 正文",
|
|
92
|
+
content: "把 -----BEGIN RSA PRIVATE KEY----- 整段贴到了 issue 里",
|
|
93
|
+
why: "PEM 头是明文私钥的确定标志,有没有正文都不影响判定"
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
id: "assigned-literal",
|
|
97
|
+
kind: "assigned_secret",
|
|
98
|
+
label: "assigned credential literal",
|
|
99
|
+
title: "本地配置",
|
|
100
|
+
content: 'config 里 password = "Tr0ub4dor&3xKcd" 这一行得删掉',
|
|
101
|
+
why: "赋值号右边是字面量凭据——这是最常见的一类,也是最需要防误杀的一类"
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: "jwt",
|
|
105
|
+
kind: "jwt",
|
|
106
|
+
label: "JSON Web Token",
|
|
107
|
+
title: "抓包",
|
|
108
|
+
content: `抓到的 Authorization 是 ${SAMPLE_VALUES.jwt}`,
|
|
109
|
+
why: "三段 base64url,header 恒以 eyJ 开头"
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
id: "connection-string",
|
|
113
|
+
kind: "connection_string",
|
|
114
|
+
label: "credentials in URL",
|
|
115
|
+
title: "连不上",
|
|
116
|
+
content: "postgres://admin:S3cr3tPa55w0rd@db.internal:5432/prod 连不上",
|
|
117
|
+
why: "URL 里带 user:password@host 就是明文凭据"
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
id: "stripe-key",
|
|
121
|
+
kind: "stripe_key",
|
|
122
|
+
label: "Stripe secret key",
|
|
123
|
+
title: "支付联调",
|
|
124
|
+
content: `联调用的 ${SAMPLE_VALUES.stripeKey} 忘了轮换`,
|
|
125
|
+
why: "sk_live_ 前缀是生产密钥(测试密钥是 sk_test_,不该报)"
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
id: "npm-auth-token",
|
|
129
|
+
kind: "npm_token",
|
|
130
|
+
label: "npm auth token",
|
|
131
|
+
title: ".npmrc",
|
|
132
|
+
content: "//registry.npmjs.org/:_authToken=npm_abcdefghijklmnopqrstuvwxyz0123456789",
|
|
133
|
+
why: "按 _authToken= 这个固定键定位,不必猜 token 的形状"
|
|
134
|
+
}
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
/** PII 类正样本。误杀面比密钥大一档,见文件头第 1 条。 */
|
|
138
|
+
export const PII_SAMPLES = [
|
|
139
|
+
{
|
|
140
|
+
id: "email-phone",
|
|
141
|
+
kind: "email",
|
|
142
|
+
label: "email address",
|
|
143
|
+
title: "客户联系方式",
|
|
144
|
+
content: "客户的联系方式是 zhang.wei@example.com,别外传",
|
|
145
|
+
why: "RFC 形状的邮箱;项目记忆里出现它是可能的,所以更适合先告警"
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
id: "cn-mobile",
|
|
149
|
+
kind: "cn_mobile",
|
|
150
|
+
label: "mainland mobile number",
|
|
151
|
+
title: "值班表",
|
|
152
|
+
content: "值班手机 13800138000,紧急情况打这个",
|
|
153
|
+
why: "1[3-9] + 9 位,中国手机号的固定形状"
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
id: "cn-id-card",
|
|
157
|
+
kind: "cn_id_card",
|
|
158
|
+
label: "mainland ID number",
|
|
159
|
+
title: "实名核验",
|
|
160
|
+
content: "身份证 11010519491231002X 已经核验过",
|
|
161
|
+
why: "17 位数字 + 校验位(数字或 X)"
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
id: "bank-card",
|
|
165
|
+
kind: "bank_card",
|
|
166
|
+
label: "payment card number",
|
|
167
|
+
title: "对公账户",
|
|
168
|
+
content: "银行卡 4111111111111111 是对公账户",
|
|
169
|
+
why: "4 开头的 16 位;这里用 Visa 的公开测试号,不用真卡号"
|
|
170
|
+
}
|
|
171
|
+
];
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* 负样本:必须**一条都不命中**。每条都写清它为什么容易误杀。
|
|
175
|
+
* 这是本文件里最值钱的部分——正样本只证明「抓得住」,负样本才约束「抓错多少」。
|
|
176
|
+
*/
|
|
177
|
+
export const NEGATIVE_SAMPLES = [
|
|
178
|
+
{
|
|
179
|
+
id: "keyword-no-value",
|
|
180
|
+
title: "密钥管理",
|
|
181
|
+
content: "把 API key 放进环境变量,别写进代码——轮换流程见 docs/handbook/12-secrets.md",
|
|
182
|
+
why: "整句在讨论凭据管理,没有一个字面量值"
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
id: "token-as-noun",
|
|
186
|
+
title: "重试策略",
|
|
187
|
+
content: "token 刷新失败时退避重试三次,第三次仍失败就报错",
|
|
188
|
+
why: "token 是普通名词,后面没有赋值号"
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
id: "redaction-placeholder",
|
|
192
|
+
title: "脱敏示例",
|
|
193
|
+
content: "password: <redacted>",
|
|
194
|
+
why: "占位符不是值——纯关键词扫描器在这里必错"
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
id: "env-var-indirection",
|
|
198
|
+
title: "配置读取",
|
|
199
|
+
content: "secret = process.env.MNEME_SECRET",
|
|
200
|
+
why: "赋值右边是环境变量读取,不是字面量"
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
id: "template-var",
|
|
204
|
+
title: "模板",
|
|
205
|
+
// 这条样本**故意**就是一个模板占位符——它量的正是「看到 = 或 : 就报」的误杀。
|
|
206
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: 样本内容本身就是待测文本,不是要插值的模板
|
|
207
|
+
content: "token: ${TOKEN}",
|
|
208
|
+
why: "模板变量同理——看到 = 或 : 就报会误杀这一整类"
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
id: "hash-not-secret",
|
|
212
|
+
title: "校验和",
|
|
213
|
+
content: "sha256 摘要 3f786850e387550fdab836ed7e6dc881de23001b 不是密钥,只是一个校验和",
|
|
214
|
+
why: "高熵十六进制串,但不是凭据;按熵阈值判会误杀全部哈希"
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
id: "uuid",
|
|
218
|
+
title: "订单号",
|
|
219
|
+
content: "订单号 550e8400-e29b-41d4-a716-446655440000 是 UUID v4",
|
|
220
|
+
why: "同样是高熵串;UUID 在项目记忆里到处都是"
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
id: "prefix-only",
|
|
224
|
+
title: "格式说明",
|
|
225
|
+
content: "GitHub PAT 的前缀是 ghp_,新格式是 github_pat_",
|
|
226
|
+
why: "只提到前缀,后面没有 token 正文"
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
id: "sk-prefix-only",
|
|
230
|
+
title: "key 前缀",
|
|
231
|
+
content: "sk- 开头的是 OpenAI 的 key 前缀",
|
|
232
|
+
why: "同上——前缀本身不是凭据"
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
id: "discuss-rotation",
|
|
236
|
+
title: "轮换周期",
|
|
237
|
+
content: "把密钥轮换周期定成 90 天,到期前一周提醒",
|
|
238
|
+
why: "在讨论密钥的生命周期"
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
id: "discuss-hashing",
|
|
242
|
+
title: "密码处理",
|
|
243
|
+
content: "用户在设置页输入密码后调用 bcrypt.hash 存哈希,明文不落盘",
|
|
244
|
+
why: "讨论密码处理方式,不含任何密码"
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
id: "discuss-pii",
|
|
248
|
+
title: "脱敏要求",
|
|
249
|
+
content: "手机号必须脱敏后再入库,日志里也不能出现",
|
|
250
|
+
why: "在讨论 PII 策略,不含具体号码"
|
|
251
|
+
}
|
|
252
|
+
];
|
|
253
|
+
|
|
254
|
+
/** 全部正样本(密钥 + PII),每条都带期望的 kind。 */
|
|
255
|
+
export const POSITIVE_SAMPLES = [...SECRET_SAMPLES, ...PII_SAMPLES];
|
|
256
|
+
|
|
257
|
+
/** 正负样本一起,便于整组遍历。 */
|
|
258
|
+
export const allSamples = [...POSITIVE_SAMPLES, ...NEGATIVE_SAMPLES];
|
|
259
|
+
|
|
260
|
+
// --- 参考扫描器(仅供测试) ---------------------------------------------------
|
|
261
|
+
// 这不是要发货的实现,也**不是** #164 A2 的实现——A2 归维护者排期。它的作用只有
|
|
262
|
+
// 两个:证明闸门侧 `sensitiveScan` 的接线真的能把命中变成拒绝;给上面的语料一个
|
|
263
|
+
// 可执行的对照。A2 落地时把同一个语料原样跑在真扫描器上,这个函数就可以删掉。
|
|
264
|
+
//
|
|
265
|
+
// 写法上刻意体现一件事:**先看形状,再看关键词**。纯关键词匹配在负样本组里会
|
|
266
|
+
// 全军覆没(见 NEGATIVE_SAMPLES 的 why),所以这里每条模式都带长度或字符集约束,
|
|
267
|
+
// 赋值型那条还要多一道占位符守卫。
|
|
268
|
+
const SECRET_PATTERNS = [
|
|
269
|
+
{ kind: "aws_access_key", label: "AWS access key id", re: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/ },
|
|
270
|
+
{ kind: "github_token", label: "GitHub token", re: /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{36}\b/ },
|
|
271
|
+
{ kind: "slack_token", label: "Slack token", re: /\bxox[abprs]-[A-Za-z0-9-]{10,}\b/ },
|
|
272
|
+
{ kind: "private_key", label: "PEM private key", re: /-----BEGIN [A-Z ]*PRIVATE KEY-----/ },
|
|
273
|
+
{ kind: "jwt", label: "JSON Web Token", re: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/ },
|
|
274
|
+
{ kind: "connection_string", label: "credentials in URL", re: /\b[a-z][a-z0-9+.-]*:\/\/[^\s/:@]+:[^\s/:@]{6,}@/ },
|
|
275
|
+
{ kind: "stripe_key", label: "Stripe secret key", re: /\bsk_live_[A-Za-z0-9]{16,}\b/ },
|
|
276
|
+
{ kind: "npm_token", label: "npm auth token", re: /_authToken\s*=\s*[A-Za-z0-9_-]{20,}/ },
|
|
277
|
+
{ kind: "openai_key", label: "OpenAI API key", re: /\bsk-(?:proj-)?[A-Za-z0-9_-]{20,}\b/ },
|
|
278
|
+
{
|
|
279
|
+
kind: "assigned_secret",
|
|
280
|
+
label: "assigned credential literal",
|
|
281
|
+
re: /\b(?:password|passwd|pwd|secret|api[_-]?key|token)\b\s*[:=]\s*["']?([^\s"']{8,})/i,
|
|
282
|
+
// 占位符 / 间接引用守卫:命中关键词但没有字面量值时不算命中。
|
|
283
|
+
guard: (value) => !/^(?:<[^>]*>|\$\{|\$[A-Z_]+$|process\.env|redacted|xx+|\*+|\u2026)/i.test(value)
|
|
284
|
+
}
|
|
285
|
+
];
|
|
286
|
+
|
|
287
|
+
const PII_PATTERNS = [
|
|
288
|
+
{ kind: "email", label: "email address", re: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/ },
|
|
289
|
+
{ kind: "cn_mobile", label: "mainland mobile number", re: /\b1[3-9]\d{9}\b/ },
|
|
290
|
+
{ kind: "cn_id_card", label: "mainland ID number", re: /\b\d{17}[\dXx]\b/ },
|
|
291
|
+
{ kind: "bank_card", label: "payment card number", re: /\b(?:4\d{15}|5[1-5]\d{14}|62\d{14,17})\b/ }
|
|
292
|
+
];
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* 参考扫描器:命中返回 `{kind, label}`,未命中返回 null——与
|
|
296
|
+
* `createWriteAdmission` 的 `sensitiveScan` 契约一致(不抛)。
|
|
297
|
+
* 先密钥后 PII:一条文本同时像两类时(如带凭据的 URL 也像邮箱),报更严重的那类。
|
|
298
|
+
*/
|
|
299
|
+
export function referenceScan(memory) {
|
|
300
|
+
const text = [memory?.title, memory?.content].filter((s) => typeof s === "string").join("\n");
|
|
301
|
+
for (const p of [...SECRET_PATTERNS, ...PII_PATTERNS]) {
|
|
302
|
+
const m = text.match(p.re);
|
|
303
|
+
if (!m) continue;
|
|
304
|
+
if (p.guard && !p.guard(m[1] ?? "")) continue;
|
|
305
|
+
return { kind: p.kind, label: p.label };
|
|
306
|
+
}
|
|
307
|
+
return null;
|
|
308
|
+
}
|