Smail/backend/config/audit_changes.go
smail 5faadcd6eb
Some checks are pending
Security Checks / security (push) Waiting to run
保存三数据库日志与三十天清理实现,继续补充验收
2026-09-08 22:30:50 +08:00

148 lines
3.7 KiB
Go

package config
import (
"encoding/json"
"reflect"
"sort"
"strconv"
"strings"
)
// ConfigAuditChange 只保存安全的显示值,不保存密文或请求原文。
type ConfigAuditChange struct {
Field string `json:"field"`
Before string `json:"before"`
After string `json:"after"`
Sensitive bool `json:"sensitive,omitempty"`
}
// AuditChanges 对比业务字段,排除代次、摘要和内部持久化元数据。
func AuditChanges(before, after RuntimeConfig) []ConfigAuditChange {
left := auditFields(before)
right := auditFields(after)
keys := make(map[string]bool)
for key := range left {
keys[key] = true
}
for key := range right {
keys[key] = true
}
ordered := make([]string, 0, len(keys))
for key := range keys {
ordered = append(ordered, key)
}
sort.Strings(ordered)
changes := make([]ConfigAuditChange, 0)
for _, key := range ordered {
if reflect.DeepEqual(left[key], right[key]) {
continue
}
change := ConfigAuditChange{Field: key, Before: auditDisplay(left[key]), After: auditDisplay(right[key])}
if auditSensitive(key) {
change.Sensitive = true
change.Before, change.After = "未配置", "未配置"
if auditConfigured(left[key]) {
change.Before = "已配置(隐藏)"
}
if auditConfigured(right[key]) {
change.After = "已配置(隐藏,已更新)"
}
}
changes = append(changes, change)
}
return changes
}
func auditFields(runtime RuntimeConfig) map[string]any {
result := map[string]any{}
for key, value := range map[string]any{
"root": runtime.Root, "smtp": runtime.SMTP, "email": runtime.Email,
"ssl": runtime.SSL, "plugins": runtime.Plugins,
} {
data, _ := json.Marshal(value)
var object any
_ = json.Unmarshal(data, &object)
flattenAudit(result, key, object)
}
return result
}
func flattenAudit(result map[string]any, path string, value any) {
name := path[strings.LastIndex(path, ".")+1:]
switch name {
case "meta", "sections", "generation", "updatedAt", "schemaVersion", "configVersion", "version":
return
}
if path == "root.smtp" || path == "root.email" || path == "root.ssl" || path == "root.plugins" {
return
}
if auditSensitive(path) {
result[path] = value
return
}
if items, ok := value.([]any); ok {
for index, item := range items {
flattenAudit(result, path+"."+strconv.Itoa(index), item)
}
return
}
if object, ok := value.(map[string]any); ok {
for key, item := range object {
flattenAudit(result, path+"."+key, item)
}
return
}
result[path] = value
}
func auditSensitive(path string) bool {
name := strings.ToLower(path[strings.LastIndex(path, ".")+1:])
for _, word := range []string{"password", "secret", "token", "apikey", "api_key", "credential", "ciphertext", "authorization", "cookie"} {
if strings.Contains(name, word) {
return true
}
}
// 插件设置是自由结构,未知键也不能作为明文审计输出。
return strings.HasSuffix(path, ".settings") || strings.HasSuffix(path, ".encryptionKeyRef")
}
func auditConfigured(value any) bool {
if value == nil {
return false
}
if text, ok := value.(string); ok {
return text != ""
}
if object, ok := value.(map[string]any); ok {
if ciphertext, exists := object["ciphertext"]; exists {
return ciphertext != ""
}
return len(object) > 0
}
return true
}
func auditDisplay(value any) string {
if value == nil {
return "未设置"
}
if text, ok := value.(string); ok {
if len([]rune(text)) > 512 {
return string([]rune(text)[:512]) + "…"
}
return text
}
if flag, ok := value.(bool); ok {
if flag {
return "开启"
}
return "关闭"
}
data, _ := json.Marshal(value)
text := []rune(string(data))
if len(text) > 512 {
return string(text[:512]) + "…"
}
return string(text)
}