OpenMed 嵌套脱敏幂等性校验实战:用 `openmed.risk.idempotence` 做无值泄漏的双次对比审查
OpenMed 嵌套脱敏幂等性校验实战用openmed.risk.idempotence做无值泄漏的双次对比审查【免费下载链接】openmedLocal-first healthcare AI: clinical NER HIPAA PII de-identification that runs 100% on-device. 2,200 medical models, 21 languages, Apple MLX Python, no cloud, no patient data leaving your network. Apache-2.0项目地址: https://gitcode.com/GitHub_Trending/ope/openmed脱敏流水线de-identification pipeline的重复执行是否稳定产出相同结果是隐私审查中最基础也最容易出问题的一环。OpenMed 在openmed.risk.idempotence中提供了一个确定性、本地运行的嵌套结构化脱敏结果幂等性检查器check_idempotence它把两次已产出的脱敏结果FHIR 资源、OMOP 表格或任意嵌套 JSON压缩成无原始值的证据快照并逐维度对比。读完本文你将掌握它的输入契约、五维对比模型、输入边界与拒绝策略以及如何在零网络调用、零敏感值回显的前提下把它接进自己的审查或门禁流程。幂等性检查是什么以及它不是什么openmed.risk.idempotence解决的是一个非常具体的问题判断一份结构化的脱敏结果在被第二次处理时是否保持稳定。它比较的是已经生产出来的两次结果而不是重新对原文做一次脱敏——检查器本身不做脱敏、不推断临床语义、也不调用任何远程服务模块 docstring 明确写着 It does not redact input, infer clinical meaning, or contact a service。文档同时给出了严格的边界声明这些定位决定了它适合被怎样使用它是一个确定性、本地的审查辅助工具a deterministic, local review aid它不是FHIR 或 OMOP 的一致性校验conformance check它不是合规认证compliance certification它不是临床决策保证clinical decision guarantee。换句话说幂等通过只说明两次产出的证据一致不说明产出本身临床正确或合规。同样的精神也体现在openmed/risk/redaction_diff.py中该模块同样坚持 value-free无值原则只对比聚合 action/category/count 数据并记录 policy 指纹让审查者理解什么变了而不接触源值或替换值。输入契约三次检查接受的四种一轮脱敏结果check_idempotence的两个参数类型都是IdempotenceInput定义见 idempotence.py即以下四种之一输入形式说明判定依据嵌套 JSON 映射Mapping最常见的 Pythondict内含资源与报告_coerce_mapping本地 JSON 文件str或Path传入文件路径自动读取并解析_read_json_path列表/元组Sequence被当作纯资源树处理_coerce_pass结果对象暴露resource/data/output等属性及report/audit_report/metadata属性或可调用to_dict()的对象StructuredRedactionResult协议对于带报告的对象报告元数据里可以携带三类信息源码_snapshot逐一提取聚合 counts如redacted、redacted_count、span_count、removed、replaced、total等来源可放在counts/count_summary/totals节也可以作为顶层键直接出现完整白名单见_COUNT_FIELDS策略指纹policy_fingerprint/policy_hash字段或policy/policy_name/policy_profile值后者会被哈希化处理脱敏事件redactions/redaction_events/events/actions/spans等容器中的事件条目每条事件包含path、action、surrogate或surrogate_fingerprint/replacement等别名。一个值得注意的实现细节_coerce_mapping与测试test_bare_fhir_resource_fields_are_not_wrapper_aliasesFHIR 资源自身的字段不会被误判为包装别名。比如{resourceType: Binary, data: ...}中的data是资源字段{resourceType: DiagnosticReport, result: []}中的result也是资源字段——resourceType判别符优先于包装键别名。同时如果同一层同时出现resource和data两个包装键{resource: {...}, data: {...}}会被判定为歧义资源别名并拒绝见test_ambiguous_resource_and_metadata_aliases_are_rejected。快速上手对比两个相同结果文档给出的最小示例直接可用from openmed.risk import check_idempotence first { resource: { resourceType: Bundle, entry: [{resource: {resourceType: Patient, id: synthetic-a}}], }, report: { policy_fingerprint: sha256: a * 64, counts: {redacted: 1}, redactions: [ { path: entry[0].resource.id, action: replace, surrogate: [SYNTHETIC-ID], } ], }, } second first # The second pass has the same synthetic evidence. result check_idempotence(first, second) assert result.is_idempotent两个参数都可以是嵌套映射、本地 JSON 文件路径或带resource/data/output属性与报告的结果对象。报告里的counts、policy_fingerprint和脱敏事件redactions会被自动抽取为证据即使两次传入的是裸资源没有报告检查器仍会执行确定性的形状shape对比。结果对象的等价写法测试文件 tests/unit/risk/test_idempotence.py 展示了带属性的 dataclass 输入方式——只要对象有resource/report属性或者可to_dict()就能直接传入from dataclasses import dataclass dataclass(frozenTrue) class _ResultObject: resource: object report: object report check_idempotence( _ResultObject(resource, report_data), _ResultObject(resource, report_data), ) assert report.passed is True注意report.passed是is_idempotent的别名源码 idempotence.py专门为门禁风格gate-style的调用方设计。五维对比模型每次检查到底比什么_compare_snapshots与_compare_scalar_values共同实现了一个结构化的多维 diff每一维对应ChangeDimension类型中的一个值见 idempotence.py维度含义判定依据shape嵌套输出形状每个路径节点的(kind, keys, length)签名ShapeNode.signaturecount聚合计数counts各键的值_compare_count_mapsaction动作及动作计数事件级action、action_counts映射、事件增删surrogate替代值指纹事件中surrogate_fingerprint的比对以及无事件元数据时的标量值对比policy_fingerprint策略指纹全局与事件级策略指纹每个 diff 都会被归类为added/removed/changed之一并按(维度顺序, 路径, 分类, before, after)稳定排序去重保证输出可复现。标量变化没有事件元数据也能抓到这是该实现最有价值的行为之一源码_compare_scalar_values测试test_scalar_change_without_event_metadata_is_not_idempotent即使两次结果都不含任何脱敏事件报告只要资源树中同一路径上的标量值如patient_name发生了改变检查器就会在surrogate维度上报一条差异——而且不会把源值写进任何输出report check_idempotence( {resource: {patient_name: synthetic-private-first-value}}, {resource: {patient_name: synthetic-private-second-value}}, ) assert report.is_idempotent is False assert report.surrogates_match is False # 序列化产物中绝不出现源值本身 assert synthetic-private-first-value not in report.to_json() assert patient_name not in report.to_json()读取报告结果IdempotenceReportidempotence.py是一组 frozen dataclass提供以下属性与方法result.is_idempotent # 所有维度均无差异 result.passed # is_idempotent 的别名 result.shape_match # 形状是否一致 result.counts_match # 聚合计数是否一致 result.actions_match # 动作与动作计数是否一致 result.surrogates_match # 替代值指纹是否一致 result.policy_fingerprint_match # 策略指纹是否一致 result.non_idempotent_paths # 发生非幂等变化的排序路径元组 result.summary # 聚合状态字典含 changes_by_dimension、total_changes result.differences # 结构化差异元组IdempotenceDifference result.to_dict() # 确定性 JSON 兼容字典 result.to_json(indentNone) # 稳定序列化sort_keysTrue, ensure_asciiTrue result.to_markdown() # 紧凑的 Markdown 审查摘要含维度变化表与非幂等路径列表non_idempotent_paths返回排序后的路径集合例如 OMOP 场景下会得到$.tables.person[0].person_id这样的 JSONPath 风格路径。测试test_omop_surrogate_change_is_classified_without_echoing_values验证了关键隐私属性当两次替代值不同synthetic-subject-surrogate-avs-b时报告判定surrogates_match is False路径被准确报告但两次的替代值字符串都不会出现在序列化结果中。有界输入检查器拒绝什么check_idempotence对外层异常做了收口源码 idempotence.py任何非预期异常都会被转换为IdempotenceInputError错误消息是封闭的closed message不包含输入值也不透传自定义容器抛出的异常文本测试test_cycles_depth_and_hostile_mappings_fail_with_sanitized_errors用_ExplodingMapping验证了 synthetic-sensitive-exception 不会泄漏到错误消息里。被拒绝的输入类别包括循环值cyclic values资源树中出现自引用歧义的包装/元数据别名ambiguous wrapper or metadata aliases如同层同时存在多个资源包装键或事件里同时出现action与operation两个动作字段重复的 JSON 对象键duplicate keys通过object_pairs_hook在解析时即拒绝测试用{id: a, id: b}验证非有限数值non-finite numbersNaN/Infinity包括文件中的NaN字面量parse_constant_reject_json_constant不支持的标量类型非 JSON 兼容的值bytes、自定义对象等。固定的资源上限检查器对输入施加硬性限制常量定义见 idempotence.py超过即抛IdempotenceInputError限制项上限文件大小_MAX_FILE_BYTES16 MiB总节点数_MAX_TOTAL_NODES100,000容器条目数_MAX_CONTAINER_ITEMS20,000嵌套深度_MAX_DEPTH64 层文本长度_MAX_TEXT_CHARS1,000,000 字符键长度_MAX_KEY_CHARS512 字符路径长度_MAX_PATH_CHARS4,096 字符脱敏事件数_MAX_EVENTS4,096元数据来源数_MAX_METADATA_SOURCES256计数值_MAX_COUNT2^63 − 1整数位宽_MAX_INT_BITS4,096 bit这些上限共同保证即使面对深度嵌套、超大或恶意的输入检查器也只会产生确定性的拒绝结果而不会耗尽内存或无限递归测试用 70 层嵌套验证了深度限制。隐私属性值不出现在任何输出通道这是该模块的设计核心文档与实现完全一致。RedactionPassSummary与IdempotenceReport中只允许出现模式路径schema paths如$.entry[0].resource.id标量类型kindobject / array / string / number / boolean / null数组长度计数counts安全动作名safe action namesSHA-256 指纹。而源值、替换值、未知的模式键、未知的动作名、未知的策略名绝不会被复制进 JSON、Markdown、repr或异常中模块 docstring 与ShapeNode.to_dict、RedactionEvent.to_dict的 docstring 反复强调 without exposing its scalar value / raw-value-free。未知标识符的统一处理方式是指纹化命名规则从源码可以清楚看到未知 schema 键 →key: sha256 摘要_safe_key如$.key:3f7a...未知动作 →action: sha256 摘要_safe_action白名单见_SAFE_ACTIONS未知计数值 →count: sha256 摘要_safe_count_key无法解析的路径 →path: sha256 摘要_render_path_value。动作白名单共有 9 个安全值idempotence.pydrop、format_preserve、hash、keep、mask、null、redact、remove、replace。其中keep/redact/replace/mask/hash/format_preserve与 OpenMed 的核心 span 动作常量ACTION_VALUES一致额外的drop/null/remove则用于兼容更多脱敏产物的报告格式。指纹格式统一为sha256:或hmac-sha256:前缀加 64 位十六进制_DIGEST_RE。两点必须明确的语义边界替代值指纹只是相等性证据equality evidence不是加密匿名化cryptographic anonymization的声明——指纹相等说明两次产出相同但不代表该替代值本身具备不可逆或匿名强度检查器只接受内存对象或本地 JSON 文件且不做任何强制网络调用——check_idempotence的 docstring 明确 performs no network access。测试test_unknown_action_and_policy_metadata_are_fingerprinted展示了端到端效果报告里policy是synthetic-private-policy-name、动作是synthetic-private-action时is_idempotent仍为True因为指纹一致但序列化结果中不包含这三个私密字符串中的任何一个。在 OpenMed 中的使用场景与边界FHIR / OMOP 形状的脱敏产物对比仓库的测试夹具tests/unit/risk/test_idempotence.py给出了两类典型输入的模板FHIR 形状resource为Bundle→entry→Patientreport含policy_fingerprint、counts、actionsOMOP 形状data.tables下为person/visit_occurrence表report含redactions事件列表。这两类夹具必须保持合成且离线synthetic and offline——文档与实现都不建议把真实患者数据哪怕是脱敏后的直接灌入检查器做长期快照比对因为检查器的设计前提就是输入本身可能是敏感的但输出必须无值。与 redaction-diff 的配合如果你只需要对聚合摘要action/category/count做无值 diff而不是对嵌套资源树逐路径比对可以参考姊妹模块 redaction-diff 及其实现 openmed/risk/redaction_diff.py它接受摘要而非文档/span对比维度更聚焦并同样把未知的 action/category/count 键指纹化为action:sha256:...形式。两个模块共享同一设计哲学审查者只拿到形状、计数与指纹拿不到任何敏感值。接入方式与限制典型接入方式是把check_idempotence放进离线审查脚本或发布门禁from openmed.risk import check_idempotence, IdempotenceInputError try: result check_idempotence(first_pass.json, second_pass.json) except IdempotenceInputError as exc: # 封闭错误消息不包含输入值 print(input rejected:, exc) else: if not result.is_idempotent: print(result.to_markdown()) print(changed paths:, result.non_idempotent_paths)API 还提供了两个兼容别名方便不同命名习惯的调用方check_redaction_idempotence与compare_structured_redaction均指向同一实现见 openmed/risk/init.py 的导出与 idempotence.py。最后重申文档给出的两条硬边界该检查器不验证临床语义clinical semantics也不能替代正式隐私审查formal privacy review。幂等性通过只说明流程稳定合规性、匿名强度与临床正确性仍需要由对应的专业流程负责。小结openmed.risk.idempotence用一个约 1900 行的纯标准库实现回答了脱敏审查中一个高频问题同一份结果再跑一遍会不会变 它把这个问题拆解成 shape、count、action、surrogate、policy_fingerprint 五个可独立判定的维度通过有界输入校验、封闭错误消息、未知值指纹化与全输出通道的无值原则保证审查过程本身不引入新的数据暴露。无论是 FHIR Bundle 还是 OMOP 表格形状的产物只要数据保持合成、检查保持本地check_idempotence都可以作为确定性审查与发布门禁中一个可靠、可解释、可复现的环节。【免费下载链接】openmedLocal-first healthcare AI: clinical NER HIPAA PII de-identification that runs 100% on-device. 2,200 medical models, 21 languages, Apple MLX Python, no cloud, no patient data leaving your network. Apache-2.0项目地址: https://gitcode.com/GitHub_Trending/ope/openmed创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
上一篇/下一篇内容由系统自动关联
返回资讯列表 →