提交代码前先查一遍密钥我用华为云码道 CodeArts 做了个查一下一键开通华为云码道 CodeArts 代码智能体一个前端项目做完页面跑通以后往往还要整理源码提交到仓库发给同事参考或者作为示例公开。这时候除了清理调试代码还有一件事需要确认开发时用过的 API Key、Token有没有留在项目里。检查起来未必只是打开一个配置文件。临时验证接口写过的脚本、排查问题保存的日志、从文档里复制后改过的示例都可能留下凭证。文件少的时候可以挨个翻项目稍微大一点就得借助全局搜索。但搜索什么也有点麻烦。搜key会找到不少普通属性和变量搜token既有需要检查的值也有正常的类型定义。换一种服务凭证的名字和格式又可能不同。搜索能帮忙缩小范围剩下的仍然要一处处确认。我想把这样的检查做成一个小工具将项目文件拖进浏览器找出疑似硬编码的密钥标出它位于哪个文件、哪一行再给出处理建议。待检查的内容本身可能包含敏感信息因此文件读取和扫描都放在本地结果里的疑似密钥只显示打码后的内容。这个工具叫查一下。这次我用华为云码道 CodeArts 来开发从扫描规则开始逐步接上文件读取和浏览器界面也做了一个能直接检查本地目录的命令行入口。先看做出来的效果。页面支持拖入文件或文件夹也能直接粘贴一段文本。扫描结束后结果按文件排列每一项都能看到风险级别、行列位置和处理建议。下面用内置示例演示里面的凭证都是运行时生成的测试值。点击“加载示例项目”会得到 16 条检测结果覆盖几种常见的配置和日志场景图 1加载内置示例项目后的扫描结果共 16 条按文件分组排列我想要的就是这种检查方式先找出值得留意的位置再回到项目里逐个确认。源码已经放在 AtomGit文末附了本地运行方法。一、任务怎么交给码道技术栈选了 Vue 3、TypeScript 和 Vite测试用 Vitest。页面需要的交互不算复杂更需要花心思的是底下的扫描逻辑能不能识别常见格式会不会把普通代码报成密钥结果里又该保留哪些信息。我先在 AtomGit 网页端通过华为云码道 Agent 建好仓库随后转到桌面版的“编程”模式开发。下面是建仓时的记录图 2在 AtomGit 网页端通过华为云码道 Agent 创建项目仓库的记录第一轮我只把扫描核心交给它。输入很明确就是文件路径和文本输出也提前约定好每条结果需要包含规则、风险级别、位置、打码值和原始长度。完整密钥不能再跟着结果传出去每条检测规则都要有对应的测试。还有一个要求是把扫描器单独放进 core不引用 Vue、浏览器 DOM 或 Node API。浏览器和命令行读取文件的方式不同但同一段文本交进来检测逻辑应该是一样的。先划清这一层后面增加入口时就能直接复用。这些约束写清楚以后码道会在项目里创建文件、补类型和测试运行命令检查结果。我再看这一轮的实现和反馈决定下一轮接什么。实际顺序是先做 core再接文件读取、Worker 和 CLI最后做 Vue 界面。代码主要交给码道实现提示词整理和独立复核也借助了其他 AI 助手。这种推进方式用下来比较顺手。每次要看的内容有限遇到问题也容易说清楚。核心阶段的一次误报就是这样继续改下去的。二、80 个测试通过普通字符串还是被报成了密钥第一阶段结束时码道给出的结果是 80 个测试通过。接下来的复核却发现下面这个普通字符串会被识别成 OpenAI 兼容密钥mask-position-x-coordinate问题出在mask-的后半截刚好是sk-原来的正则没有限制左侧边界就从单词中间开始匹配。类似的普通英文组合、文件名也可能触发。对一个要扫描前端项目的工具来说这个误报很影响使用。目录里本来就有大量样式和变量名如果这些东西也被标成风险每次检查都得先排除一堆无关结果。我把这些反例带回下一轮任务让码道修正匹配边界并补上“不应命中”的测试。修正后的 OpenAI 兼容规则使用的是这条正则/(?![A-Za-z0-9_-])sk-(?!ant-)[A-Za-z0-9_-]{20,200}/g前面的(?![A-Za-z0-9_-])限制了左侧边界sk-前面不能紧挨着字母、数字、下划线或短横线。这样就不会从mask-中间截出一个sk-。后面的(?!ant-)排除 Anthropic 前缀因为它有自己的专用规则{20,200}则限定这一步匹配的候选主体长度。正则之后还有一次校验。下面把相关函数和常量放在一起省略了源码中的说明性注释constOPENAI_LONG_PREFIXES:readonlystring[][sk-proj-,sk-svcacct-,sk-admin-];exportconstOPENAI_LONG_KEY_MIN_LENGTH40;functionhasRandomSegment(value:string):boolean{returnvalue.split(/[-_]/).some((segment)segment.length16/[A-Za-z]/.test(segment)/[0-9]/.test(segment),);}exportfunctionisOpenAiCompatibleKey(value:string):boolean{if(OPENAI_LONG_PREFIXES.some((prefix)value.startsWith(prefix))){returnvalue.lengthOPENAI_LONG_KEY_MIN_LENGTH;}returnhasRandomSegment(value);}普通sk-候选值按短横线和下划线拆开后至少要有一段达到 16 位并且同时包含字母和数字。函数名虽然叫hasRandomSegment做的其实是这种简单的形态判断不能证明字符串真的随机。sk-proj-等前缀单独走另一条分支是因为主体里也可能有较密的短横线和下划线切开以后每段都不长。当前实现对这几类候选值改为检查总长度是否达到 40。这些条件用于减少误报扫描时不会调用服务商接口验证密钥是否有效。这一轮修复既补误报用例也继续检查应该命中的样本核心阶段的测试最终增加到 102 个。开发阶段自动测试数量扫描核心首版80修正匹配边界、补完回归用例后102接入界面与 CLI当前版本237图 3核心规则修正、补完回归用例后的执行记录这段经历让我对怎么给码道反馈有了更具体的认识。直接提供一段输入说明它现在报了什么、预期应该怎样后续修改就有了明确目标。反例写进测试以后下次再改规则同样的输入还会重新检查一遍。三、打码以后也要看看到底露出了多少在检查核心输出时还发现了一个很容易忽略的细节。初版对稍长的值保留前后各四位中间打码。放在很长的 Key 上看着还行换成 13 位口令就有 8 位直接显示出来了。后来把保留字符数改成跟长度一起变化。实现很短exportconstELLIPSIS…;exportfunctionmask(secret:string):string{constnMath.min(4,Math.floor(secret.length/8));if(n0)returnELLIPSIS;returnsecret.slice(0,n)ELLIPSISsecret.slice(-n);}n是每一端保留的字符数。先用长度除以 8 并向下取整让两端加起来露出的字符不超过原长度的四分之一再用Math.min(4, …)把每端上限卡在四位。13 位口令算出来的n是 1所以abcdefghijklm会显示成a…m7 位及以下的值只显示省略号。打码就在扫描核心里完成。scanContent逐行处理文本有关键词的规则先做预筛再执行正则。下面是匹配成功后生成结果的节选省略了外层循环和后续去重代码constextractedextractSecret(match);if(!extracted)continue;const{value,start}extracted;if(isPlaceholder(value))continue;if(rule.validate!rule.validate(value))continue;constfinding:Finding{ruleId:rule.id,ruleName:rule.name,severity:rule.severity,file:path,line:i1,column:start1,masked:mask(value),length:value.length,};这里先过滤已知占位值再调用规则可选的validate前面的 OpenAI 兼容校验就是从这里接入的。通过检查以后才生成包含位置和打码值的Finding。行号来自当前行下标i 1列号来自密钥本体在行内的起点start 1。有些正则会一起匹配变量名和赋值符号因此项目约定带捕获组时把第一个捕获组作为密钥本体并通过正则的d标志和match.indices取得它的位置没有捕获组时就取整个匹配的起点。这样结果标出的就是密钥起点找回原文件时更方便。Vue 页面和 CLI 都使用这份结果打码规则也就保持一致。页面需要的处理建议根据ruleId从规则定义中查询。导入和扫描过程仍需要读取原文但结果对象不会再携带完整密钥。我原先更关注“能不能查出来”这个例子让我把检查范围也放到了输出上。尤其是这类工具用户很可能会把结果截图拿去讨论显示出来的内容同样值得仔细看一遍。四、查出来以后要方便回去改代码页面接起来时我考虑的是一次检查会怎么进行选中项目目录看看哪些文件有问题先处理严重的再回到源码里修改。所以输入区放在页面最上面拖入、选择文件和粘贴都能从这里开始。第一次打开的人可以先加载示例不必拿自己的项目试。结果则按文件分组文件名下面列出每一处命中行列位置和建议放在一起。顶部的风险统计也做成了筛选入口。示例里有 6 条严重级别的结果点击对应卡片页面就只留下这些内容再点一次恢复全部结果图 4点击顶部“严重”统计卡片后结果只保留 6 条严重级别的命中页面用了暖灰背景和深色文字普通操作用少量绿色强调把红、橙、黄留给风险级别。扫描结束后注意力可以先落在需要处理的内容上。文件多了以后还得照顾页面响应。扫描交给 Web Worker 执行Vue 这边只负责提交文件、接收进度和更新结果。下面是界面发起扫描的节选省略了状态初始化、异常处理和资源清理constmyTokenscanToken;constworkercreateScanWorker();activeWorkerworker;constresultawaitrunScan(files,{worker,onProgress:(done,total){if(myTokenscanToken)progress.value{done,total};},});if(myToken!scanToken)return;findings.valueresult.findings;stats.valueresult.stats;status.valuedone;runScan内部通过postMessage把文件传给 WorkerWorker 再逐个调用核心层的scanContent。扫描结束后返回的是打码后的结果页面不用自己跑一遍规则。这里还有一个界面侧的计数器scanToken。每次开始扫描先递增把当时的值存进myToken更新进度或结果前再比较。只要当前计数变了旧回调就不再更新界面避免过期结果覆盖当前状态。完整实现还会在finally里终止这次创建的 Worker释放资源。Worker 每扫完一个文件会检查是否需要回报进度距离上次回报达到 50ms或较上次回报累计增加至少 5 个百分点就回报一次最后一个文件一定回报。文件很多时可以合并一部分进度更新。读取目录时会跳过node_modules、.git、dist和build超过 2MB 的文件以及识别出的二进制文件也会被过滤。这些被跳过的文件我在界面里单独留了一个区域逐项写出原因图 5扫描目录时被跳过的文件单独列出并逐项写出跳过原因拖进去的是整个目录实际检查的可能只是其中一部分。把差别显示出来用户才能知道扫描范围也方便决定要不要单独处理某个文件。对我来说这个说明和命中列表一样有用。五、在终端里也能查在项目目录里工作时有时直接敲一条命令会更方便。这也是前面把 core 单独拆出来的原因。CLI 负责遍历目录再把文本交给同一个扫描核心终端默认输出表格需要继续处理结果时也可以输出 JSONnpm run build:clinode dist-cli/index.js./your-project node dist-cli/index.js./your-project--json两边共用检测和打码逻辑修复一条规则以后浏览器和 CLI 都能用上。命令行还会在发现严重或高危问题时返回非零退出码后续要接本地提交检查可以继续从这里扩展。做完这个入口前面那层拆分的好处就很直观了读取文件的代码各写各的最需要反复调整的扫描规则只维护一份。六、实际使用感受我觉得码道比较省事的地方是能把一轮需求直接落实到项目文件里。规则、类型、测试和调用代码按阶段补齐以后再往上接界面应用就逐渐能跑起来了。发现问题时也可以继续在现有代码上修改。这次普通字符串被误报反馈回去以后留下了修正后的规则和回归用例。而我需要花时间的地方越来越集中在具体行为上普通代码该不该命中短口令显示多少合适拖进目录后哪些文件被跳过结果能不能帮助人回到源码里处理。这些要求在真正看过输入和输出以后会比第一版提示词细得多。目前项目有 17 条检测规则237 个自动测试通过。当前扫描流程在浏览器本地处理文件没有上传动作。不过这个版本还有需要继续补的地方清空操作和粘贴输入的边界处理仍需完善网页报告导出也还没做。使用时我会把它当作提交或分享项目之前的一次辅助检查。规则命中后仍要人工确认未命中也不代表项目一定没有遗漏如果发现凭证已经泄露删除代码里的值之后还需要撤销或轮换凭证。做这个工具的初衷是让整理项目时少一点挨个翻找。现在至少可以先拿到一份带位置和建议的结果再逐项处理。后面继续加规则时mask-position-x-coordinate这个字符串也会留在测试里一起检查。回头看这个项目里花时间最多的是规则边界和输出打码这些小地方基本都是看过真实输入输出以后才发现要改。你提交代码前都是怎么排查硬编码密钥的有好用的流程或工具欢迎评论区聊聊。想试试当前版本可以拉下代码在本地启动后先加载内置示例git clone https://atomgit.com/qq_40202349/local-secret-scan.git cd local-secret-scan npm install npm run dev源码与 README · MIT License
