All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 10m19s
157 lines
5.0 KiB
Go
157 lines
5.0 KiB
Go
package logger
|
|
|
|
import (
|
|
"fmt"
|
|
"net/url"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/bytedance/sonic"
|
|
)
|
|
|
|
// AccessPolicy 是敏感路由的安全摘要策略。
|
|
type AccessPolicy struct {
|
|
Name string
|
|
Sensitive bool
|
|
PresenceOnly bool
|
|
SafeFields map[string]struct{}
|
|
ResultFields map[string]struct{}
|
|
FileFields map[string]struct{}
|
|
}
|
|
|
|
var (
|
|
loginPolicy = AccessPolicy{
|
|
Name: "login_token", Sensitive: true, PresenceOnly: true,
|
|
SafeFields: fieldSet("username", "user_id", "account_id", "success", "result_code"),
|
|
ResultFields: fieldSet("success", "result_code", "status"),
|
|
}
|
|
paymentPolicy = AccessPolicy{
|
|
Name: "payment", Sensitive: true, PresenceOnly: true,
|
|
SafeFields: fieldSet("order_no", "payment_no", "channel", "payment_method", "result_code", "amount", "status"),
|
|
ResultFields: fieldSet("result_code", "status"),
|
|
}
|
|
wecomPolicy = AccessPolicy{
|
|
Name: "wecom_callback", Sensitive: true,
|
|
SafeFields: fieldSet("event_type", "resource_type", "resource_id", "result_code", "status"),
|
|
}
|
|
configPolicy = AccessPolicy{
|
|
Name: "sensitive_config", Sensitive: true, PresenceOnly: true,
|
|
SafeFields: fieldSet("config_key", "value_type", "module", "readonly", "sensitive", "configured", "status", "result_code"),
|
|
ResultFields: fieldSet("readonly", "sensitive", "configured", "status", "result_code"),
|
|
}
|
|
filePolicy = AccessPolicy{
|
|
Name: "file_export", Sensitive: true,
|
|
SafeFields: fieldSet("content_type", "file_size", "size", "count", "task_id", "status", "result_code"),
|
|
FileFields: fieldSet("file_name", "filename", "name"),
|
|
}
|
|
defaultPolicy = AccessPolicy{Name: "default_json"}
|
|
)
|
|
|
|
func fieldSet(fields ...string) map[string]struct{} {
|
|
result := make(map[string]struct{}, len(fields))
|
|
for _, field := range fields {
|
|
result[field] = struct{}{}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func policyForPath(path string) AccessPolicy {
|
|
normalized := strings.ToLower(path)
|
|
switch {
|
|
case strings.Contains(normalized, "/login"), strings.Contains(normalized, "token"):
|
|
return loginPolicy
|
|
case strings.Contains(normalized, "/system-configs"), strings.Contains(normalized, "/wechat-configs"):
|
|
return configPolicy
|
|
case strings.Contains(normalized, "/callback") && (strings.Contains(normalized, "wecom") || strings.Contains(normalized, "wework")):
|
|
return wecomPolicy
|
|
case strings.Contains(normalized, "payment"), strings.Contains(normalized, "wechat-pay"),
|
|
strings.Contains(normalized, "alipay"), strings.Contains(normalized, "fuiou-pay"):
|
|
return paymentPolicy
|
|
case strings.Contains(normalized, "/storage"), strings.Contains(normalized, "/upload"),
|
|
strings.Contains(normalized, "/download"), strings.Contains(normalized, "/export"):
|
|
return filePolicy
|
|
default:
|
|
return defaultPolicy
|
|
}
|
|
}
|
|
|
|
func sanitizeWithPolicy(raw []byte, contentType string, policy AccessPolicy) SanitizedContent {
|
|
if len(raw) == 0 {
|
|
return SanitizedContent{}
|
|
}
|
|
if !policy.Sensitive {
|
|
return sanitizeBody(raw, BodyPolicyJSON)
|
|
}
|
|
summary := make(map[string]any)
|
|
normalizedContentType := strings.ToLower(contentType)
|
|
switch {
|
|
case strings.Contains(normalizedContentType, "json"):
|
|
var payload any
|
|
if sonic.Unmarshal(raw, &payload) == nil {
|
|
collectSafeFields(payload, policy, summary)
|
|
}
|
|
case strings.Contains(normalizedContentType, "x-www-form-urlencoded"):
|
|
if values, err := url.ParseQuery(string(raw)); err == nil {
|
|
for key, items := range values {
|
|
collectSafeScalar(key, strings.Join(items, ","), policy, summary)
|
|
}
|
|
}
|
|
}
|
|
content := "[仅记录安全摘要]"
|
|
if len(summary) > 0 {
|
|
if encoded, err := sonic.Marshal(summary); err == nil {
|
|
content, _ = truncateBody(encoded, MaxBodyLogSize)
|
|
}
|
|
}
|
|
return SanitizedContent{Content: content, Size: len(raw), SHA256: digest(raw), Truncated: len(raw) > MaxBodyLogSize}
|
|
}
|
|
|
|
func collectSafeFields(value any, policy AccessPolicy, output map[string]any) {
|
|
switch typed := value.(type) {
|
|
case map[string]any:
|
|
for key, item := range typed {
|
|
switch scalar := item.(type) {
|
|
case string:
|
|
collectSafeScalar(key, scalar, policy, output)
|
|
case float64, bool:
|
|
if policy.PresenceOnly {
|
|
if _, isResult := policy.ResultFields[strings.ToLower(key)]; isResult {
|
|
output[key] = scalar
|
|
} else {
|
|
output[key] = map[string]any{"present": true, "length": len(fmt.Sprint(scalar))}
|
|
}
|
|
} else if _, allowed := policy.SafeFields[strings.ToLower(key)]; allowed {
|
|
output[key] = scalar
|
|
}
|
|
default:
|
|
collectSafeFields(item, policy, output)
|
|
}
|
|
}
|
|
case []any:
|
|
for _, item := range typed {
|
|
collectSafeFields(item, policy, output)
|
|
}
|
|
}
|
|
}
|
|
|
|
func collectSafeScalar(key, value string, policy AccessPolicy, output map[string]any) {
|
|
normalized := strings.ToLower(key)
|
|
if policy.PresenceOnly {
|
|
if _, isResult := policy.ResultFields[normalized]; isResult {
|
|
output[key] = value
|
|
return
|
|
}
|
|
output[key] = map[string]any{"present": true, "length": len(value)}
|
|
return
|
|
}
|
|
if _, isFileName := policy.FileFields[normalized]; isFileName {
|
|
base := filepath.Base(value)
|
|
hash := digest([]byte(base))
|
|
output[key] = "sha256:" + hash[:16]
|
|
return
|
|
}
|
|
if _, allowed := policy.SafeFields[normalized]; allowed {
|
|
output[key] = value
|
|
}
|
|
}
|