All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m2s
- 新增成对迁移 000223(tb_phone_asset_association,含有效关系部分唯一索引与 down 守卫)与 000224(解绑导入任务表),不回填历史 - H5:need_bind_phone 三支判定(开关关闭完全短路);已有主号幂等建联;十项上限按手机号 advisory 串行化(含换绑到全新号的并发场景);换绑原子迁移与冲突整单回滚;不写遗留列 - 后台:关联列表、单项/批量解绑、CSV 导入解绑(B1–B16),超管/平台 gate + 资产数据范围复核,三态统一文案 - 读侧:卡/设备列表与详情按页一次 IN 聚合;两类导出补「关联手机号」列并保留历史表头反解兼容 - 脱敏:关联审计走独立动作/资源只写脱敏手机号;访问日志手机号类字段脱敏 - 同步主 Spec openspec/specs/phone-asset-association 并归档 AUG26-009,补齐 requirement-evidence 与入口矩阵,context-health 通过
133 lines
4.0 KiB
Go
133 lines
4.0 KiB
Go
// Package sanitizer 提供 Access、Audit 与 Integration 共用的敏感字段清理能力。
|
||
package sanitizer
|
||
|
||
import (
|
||
"crypto/sha256"
|
||
"encoding/hex"
|
||
"fmt"
|
||
"regexp"
|
||
"strings"
|
||
"unicode"
|
||
|
||
"github.com/bytedance/sonic"
|
||
)
|
||
|
||
var forbiddenFragments = []string{
|
||
"password", "passwd", "credential", "operation_password", "verification_code", "captcha",
|
||
"token", "access_token", "refresh_token", "id_token", "session_token", "sms_code", "api_key", "payment_key",
|
||
"authorization", "cookie", "secret", "private_key", "public_key",
|
||
"encoding_aes_key", "callback_token", "signature", "sign", "nonce", "media_id", "signed_url",
|
||
"private_url", "qr_content", "id_card", "identity_number",
|
||
}
|
||
|
||
var forbiddenTextPattern = regexp.MustCompile(`(?i)(bearer[[:space:]]+[a-z0-9._~+/=-]{8,}|-----BEGIN [A-Z ]*PRIVATE KEY-----|(?:access_token|refresh_token|id_token|session_token|authorization|cookie|secret|private_key|api_key|payment_key|signed_url|x-amz-signature|x-amz-credential)[[:space:]]*[=:][[:space:]]*[^&[:space:]]+)`)
|
||
|
||
// IsForbiddenField 判断字段是否禁止进入普通日志、审计或外部交互摘要。
|
||
func IsForbiddenField(key string) bool {
|
||
normalized := normalizeFieldName(key)
|
||
if normalized == "credentials_configured" || normalized == "token_present" {
|
||
return false
|
||
}
|
||
for _, fragment := range forbiddenFragments {
|
||
if strings.Contains(normalized, fragment) {
|
||
return true
|
||
}
|
||
}
|
||
if strings.HasSuffix(normalized, "_key") || normalized == "key" || strings.HasSuffix(normalized, "_url") || normalized == "url" {
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
func normalizeFieldName(key string) string {
|
||
var normalized strings.Builder
|
||
for index, char := range key {
|
||
if unicode.IsUpper(char) && index > 0 {
|
||
normalized.WriteByte('_')
|
||
}
|
||
if char == '-' || char == '.' {
|
||
normalized.WriteByte('_')
|
||
continue
|
||
}
|
||
normalized.WriteRune(unicode.ToLower(char))
|
||
}
|
||
return normalized.String()
|
||
}
|
||
|
||
// MarshalSummary 递归删除禁止字段并返回 sonic 编码的安全 JSON。
|
||
func MarshalSummary(value any) ([]byte, error) {
|
||
if value == nil {
|
||
return nil, nil
|
||
}
|
||
encoded, err := sonic.Marshal(value)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
var normalized any
|
||
if err := sonic.Unmarshal(encoded, &normalized); err != nil {
|
||
return nil, err
|
||
}
|
||
RemoveForbiddenFields(normalized)
|
||
return sonic.Marshal(normalized)
|
||
}
|
||
|
||
// RemoveForbiddenFields 原地递归删除 Map 或数组中的禁止字段。
|
||
func RemoveForbiddenFields(value any) {
|
||
switch typed := value.(type) {
|
||
case map[string]any:
|
||
for key, item := range typed {
|
||
if IsForbiddenField(key) {
|
||
delete(typed, key)
|
||
continue
|
||
}
|
||
if text, ok := item.(string); ok {
|
||
typed[key] = SanitizeText(text)
|
||
continue
|
||
}
|
||
RemoveForbiddenFields(item)
|
||
}
|
||
case []any:
|
||
for index, item := range typed {
|
||
if text, ok := item.(string); ok {
|
||
typed[index] = SanitizeText(text)
|
||
continue
|
||
}
|
||
RemoveForbiddenFields(item)
|
||
}
|
||
}
|
||
}
|
||
|
||
// SanitizeText 将疑似包含安全凭据的文本转换为不可逆摘要。
|
||
func SanitizeText(value string) string {
|
||
if !forbiddenTextPattern.MatchString(value) {
|
||
return value
|
||
}
|
||
return TextSummary(value)
|
||
}
|
||
|
||
// 手机号脱敏保留位数:前 3 位与后 4 位。
|
||
const (
|
||
phoneMaskedKeepPrefix = 3
|
||
phoneMaskedKeepSuffix = 4
|
||
)
|
||
|
||
// MaskPhone 生成脱敏手机号,仅保留前 3 位与后 4 位。
|
||
// 审计、日志与错误文案一律使用本函数的结果;字段名脱敏清单不含 phone,
|
||
// 因此不能依赖 RemoveForbiddenFields 自动脱敏,必须显式调用。
|
||
func MaskPhone(phone string) string {
|
||
phone = strings.TrimSpace(phone)
|
||
if len(phone) < phoneMaskedKeepPrefix+phoneMaskedKeepSuffix {
|
||
return ""
|
||
}
|
||
return phone[:phoneMaskedKeepPrefix] + "****" + phone[len(phone)-phoneMaskedKeepSuffix:]
|
||
}
|
||
|
||
// TextSummary 将不可信外部文本转换为不可逆大小和哈希摘要。
|
||
func TextSummary(value string) string {
|
||
if value == "" {
|
||
return ""
|
||
}
|
||
sum := sha256.Sum256([]byte(value))
|
||
return fmt.Sprintf("外部文本摘要 bytes=%d sha256=%s", len(value), hex.EncodeToString(sum[:8]))
|
||
}
|