148 lines
3.7 KiB
Go
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)
|
|
}
|