
Bytebase 审计脱敏架构演进从 35 个手写 Redactor 到 proto 注解驱动的统一方案【免费下载链接】bytebaseDatabase governance built for humans and agents — controlling changes and access across every major database.项目地址: https://gitcode.com/GitHub_Trending/by/bytebase引言Bytebase 是一款面向人类与 Agent的数据库治理平台其审计系统会把被审计 RPC 的请求与响应载荷写入audit_log表并同步输出到 stdout。当这些载荷里混入数据库实例密码、OIDC 客户端密钥、服务账号密钥、SCIM 目录同步令牌、OTP 动态验证码等敏感字段时就会形成真实的信息泄露面。本文基于仓库中的设计文档 docs/design/audit-payload-redaction-from-proto.mdStatus: implemented · 2026-08-21 · addresses BYT-10090完整还原 Bytebase 如何将审计脱敏从手工维护的 35 个红名单函数重构成由 proto 注解驱动、带 CI 强制校验的统一方案。读完本文你将掌握为什么SENSITIVE与OMIT两个枚举值足以刻画全部脱敏语义以及单枚举相对双布尔的设计取舍脱敏引擎如何通过按计划复制共享copy-and-share实现成本不随载荷体积增长的承诺覆盖度扫描、字段清单inventory、读路径断言、Any注册表四道强制防线如何把漏脱敏从运行时事故前移到 CI 构建失败35 个手写 redactor 全部删除后性能指标与遗留缺口如Status.message的真实边界。一、背景为什么 35 个手写 redactor 挡不住泄露1.1 旧方案的缺陷顶层类型分派 人工记忆在被审计 RPC 落库前旧实现通过getRequestString与getResponseString原位于 backend/api/v1/audit.go按顶层消息类型做 switch 分派把载荷交给 35 个手写脱敏函数之一。这套方案的根本问题是一个字段只有在有人记得为它所在的消息类型写了 redactor时才受保护一旦同一个敏感字段出现在新的父消息下它就被原样写入审计日志。仓库真实发生过三次此类泄露已在main分支通过 #21110、#21109 修复OIDC 客户端密钥、服务账号密钥、SCIM 目录同步令牌。设计文档明确指出还有三处未被保护其中两处当时仍处于活跃状态。1.2 三个未被保护的真实泄露点泄露字段注解状态原因Instance.roles[].password字段本身INPUT_ONLY但容器Instance.roles是OUTPUT_ONLY客户端本应发送它CreateInstance、UpdateInstance、BatchUpdateInstances全部被审计而redactInstance原样透传 rolesUpdateUserRequest.otp_code无注解redactUpdateUserRequest逐字复制它原audit.go:1177处理器将其作为 MFA 校验码校验user_service.go校验失败既不消费该码也不抑制审计行于是仍有效的 OTP 落入audit_log与 stdoutdatabase.instance_resource.data_sources[]OUTPUT_ONLY挂在Database.instance_resource上而非凭证字段本身读取路径会填充data_sources[]目前靠convertDataSources中的叶级置空保护另有QueryResult的两个 redactor 对rows_count是否保留存在分歧属于同一字段在不同函数里行为不一致的典型表现。1.3 设计目标与非目标六个目标Goals字段在.proto中标记一次则在任意深度、任意 RPC 上都被脱敏审查审计日志记录内容 读.proto而非读 Go 代码成本不随载荷体积扩展QueryResponse可携带 10 万行以上数据新增秘密字段、或已有字段出现在新父消息下都应让 CI 失败而不是随版本发布凭证绝不能出现在读路径的返回中对应 #21205、66fba49268两次读路径泄露脱敏不得修改调用方的消息——它在处理器即将返回的活动对象上运行。五个非目标Non-goals不清洗已有audit_log行视为已泄露应轮换密钥且该表没有清理通道不在声明面之外证明目标 5读路径断言只覆盖 Enforcement 节定义的族群无法覆盖在族群之外、在声明铸币对之外填充 SENSITIVE 字段的函数这属于评审关卡而非证明自由文本中的秘密redactRoleAttributeread_redaction.go对 MariaDBSHOW GRANTS输出中的密码哈希做掩码这无法用字段注解表达保留手写实现拦截器之外写入的审计行backend/component/recovery/service.go 直接调用store.CreateAuditLog用encoding/json拼接匿名 Go 结构体构造 Request 字符串——没有 proto 消息和 descriptor任何注解、计划、扫描、清单、注册表都观察不到它resetUserPassword会把request.Password一起序列化进去运行时强制新 redactor 本质是拒绝名单denylist运行时未注解字段仍会被记录清单机制把这个失败前移到 CI跳过 lint 构建出的二进制里没有任何兜底。二、设计核心proto 注解Annotations2.1 一个扩展点 一个枚举在 proto/v1/v1/annotation.proto 中声明一个FieldOptions扩展与一个枚举enum AuditBehavior { AUDIT_BEHAVIOR_UNSPECIFIED 0; // 按原样记录 SENSITIVE 1; OMIT 2; } extend google.protobuf.FieldOptions { AuditBehavior audit_behavior 100010; }使用示例string client_secret 5 [(bytebase.v1.audit_behavior) SENSITIVE]; repeated QueryRow rows 3 [(bytebase.v1.audit_behavior) OMIT];语义表这是整个设计的地基注解是否可进入审计载荷API 是否可返回SENSITIVE否仅允许出现在铸造mint它的那个响应上读路径上永远不允许OMIT否允许——返回它正是该 RPC 的目的ServiceAccount.service_key恰好逼出这个区分CreateServiceAccount与密钥轮换只返回它一次service_account_service.go而convertToServiceAccount从不填充它RotateDirectorySyncTokenResponse形状相同。2.2 为什么用单个枚举而非两个布尔设计文档从三个角度否定了sensitive trueaudit_omit true的双布尔方案互斥成为结构性的两个布尔允许[(sensitive) true, (audit_omit) true]这种无意义且无人拒绝的组合枚举中一个字段只能有一个分类天然不可违反字段处只有一套词汇 SENSITIVE与 OMIT平行可读一次 grep 即可审计整个 proto 文件两个布尔意味着两个标识符、两次 grepredactor 只需一次读取单个GetExtension加 switch而非两次扩展读取各自处理缺省值将来需要第三种行为时只是新增一个枚举值而非第三个扩展。同时否决了google.protobuf.FieldOptions.debug_redact作为凭证标记它在 Go 中完全惰性——protojson、prototext、运行时都不会读取它字段级或枚举值级都不读对 Go 后端零强制力却给人已有强制的错觉。2.3 铸造面必须按 (函数, 字段) 对声明边界不是处理器 vs 转换器。UpdateUser通过转换器铸造当 MFA 注册进行中时它选择convertToUserMintingMFAEnrollment而非convertToUseruser_service.go该函数刻意设置temp_otp_secret与temp_recovery_codes。因此铸造面被声明为(函数, 字段) 对而非整函数豁免convertToUserMintingMFAEnrollment可以填充temp_otp_secret和temp_recovery_codes除此之外什么都不行。User还携带password和service_key整函数豁免会让该函数后续任意一行返回它们都能通过 CI。断言运行在每个转换器包括铸造型上只允许声明的配对。但绑定到产生函数还不够一个只是return convertToUserMintingMFAEnrollment(...)的调用方自己不赋值任何字段——一个写成这样的GetUser会同时满足转换器配对允许 MFA 字段与自身零赋值却把注册密钥交给读 RPC。所以豁免要在每个 RPC 输出上再声明一次UpdateUser可发射temp_otp_secretGetUser不可。值只有在产生函数被允许赋值 且 发射 RPC 被允许返回时才到达客户端。清单也不是一条就够LoginResponse.token、.mfa_temp_token与ExchangeTokenResponse.access_tokenauth_service.proto是产品的主访问令牌设计上就是铸造并返回——它们不走convertTo*函数完整性 lint 永远扫不到必须与ServiceAccount.service_key、SCIM 令牌一起显式命名。2.4 命名与编号的防混淆考量扩展名不用audit、编号不用 100000bytebase.v1.audit已作为MethodOptions上的bool存在100000 是allow_without_credential。不同扩展作用域使复用合法但 grep 是评审者的审计手段标识符必须无歧义。仓库中的 annotation.proto 注释还揭示了一条重要的演进规则100006 曾是mcp_exclusion_reason合并后不再复用该编号——因为 extend 块无法携带reserved语句旧二进制会把 100006 读成旧枚举赋予新含义等于让旧版本读出自信的错误答案。三、脱敏引擎按计划复制共享copy-and-share3.1 挂载点为什么必须是marshalAuditPayload新实现把两个旧函数合并为单个 marshalAuditPayload注释明确说明注解决定字段如何处理与字段来自请求还是响应无关。挂载点不能是 unary 拦截器WrapUnary因为流式路径通过auditConnectStreamingConn.Sendaudit.go自建auditEntry并直接调用createAuditLogAdminExecute是唯一的流式 RPC、被审计、其响应携带管理员模式查询的每一行若把遍历上移到WrapUnary那里是service_data与mcpPolicyDenied管道所在会静默丢失流式脱敏。因此必须有一条端到端断言覆盖Send路径——流式持久化否则只能通过createAuditLogFunc桩缝audit.go:49-52测试绕过真实路径。3.2 按消息类型的计划plan与缓存每个消息类型一份计划plan要丢弃的字段、以及通往这些字段的子消息字段。计划在首次使用时由 descriptor 构建并按protoreflect.FullName缓存。已注解字段被记录后不再向下深入无需脱敏的类型缓存一个 nil 计划——418 个(method, direction)对中有 343 个属于此类。表面有 209 个方法却只有 320 个不同的请求/响应类型因此对与类型不可互换缓存必须按类型键控。实现上audit_redact.goplanCache直接键控 descriptor 本身而非全名计划按字段编号应用两个同名但不同的 descriptor 否则会互相套用计划。3.3 复制共享的单一机制与四条硬约束机制通往脱敏字段路径上的每个消息都由Range复制——绝不通过迭代Descriptor().Fields()再Set那会在请求路径上对未设置的 message/list/map 字段 paniccannot be set with read-only value、has invalid nil pointer。Range只产出已填充字段这也是被丢弃字段从不被复制的方式复制时跳过而不是在从未持有它的消息上事后Clear。没有脱敏字段的子树按指针共享、只读。Go 没有现成的过滤克隆原语——proto.Clone、CloneOf、Merge都不接受选项field-mask 库是克隆后再裁剪——所以这就是该原语本身。由此引出四条硬约束每条都有对应测试钉住共享子树可从两个消息到达因此脱敏副本是一次性写入marshal 后即丢弃。TestAuditRedactionDoesNotMutateInput钉住这一点测试名在仓库中为TestAuditRedactionDoesNotMutateTheCallersMessage见 audit_redact_test.go。必须丢弃的字段用Clear()而非赋对optional字段二者不同——赋会留下{password:}。InstanceRole.password是optional在复制路径上该字段根本不复制。SENSITIVE的 oneof 成员被置空blankOMIT的成员被清除clear清除标量臂会解除 oneof 并抹掉哪个臂被提供的信息。DataSourceExternalSecret.tokeninstance_service.proto是字符串臂其兄弟app_role是消息臂、会以{}存活清除会让 token 认证与未配置无法区分。置空保留臂的存在——{token:}——这已是读路径的既有约定instance_service_converter_test.goOneof members may stay present as an is-configured signal... require blank content, not absence。该理由仅适用于SENSITIVEOMIT清空因为臂的存在不是审计元数据。当前没有OMIT字段是 oneof 臂规则是为了第一个出现时不自相矛盾。descriptor 图有四个循环组件遍历必须被守卫TablePartitionMetadata.subpartitionsdatabase_service.proto是唯一自递归字段另外三个是互递归——ObjectSchema ↔ StructKind ↔ ArrayKinddatabase_catalog_service.proto、google.protobuf.Value ↔ Struct ↔ ListValue、google.api.expr.v1alpha1.Expr。守卫若键控f.Message().FullName() parent自递归的直觉写法会漏掉全部三个。无守卫的下钻会一直递归到栈耗尽在拦截器内把整个 RPC 一起带走。实现的typeNeedsRedaction用带 visited 集合的普通可达性求解而非不动点。3.4 Map 与 repeated 字段必须重建绝不能共享Mapdst.Set(fd, src.Get(fd))共享 Go map 本身及其消息值清除条目会写穿回调用方的活动消息。正确形式是dst.Mutable(fd).Map()加逐条Set脱敏副本。已注解的 map 整张丢弃值类型有计划的 map 逐条重建。设计文档特别警告当时不存在值类型有计划的 map fixture因此TestAuditRedactionDoesNotMutateInput若不补该 fixture目标 6 的此类违反就抓不到。Repeated 消息字段Range产出只读列表赋值会 panic共享则无法产出脱敏元素且不写穿。所以元素类型有计划的 repeated 字段通过dst.Mutable(fd).List()逐元素追加脱敏副本无计划的整段共享。这正是整个设计的动机案例Instance.roles[].password和instance_resource.data_sources[]跨越的字段类别。非变更测试需要同时有 repeated 消息与 map 两种 fixture。3.5Any处理注册表 双路径强制descriptor 遍历只能看到type_url和value永远看不到被打包的消息——其内部被注解的字段对 redactor、覆盖度 lint、清单三者都不可见。三条路径把Any放上审计行且都不经过marshalAuditPayload来源当前调用点打包类型service_dataaudit.go4 处经GetSetServiceDataFromContext变更前 Setting 镜像、IAM policy 增量Status.detailsconvertErrToStatus6 处经connect.NewErrorDetailPermissionDeniedDetail、PlanCheckRun_Result普通Any字段2 处均在SearchAuditLogsResponse.audit_logs上写入时已脱敏的行处理方式是解包 → 按自身 descriptor 的计划脱敏 → 重新打包——除非打包类型的计划为 nil此时原样保留。原样保留是关键细节connect的ErrorDetail.Type()返回裸全名今日Status.details[].type_url存的就是bytebase.v1.PermissionDeniedDetail无条件经anypb.New往返会改写成type.googleapis.com/...静默改变SearchAuditLogs输出的type。由于打包类型在 Go 中选定而非在 descriptor 中声明强制必须分两半调用点 lint两个 setter 的十个调用点加上所有向普通Any字段赋值的点打包了注册表未为该目的地列出的类型就构建失败descriptor 半程遍历也要求审计表面中每个Any类型字段都有注册表条目。两者捕获不同问题——descriptor 半程抓新字段调用点半程抓已有字段中的新打包类型——缺一不可。拦截器还会丢弃未注册的Any而非记录它protojson.Marshal在遇到无法解析的Any时会失败整个消息不丢弃则整行审计记录丢失。但丢弃是对保密 fail-closed、对记录 fail-silentSetIamPolicy的 policy 增量是那一行的核心价值无错丢失本身就是缺陷所以注册表同时是清单Any半程的推导来源。设计文档还披露了两个隐蔽的当前安全事实四个service_data调用点中的两个打包convertToSettingMessage输出该转换器显式置空其秘密另两个打包anypb.New(v1pb.AuditData{PolicyDelta: ...})来自findIamPolicyDeltas非转换器、不置空仅因BindingDelta恰好只含action/role/member/condition而安全。Status.message是第四个载荷且没有任何注解能触及它convertErrToStatus从connectErr.Message()或非 connect 错误的err.Error()设置它。idp_service.go:283用 oauth2 交换错误格式化failed to exchange access token, error: %s该错误的Error()内嵌 IdP 令牌端点的原始响应体而 oauth2 以AuthStyleInParams发送客户端密钥——同一方法其请求恰因携带该密钥而被脱敏。插值远程响应的错误串必须在构建处处理本设计不覆盖。3.6 直接赋值的结构化字段同样入列createAuditLog把RequestMetadata和McpDelegation直接设在行上与ServiceData并列两者既不走入口也不是Any但都是带 descriptor 的普通storepb消息计划原样作用于它们并加入清单。这不是理论性家务RequestMetadata.caller_supplied_user_agent由调用方控制store proto 自己注明未认证应相应对待。四、35 个 redactor 删除后的残留决策所有 35 个手写 redactor 被删除包括 allowlist 重建AdminExecute行恢复rows_count。但审计行携带非秘密剩余部分并非一律无害两个枚举值并不足以对全部内容分类OMIT必须覆盖比批量更广的内容redactMaskingReasons丢弃semantic_type_icon以避免用 base64 数据污染审计日志redactAIChatRequest丢弃messages与tool_definitions无界体。这两个字段都不在OMIT集里删除这两个重建会把 base64 块恢复到每个Query行、把完整 AI 对话恢复到每个AIService/Chat拒绝行所以必须纳入。最难处理的残留是既非凭证也非批量的内容redactUser返回{name, email, title}而convertToUser填充phone、groups[]与 profile 时间戳。SENSITIVE不可用读路径断言会因此在convertToUser本身上失败OMIT读作返回它是该 RPC 的目的于是不注解成了阻力最小路径——每个被审计的Login、CreateUser、UpdateUser都开始把电话号码和组成员写进audit_log与 stdout两者保留期不同且无清理通道。redactPurchaseResponse丢弃 Stripe 结账 URL一种承载能力评审者不将其归类为凭证。结论是要么把OMIT重新定义为出于任何原因都不得记录更小的改动也是本设计其余部分的前提要么新增第三个枚举值。删除十个重建之前必须先解决这个问题并逐一检查它们新放行的内容。五、强制Enforcement把泄漏变成构建失败5.1 覆盖度Coverage对下述族群中的每个 RPC用唯一哨兵值填充请求/响应树中每个已注解字段脱敏后断言无哨兵幸存。这能抓住redactor 不再覆盖某字段的回归并取代TestAuditRedactsEveryInputOnlyDataSourceField。关键约束oneof 需要每个臂一个消息而非一个消息设置所有臂——设置第二个臂会清除第一个单消息扫描会静默只覆盖最后一个编号的臂其余断言全部空转通过。DataSourceExternalSecret.auth_option会只测token(5) 而永不测app_role(4)——而app_role恰是置空规则论证所依赖的臂其role_id、secret_id均INPUT_ONLY。这是 oneof 上的叉积成本显著高于单遍。5.2 清单Inventory——让目标 4 真正成立的半边哨兵扫描无法区分未注解的凭证与审计行刻意保留的普通字段——两者经脱敏后幸存方式完全相同扫描没有 oracle。oracle 是一份 checked-in 清单列出从可达marshalAuditPayload的消息可达的每个 string/bytes 字段。字段缺失即构建失败消除失败意味着注解该字段或记录它不是凭证。其形状与 mcp_gate_test.go 的mcpDenialRequestsUnderReview相同。该族群比被审计 RPC 更宽WrapUnary在needAudit(ctx) || mcpPolicyDeniedaudit.go时写行因此未被审计注解的 gate 拒绝方法如ListInstanceDatabaseRequest、SwitchWorkspaceRequest也在范围内——只从被审计方法推导清单会恰好漏掉TestLintDenialRequestsAreReviewedForRedaction要覆盖的族群。每个已注册Any类型也都在范围内不经入口到达行这部分来自注册表而非 descriptor 遍历——这就是注册表必须在调用点被强制的原因。直接赋值的RequestMetadata与McpDelegation同理由入列这两者来自 descriptor 遍历。仓库实现位于 audit_redact_inventory_test.goTestLintAuditPayloadInventory对加入或离开审计表面的 string/bytes 字段执行声明 lint记录 866 个(message, string-or-bytes field)对。已注解字段按设计缺席清单读作审计行写了什么新凭证进入它只是一行 diff。5.3 读路径断言Read-path assertioninstance_service_converter_test.go 的assertNoInputOnlyValues已要求每个INPUT_ONLY字段从转换器返回空白将其泛化为SENSITIVE即INPUT_ONLY ∪ SENSITIVE并遍历 map 值。它运行在声明列表中的每个转换器上铸造型也包括铸造转换器不是被跳过而是只被允许上面声明的(函数, 字段)对因此其内部后续某行填充password或service_key仍然失败。完整性 lint 按返回类型键控而非convertTo*名称前缀——返回类型包括v1pb消息无论是直接返回、在 slice/map 内、在*connect.Response[T]包装内、多值返回的任一臂以及流式处理器的响应类型参数。原因有三AdminExecute只返回error通过Send发射stream *connect.BidiStream[v1pb.AdminExecuteRequest, v1pb.AdminExecuteResponse]没有任何返回形状能选中它——这个响应携带管理员模式查询每一行的处理器若不单独命名就落在族群之外包装器重要是因为每个 RPC 处理器都返回它GetUser是(*connect.Response[v1pb.User], error)内联构建响应而非走转换器的处理器否则完全在族群之外——它们大部分成为静态检查条目需要 store但这是逐处理器的 checked 决策而非静默遗漏容器不是边角convertDataSources返回[]*v1pb.DataSource、convertInstanceRoles返回[]*v1pb.InstanceRole——裸消息谓词会漏掉这两者加convertToAuditLogs等共 20 个138 个v1pb产出函数中最承重的那些前缀还会误匹配 19 个反向的convertToStore*函数它们按设计携带凭证且assertNoInputOnlyValues因接收protoreflect.Message断言 v1 契约而完全无法运行产生 19 个只有整函数豁免一个便宜解法的构建失败——正是本设计拒绝的 blanket 豁免。因此 lint 检查的是成员资格而非可执行性族群中每个函数都需要一个条目条目分两种——asserted测试把非空哨兵种入转换器可源的每个SENSITIVE字段运行泛化断言要求无幸存或statically checked测试无法执行的函数需要 store、活连接、出站调用。静态条目不是散文豁免它带 AST 检查——函数体不得向声明的(函数, 字段)对之外的SENSITIVE字段赋值并附记录的理由纯散文是这条规则的第一版草稿因整函数放行 对其后落入的一切放行而被否决。播种seeding是 asserted 条目 fail-closed 的原因assertNoInputOnlyValues只遍历已填充字段fixture 若让源保持零值会平凡通过——且在某行复制password或service_key之后仍继续通过。未播种与一个消息设置全部 oneof 臂是同一类空洞。泛化断言还必须遍历 map 值现版本跳过四个转换器可达的 map 携带消息值——ObjectSchema.StructKind.properties、DataClassificationSetting.DataClassificationConfig.classification、SQLEditorThemeSetting.tokens、google.protobuf.Struct.fields——其中任一含SENSITIVE字段都会在 CI 全绿下到达读响应。六、性能成本不随载荷体积扩展的实证设计文档给出的测量数据darwin/arm64单次运行INPUT_ONLY代指注解集载荷现状提案Instance1 个数据源4.1 µs5.9 µsInstance20 个数据源38 µs51 µsQueryResponse10 万行1.3 µs1.6 µsSheet5 MB0.16 ms · 5 MB0.001 ms · 0 MBBatchCreateSheets20 × 1 MB0.59 ms · 20 MB0.001 ms · 0 MBBatchUpdateInstances100 × 5 数据源1.07 ms · 2.0 MB1.29 ms ·1.2 MBBatchUpdateInstances100 × 20 数据源3.93 ms · 7.8 MB4.74 ms ·4.5 MB超过 1 ms 的唯一成本是批量对0.22 ms 与 0.81 ms发生在携带 500 与 2000 个数据源的请求上换来的是同一请求约 40% 的内存缩减。Sheet 行是修复redactSheet与两个 sheet 请求臂用proto.CloneOf克隆整个 sheet 再置空 content被审计的 20-sheet 批量创建会复制 20 MB 来丢弃共享机制从不复制被丢弃的子树。测量否决的方案clone-then-clearfield-mask 库的做法对 10 万行QueryResponse花 114 ms 与 87 MB 产出 215 字节的行marshal 期间脱敏需要自研 protojson 兼容发射器protojson.MarshalOptions没有字段过滤钩子只能节省微秒级。整个 v1 表面的计划构建一次耗时亚毫秒。七、备选方案评估Alternatives consideredfield_behavior INPUT_ONLY回答的是 API 契约问题而非保密问题。ServiceAccount.service_key是OUTPUT_ONLY却是活凭证IdPclient_secret、AIapi_key、Loginpassword完全无注解。udpa.annotations.sensitiveCNCFEnvoy 所用把 PII 与秘密混为一谈而审计行往往应当携带身份且为一个布尔引入新的buf依赖。在OMIT旁加MASK值没有可建模的掩码——maskedString就是protojson 会丢弃它现有每个掩码其实已是省略。SENSITIVE与OMIT的区别在于字段是什么而非它存活多少两者都丢弃它。双布尔见 2.2 节。debug_redact作凭证标记在 Go 中惰性暗示运行时并不提供的强制力。AIP-147规定的是模式而非注解INPUT_ONLYOUTPUT_ONLY bool name_set。Bytebase 并不一致地遵循它——ssl_ca_set遵循directory_sync_token_configured不遵循且后者尚未进入任何带 tag 的发布。八、实现落地Implementation注解AuditBehavior与audit_behavior 100010位于 proto/v1/v1/annotation.proto17 个服务 proto 中 77 个字段被注解。OMIT定义为出于任何原因都不得记录本设计其余部分的前提它承载User.phone、groups、profile、MaskingReason.semantic_type_icon、AIChatRequest.messages以及批量内容字段因此删除十个重建不会放行任何新内容。Redactorbackend/api/v1/audit_redact.go。每消息类型计划按 descriptor 身份缓存descend臂持有子 descriptor而非嵌套计划使循环类型可惰性跟随至任意深度Range复制共享map 与 repeated 消息重建SENSITIVEoneof 臂置空、OMIT臂丢弃。35 个手写 redactor 全部消失getRequestString/getResponseString坍缩为单个marshalAuditPayload挂载点不变仍由createAuditLog调用而非WrapUnary流式路径保持覆盖。行本身不整体遍历service_data与status在赋值处由redactAuditServiceData/redactAuditStatus脱敏request_metadata与mcp_delegation原样赋值其下无注解字段TestAuditRowNeedsNoRedactionBeyondTheAnyPayloads断言这一点。Any注册表位于 audit_redact.go按目的字段而非类型键控使Status.details与service_data允许不同内容。TestLintAuditAnyFieldsAreRegistered强制 descriptor 半程新Any字段必须声明可打包类型新类型打进已有字段不改变 descriptor由redactPackedAny的slog.Warn在运行时捕获。覆盖度测试按redactor 欠行两件事而非消息类型划分两半都在 audit_redact_test.go。TestAuditRedactionCoversEveryAnnotatedField扫描 321 个请求/响应类型上的 567 条注解字段路径每字段一个消息每个 oneof 臂被单独演练TestAuditRowKeepsItsSubstance必须保持手写——扫描断言缺席把整行置空的 redactor 也能通过它。双向变异检查禁用脱敏使 91 个子测试失败空消息替换使全部 13 个幸存行失败。清单audit_redact_inventory_test.go 持有声明 lint相对于 audit_redact_test.go 测试代码本身866 个(message, string-or-bytes field)对。读路径assertNoInputOnlyValues泛化为assertNoSensitiveValuesINPUT_ONLY ∪ SENSITIVE遍历 map 值在测试可构造的转换器上无条件运行。基于 AST 调用图、覆盖backend/api/v1每个v1pb产出函数的静态 lint 曾被原型验证后放弃读路径与审计行是不同表面转换器仅被重命名就上报凭证泄露的 AST 检查是本变更内不值得付的代价——若需要应独立 PR。保留而非替换mcpDenialRequestsUnderReview与清单并存。它是按(method, field)的覆盖清单标量作用域之外的消息类型请求字段DiffMetadata.target_metadata是完整 schema没有 string 字段能命名它。刻意不 fail-openAWSCredential.role_arn与external_id被注解为SENSITIVE。它们不是凭证且 Redaction 节指出删除redactIAMExtension会开始记录它们注解它们保持今日行为而非默默接受那个权衡。有意放宽并记录在案redactDataSource曾用空消息替换整个external_secret子消息现在只注解其凭证叶因此DataSourceExternalSecret的url、engine_name、secret_name、password_key_name、mount_path与类型枚举到达审计行。它们说明秘密在哪里而非是什么——但客户的内部 Vault 端点与秘密路径现在对任何拥有bb.auditLogs.search的人可读这是需要 sign-off 的决策而非副作用。QueryResult.messages是OMIT被删的redactQueryResponse是先于该字段存在的 allowlist靠遗漏丢弃它。引擎输出RAISE NOTICE、PRINT、DBMS_OUTPUT是与rows一样的结果数据通道列掩码够不到它。v1.AuditLog.service_data是OMIT否则SearchAuditLogs自己的审计行会解包、脱敏、重转录调用者读到的每行的变更前镜像——那是唯一成本随载荷体积扩展的地方。有显式 presence 的SENSITIVE标量置空而非丢弃proto3optional凭证与 oneof 臂是同一问题LoginRequest.otp_code是重要案例——失败登录必须与未呈现第二因素可区分。非 optional 标量仍被丢弃protojson 渲染两者相同。仍未覆盖Status.message与 Redaction 节所述一致。idp_service.go 仍插值 oauth2 错误其Error()可内嵌 IdP 原始响应体它没有 descriptor本方案无法触及修复意味着决定该错误应如何表述。九、结语这套方案对同类系统的可迁移价值Bytebase 的这次重构提供了一个可完整迁移到其他gRPC/Connect 服务 审计日志系统的模板声明面唯一化用FieldOptions扩展 单一枚举替代逐函数分派让审查审计记录什么回归到读.proto强制面前移覆盖度扫描、字段清单、读路径断言、Any注册表四道闸把漏脱敏变成构建失败并刻意设计为 denylist运行时默认记录未注解字段 CI 兜底的双层结构成本可控按计划复制共享让丢弃的子树从不被复制把大载荷审计的成本压到微秒级、内存按约 40% 缩减诚实的边界设计文档显式记录了Status.message、自由文本、拦截器外写入者等无法被注解机制触及的残余并给出每个残余的处置方向——这种知道什么做不到的记录方式与什么都声称覆盖同样重要。如需深入源码推荐按以下路径阅读注解定义 proto/v1/v1/annotation.proto → 脱敏引擎 backend/api/v1/audit_redact.go → 挂载点 backend/api/v1/audit.go → 强制测试 backend/api/v1/audit_redact_test.go 与 backend/api/v1/audit_redact_inventory_test.go。【免费下载链接】bytebaseDatabase governance built for humans and agents — controlling changes and access across every major database.项目地址: https://gitcode.com/GitHub_Trending/by/bytebase创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考