- 开发工具
- 静态分析
- 代码质量
【免费下载链接】dependency-cruiser
Validate and visualize dependencies. Your rules. JavaScript, TypeScript, CoffeeScript. ES6, CommonJS, AMD.
本文聚焦 dependency-cruiser 匿名化(anon)报告器底层的核心算法anonymizePathElement,完整讲解它如何用词表替换路径元素、在词表耗尽时回退到“智能”随机字符串、如何通过白名单保留src、index等通用元素,以及缓存机制如何保证同一输入在同一轮输出中结果一致。读完本文,你既能透彻理解该算法的设计意图与源码实现,也能直接在配置文件中配置reporterOptions.anon.wordlist,产出可分享、可复现且不泄露业务语义的匿名化依赖图。
背景:为什么需要路径元素匿名化
依赖分析工具的输出(JSON 报告、依赖图)中天然包含源码文件路径,这些路径往往直接暴露业务模块命名、内部目录结构甚至敏感服务名。dependency-cruiser 提供了anon输出类型("obfuscated json"),与json输出格式相同,但所有路径都会被混淆,从而可以在不透露源码内容的前提下分享巡航(cruise)结果用于排查问题。官方命令行文档对该用途的说明见 cli.md,例如把匿名化结果保存为 JSON:
dependency-cruiser --config --output-type anon --output-to anonymized-result.json bin src也可以把匿名化结果经depcruise-fmt转为 dot 再渲染成 SVG 依赖图:
dependency-cruiser --config --output-type anon bin src | depcruise-fmt --output-type dot - | dot -T svg > anonymized_graph.svg上面两张图对比了 dependency-cruiser 自身依赖图匿名化前后的形态:节点名、目录层级结构被打乱,但整体拓扑关系与“哪些元素是通用目录”的可读性依然保留。这套能力的地基,就是 anonymize-path-element.md 所描述的路径元素级匿名化算法。
anonymizePathElement 核心算法:三种替换规则的叠加
anonymizePathElement的职责是:接收一个路径元素(path element,即路径中以/分隔的片段,例如src、superSecureThing.ts),返回一个匿名化的等价元素。原文档把算法规则总结为三点:
- 用词表(word list)中的单词替换传入的路径元素;如果词表为空,则改用('smart')随机字符串(见
randomString)。 - 如果传入元素命中白名单,则不替换。
- 只替换到模式中第一个点(dot)为止,因此扩展名得以保留。
原文档给出的示例:
superSecureThing.ts => abandon.ts superSecureThing.spec.ts => abandon.spec.ts src/index.ts => src/index.ts // 'src' 和 'index' 在白名单中 lib/somethingElse.service.js => lib/ability.service.js逐条解读:superSecureThing.ts整体被词表首词abandon替换,但.ts扩展名原样保留;superSecureThing.spec.ts同样只替换到第一个点(即superSecureThing部分),spec.ts后缀被保留——这使得“被测文件与对应源码”之间的命名相似性在匿名化后依然可辨识;src/index.ts因为src和index都命中白名单而完全不变;最后一个示例展示了多段后缀(.service.js)场景下同样只替换首段。
从源码看,该函数的完整签名位于 anonymize-path-element.mjs:
export function anonymizePathElement( pPathElement, pWordList = [], pWhiteListRE = /^$/, pCached = true, ) { return pWhiteListRE.test(pPathElement) ? pPathElement : replaceFromWordList(pPathElement, pWordList, pCached); }四个参数的语义与默认值如下:
| 参数 | 类型 | 默认值 | 作用 |
|---|---|---|---|
pPathElement | string | — | 待匿名化的路径元素,例如superSecureThing.ts |
pWordList | string[] | [] | 替换用词表;为空时回退到randomString |
pWhiteListRE | RegExp | /^$/ | 命中该正则的元素不做任何替换(默认正则不匹配任何非空串,即默认全量匿名化) |
pCached | boolean | true | 是否启用“路径元素 → 替换结果”缓存,保证同输入同输出 |
“只替换到第一个点为止”在源码中由replaceFromWordList实现——它用.拆分元素,只对拆分后的第一段(pIndex === 0)执行替换,其余片段原样保留,最后再以.重新拼接:
function replace(pElement, pIndex, pWordList) { return pIndex === 0 ? pWordList.shift() || randomString(pElement) : pElement; }注意这里的pWordList.shift() || randomString(pElement):每次替换都会从词表头部取走一个词,词表取空后自然回退到randomString——这正是原文档规则 1 的落地实现,也引出了“词表会被消耗、需要传克隆”的注意事项(见下文)。
白名单机制:哪些路径元素保持原样
原文档指出“don't replace if the pPathElement matches the whitelist”。在路径级入口 anonymize-path.mjs 中定义了一份默认白名单正则WHITELIST_RE:
export const WHITELIST_RE = /^(?:[.]+|~|bin|apps?|cli|src|libs?|configs?|components?|fixtures?|helpers?|i18n|index\.(?:jsx?|[mc]js|d\.ts|tsx?|vue|coffee|ls)|_?_?mocks?_?_?|node_modules|packages?|package\.json|scripts?|services?|sources?|specs?|_?_?tests?_?_?|types?|uti?ls?|tools)$/;该正则刻意放行工程结构中“通用、无业务信息”的目录与文件命名,让匿名化后的报告仍保留一定可读性,大致可归纳为几组:
- 通用目录名:
src、bin、lib/libs、app/apps、config/configs、component/components、fixture/fixtures、helper/helpers、i18n、node_modules、package/packages、script/scripts、service/services、source/sources、spec/specs、test/tests(含前后双下划线变体__tests__、__mocks__等)、type/types、util/utils、tool/tools; - 特定文件:
package.json,以及各类入口文件index.js、index.jsx、index.mjs、index.cjs、index.d.ts、index.ts、index.tsx、index.vue、index.coffee、index.ls; - 路径符号:
.(..、...等)与~。
这意味着src、index.ts、node_modules等元素在匿名化后保持不变,依赖图中“源码目录、测试目录、依赖目录”的轮廓仍然可辨;而业务命名的目录(如dragonfly-algorithm、secretService-record)则会被替换。需要自定义白名单时,向anonymizePathElement/anonymizePath传入pWhiteListRE即可(注意:正则不要带global修饰符,参见 anonymize-path.mjs 的注释说明)。
"smart" 随机字符串回退:randomString 的字符级模式保持
当词表为空或耗尽时,算法退化为随机字符串。这里的“smart”体现在 random-string.mjs 的实现上:它并不生成完全随机的乱码,而是逐个字符地保持原串的“形状”——长度不变,且每个字符的类型(数字、分隔符、大写、小写)也被保留。
实现思路是先把字符分为四类(源码中的常量):
| 类别 | 判定 | 替换策略 |
|---|---|---|
NUMBER | \d数字 | 随机 0–9 数字 |
SEPARATOR | -、_、. | 原样保留 |
UPPERCASE | 大写字母 | 随机大写字母 |
NOTHING_SPECIAL | 其余(小写字母等) | 随机小写字母 |
随机字符由node:crypto的randomInt生成。函数注释中给出的示例很好地说明了“形状保持”的效果:
hello => tbkwd randomString => ybmaecNtpmty __interesting-stuff => __uuhfiitcvoq-rudbk pulp2slurp => jgyb3guyow可见:__interesting-stuff中的双下划线前缀和-分隔符被原样保留,仅字母被打乱;pulp2slurp中的数字2位置仍是一个数字。官方文档 cli.md 中给出的实战例子secretService-record.ts→fnwarqVboiuvq-pugnmh.ts也正是这一机制的体现:大写字母位置被大写字母替换、连字符保留、.ts扩展名不变。
对应测试见 random-string.spec.mjs,覆盖了空串、小写、大写、带变音符号的大小写字符、分隔符回显、数字以及混合模式的整体形状断言。
缓存机制:pCached 与 clearCache
原文档明确指出:“To make sure the same input value gets the same output on consecutive calls, this function saves the (path element, result) pairs in a cache. If you don't want that pass false to the pCached parameter.”
这是因为词表是顺序消耗的(每次shift()取词),如果同一路径元素在报告的不同位置(例如某模块既出现在source又出现在某个依赖的resolved)被多次处理,不做缓存就会得到不同的替换结果,破坏报告的连贯性。缓存的实现位于 anonymize-path-element.mjs:模块级MapALREADY_USED_WORDS以原始元素为键保存替换结果;pCached为true(默认)时命中缓存直接返回,否则走replace并写入缓存:
const ALREADY_USED_WORDS = new Map(); function replaceCached(pElement, pIndex, pWordList) { if (ALREADY_USED_WORDS.has(pElement)) { return ALREADY_USED_WORDS.get(pElement); } const lReplaced = replace(pElement, pIndex, pWordList); ALREADY_USED_WORDS.set(pElement, lReplaced); return lReplaced; }配套的导出函数 clearCache 用于清空该缓存,源码注释说明它“主要为测试目的”而存在;同时在匿名化报告入口 index.mjs 中,每轮报告生成完毕后也会调用clearCache(),避免缓存跨巡航(cruise)污染。
缓存行为有对应的单元测试覆盖(见 anonymize-path-element.spec.mjs):同一字符串连续两次调用结果相等,不同字符串结果不同。路径级测试 anonymize-path.spec.mjs 则验证了“相似路径得到相似匿名化路径”:src/tien/kleine/geitjes/index.ts与src/tien/kleine/geitjes/tien.ts被映射为src/aap/noot/mies/index.ts与src/aap/noot/mies/aap.ts——目录层级共享同一批替换词,index.ts因白名单不变,而tien.ts复用aap(此时aap已在缓存中)。
词表消耗与去重:为什么需要传克隆
原文档末尾有一条醒目的警告:
The functionremoveselements from pWordList to prevent duplicates, so if the word list is precious to you - pass a clone.
即:replace每次取词都使用pWordList.shift(),直接修改传入的词表数组。这样设计有两个目的:
- 防止重复:每个词只被使用一次,避免同一单词出现在多个替换位置而显得不自然;
- 顺序确定性:按数组顺序依次取词,配合缓存机制使结果可复现。
代价是调用方持有的数组会被清空。若词表后续还要复用(例如多次生成匿名化报告,或同一配置在测试中反复调用),应当传入克隆:anonymizePathElement(pPathElement, [...myWords])。路径级与报告级的入口同样继承了这一语义(anonymize-path.mjs 与 index.mjs 的 JSDoc 中重复了这条提示)。
词表与白名单的另一个协同点体现在报告入口的sanitizeWordList(index.mjs):用户配置的词表会先被清洗——非字母与连字符的字符替换为_,仅保留形如/^[a-zA-Z-_]+$/的单词,并剔除命中白名单的词(例如用户误把src放进词表会被过滤掉),从源头保证词表不会与白名单冲突。
从路径元素到完整路径:anonymizePath 与 anon 报告器
anonymizePathElement是单元素级的原子操作,路径级入口 anonymizePath 将其组合起来:按/拆分路径得到元素序列,逐元素调用anonymizePathElement(默认WHITELIST_RE与缓存),再以/重新拼接。测试示例src/tien/kleine/geitjes/index.ts配词表["foo","bar","baz"]得到src/foo/bar/baz/index.ts,src与index.ts均因白名单保持原样。
在此基础上,anon 报告器入口 index.mjs 对整份巡航结果ICruiseResult做结构化克隆后,逐字段匿名化(anonymize函数):
modules[].source、modules[].dependencies[].resolved、modules[].dependencies[].module、modules[].dependencies[].cycle[]summary.violations[].from、summary.violations[].to、summary.violations[].cycle[]与via[]folders[].name及其dependencies[]、dependents[]- 模块的
dependents[]与reaches[].modules[].source/via[](存在时才处理)
值得注意的一个细节:node_modules内部的依赖路径同样会被匿名化(白名单只保护node_modules这个元素本身,不保护其子路径),因此匿名化后的依赖图不会暴露你的应用实际使用了哪些第三方包,见 cli.md 的说明。测试夹具 src-report.mjs 提供了结构完整的样例巡航结果,配合 anonymize.spec.mjs 可对照验证上述各字段的匿名化行为。
实战:配置 wordlist 与输出可复现的匿名化报告
正如原文档规则 1 所言,词表为空时算法回退到随机字符串——而随机字符串每次运行结果不同。若希望输出可复现(且更易读),应通过options.reporterOptions.anon.wordlist配置词表。官方选项参考 options-reference.md 给出的 JSON 配置示例:
{ "options": { "reporterOptions": { "anon": { "wordlist": [ "foo", "bar", "baz", "qux", "grault", "garply", "waldo", "fred" ] } } } }配置的加载顺序在 index.mjs 中体现:优先使用调用时传入的pAnonymousReporterOptions.wordlist,否则回退到pResults.summary.optionsUsed.reporterOptions.anon.wordlist,两者都缺失时使用空数组(即纯随机模式)。
需要提醒的是:路径元素的数量通常远多于 8 个词,想让随机回退尽可能少发生,就需要一份足够大的词表。官方文档建议可以使用现成的助记词词表(如mnemonic-words,一个专为生成易读随机短语设计的单词列表),并在 JavaScript 格式的配置文件中直接引入:
const mnemonicWords = require('mnemonic-words'); module.exports = { // ... options: { reporterOptions: anon: { wordlist: mnemonicWords } } }配置完成后即可用--output-type anon生成匿名化报告,例如:
dependency-cruiser --config --output-type anon --output-to anonymized-result.json bin src由于词表按顺序消耗且同名元素命中缓存,同一份配置、同一输入路径集合下,多次运行会得到一致的匿名化结果,便于将匿名化报告纳入工作流或与协作者稳定地对比。
源码与测试索引
围绕该算法可深入研读的文件(均在当前仓库中):
- 算法设计文档:anonymize-path-element.md
- 核心实现:anonymize-path-element.mjs、random-string.mjs
- 路径级与报告级入口:anonymize-path.mjs、index.mjs
- 单元测试:anonymize-path-element.spec.mjs、anonymize-path.spec.mjs、random-string.spec.mjs、anonymize.spec.mjs
- 使用文档:cli.md、options-reference.md
综上,anonymizePathElement用“词表优先、随机回退、白名单放行、首点截断”四条规则,在隐私保护与可读性之间取得了精巧的平衡;理解它,你就能完全掌控 dependency-cruiser 匿名化报告的行为——从词表配置到缓存语义,再到整份巡航结果的字段级匿名化范围。
- 开发工具
- 静态分析
- 代码质量
【免费下载链接】dependency-cruiser
Validate and visualize dependencies. Your rules. JavaScript, TypeScript, CoffeeScript. ES6, CommonJS, AMD.
相关推荐
Plano PII 匿名化过滤器链实战:在 LLM 请求/响应路径上自动脱敏与还原
Plano PII 匿名化过滤器链实战:在 LLM 请求/响应路径上自动脱敏与还原 本文以 Plano 仓库中的 PII Anonymization Filte
人工智能大模型后端API网关LLM 网关AI AgentAgent 编排可观测性AI 安全治理提示词注入防护Puppeteer 元素拖放实践:深入解析 ElementHandle.drag() 的签名、执行路径与 CDP 原理
Puppeteer 元素拖放实践:深入解析 ElementHandle.drag 的签名、执行路径与 CDP 原理 Puppeteer 通过 ElementHa
浏览器控制测试网页爬虫开发工具如何快速上手Parabolic:跨平台媒体下载工具安装配置指南
如何快速上手Parabolic:跨平台媒体下载工具安装配置指南 Parabolic是一款基于yt dlp的强大媒体下载工具,为用户提供跨平台的视频和音频下载解决
桌面应用音视频
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考