实现审计覆盖门禁与外部集成日志闭环
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 10m19s

This commit is contained in:
2026-07-23 21:31:22 +09:00
parent 7e0171a8b4
commit ff44305d0e
21 changed files with 12770 additions and 75 deletions

View File

@@ -1,6 +1,7 @@
package logger
import (
"fmt"
"net/url"
"path/filepath"
"strings"
@@ -10,25 +11,34 @@ import (
// AccessPolicy 是敏感路由的安全摘要策略。
type AccessPolicy struct {
Name string
Sensitive bool
SafeFields map[string]struct{}
FileFields map[string]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,
SafeFields: fieldSet("username", "user_id", "account_id", "success", "result_code"),
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,
SafeFields: fieldSet("order_no", "payment_no", "channel", "payment_method", "result_code", "amount", "status"),
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"),
@@ -50,6 +60,8 @@ func policyForPath(path string) AccessPolicy {
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"),
@@ -102,7 +114,13 @@ func collectSafeFields(value any, policy AccessPolicy, output map[string]any) {
case string:
collectSafeScalar(key, scalar, policy, output)
case float64, bool:
if _, allowed := policy.SafeFields[strings.ToLower(key)]; allowed {
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:
@@ -118,6 +136,14 @@ func collectSafeFields(value any, policy AccessPolicy, output map[string]any) {
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))

View File

@@ -5,10 +5,10 @@ import (
"crypto/sha256"
"encoding/hex"
"net/url"
"strings"
"time"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/sanitizer"
"github.com/bytedance/sonic"
"github.com/gofiber/fiber/v2"
"go.uber.org/zap"
@@ -135,20 +135,7 @@ func sanitizeJSONValue(value any) {
// shouldMaskField 判断字段名是否属于访问日志敏感字段
func shouldMaskField(key string) bool {
normalized := strings.ToLower(key)
return strings.Contains(normalized, "password") ||
strings.Contains(normalized, "passwd") ||
strings.Contains(normalized, "credential") ||
strings.Contains(normalized, "authorization") ||
strings.Contains(normalized, "cookie") ||
strings.Contains(normalized, "key") ||
strings.Contains(normalized, "url") ||
strings.Contains(normalized, "qr_content") ||
strings.Contains(normalized, "verification_code") ||
strings.Contains(normalized, "sign") ||
strings.Contains(normalized, "nonce") ||
strings.Contains(normalized, "token") ||
strings.Contains(normalized, "secret")
return sanitizer.IsForbiddenField(key)
}
// Middleware 创建 Fiber 日志中间件

View File

@@ -92,6 +92,8 @@ func TestSensitiveRouteMatrixNeverLogsRawPayload(t *testing.T) {
})
request := httptest.NewRequest("POST", tc.path+"?signature=query-secret", strings.NewReader(tc.body))
request.Header.Set("Content-Type", tc.contentType)
request.Header.Set("Authorization", "Bearer header-secret")
request.Header.Set("Cookie", "session=cookie-secret")
response, err := app.Test(request)
if err != nil {
t.Fatalf("执行敏感路由请求失败:%v", err)
@@ -99,7 +101,7 @@ func TestSensitiveRouteMatrixNeverLogsRawPayload(t *testing.T) {
_, _ = io.Copy(io.Discard, response.Body)
_ = response.Body.Close()
logged := output.String()
for _, secret := range []string{tc.secret, "query-secret", "response-secret"} {
for _, secret := range []string{tc.secret, "query-secret", "response-secret", "header-secret", "cookie-secret"} {
if strings.Contains(logged, secret) {
t.Fatalf("敏感路由日志泄露 %q%s", secret, logged)
}
@@ -131,6 +133,92 @@ func TestSensitiveRouteLongBodyRecordsTruncationWithoutRawSecret(t *testing.T) {
}
}
func TestRealConfigurationRoutesOnlyLogSafeSummary(t *testing.T) {
for _, path := range []string{
"/api/admin/system-configs/payment.private_key",
"/api/admin/wechat-configs/1",
} {
t.Run(path, func(t *testing.T) {
var output bytes.Buffer
log := zap.New(zapcore.NewCore(zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()), zapcore.AddSync(&output), zapcore.InfoLevel))
app := fiber.New()
app.Use(MiddlewareWithLogger(log))
app.Put(path, func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{"value": "response-private-material", "status": "ok"})
})
request := httptest.NewRequest("PUT", path, strings.NewReader(`{"value":"request-private-material","config_key":"payment.private_key"}`))
request.Header.Set("Content-Type", "application/json")
response, err := app.Test(request)
if err != nil {
t.Fatalf("执行配置路由请求失败:%v", err)
}
_ = response.Body.Close()
logged := output.String()
for _, secret := range []string{"request-private-material", "response-private-material"} {
if strings.Contains(logged, secret) {
t.Fatalf("配置路由日志泄露 %q%s", secret, logged)
}
}
if !strings.Contains(logged, `"body_policy":"sensitive_config"`) {
t.Fatalf("配置路由未应用安全摘要策略:%s", logged)
}
if !strings.Contains(logged, `\"present\":true`) || !strings.Contains(logged, `\"length\":`) {
t.Fatalf("配置路由未记录字段存在性和长度:%s", logged)
}
})
}
}
func TestLoginRouteDoesNotLogUsernameAndOnlyKeepsPresenceLength(t *testing.T) {
var output bytes.Buffer
log := zap.New(zapcore.NewCore(zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()), zapcore.AddSync(&output), zapcore.InfoLevel))
app := fiber.New()
app.Use(MiddlewareWithLogger(log))
app.Post("/api/auth/login", func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{"username": "visible-user", "success": true})
})
request := httptest.NewRequest("POST", "/api/auth/login", strings.NewReader(`{"username":"visible-user","password":"login-secret"}`))
request.Header.Set("Content-Type", "application/json")
response, err := app.Test(request)
if err != nil {
t.Fatalf("执行登录请求失败:%v", err)
}
_ = response.Body.Close()
logged := output.String()
if strings.Contains(logged, "visible-user") || strings.Contains(logged, "login-secret") {
t.Fatalf("登录路由泄露账号或凭证:%s", logged)
}
if !strings.Contains(logged, `\"present\":true`) || !strings.Contains(logged, `\"length\":`) {
t.Fatalf("登录路由未记录字段存在性和长度:%s", logged)
}
}
func TestPaymentRouteOnlyLogsPresenceLengthAndSafeResult(t *testing.T) {
var output bytes.Buffer
log := zap.New(zapcore.NewCore(zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()), zapcore.AddSync(&output), zapcore.InfoLevel))
app := fiber.New()
app.Use(MiddlewareWithLogger(log))
app.Post("/api/callback/alipay", func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{"payment_no": "PAY-SECRET-1", "amount": 12345, "status": "success"})
})
request := httptest.NewRequest("POST", "/api/callback/alipay", strings.NewReader("order_no=ORDER-SECRET-1&amount=12345&sign=signature-secret"))
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
response, err := app.Test(request)
if err != nil {
t.Fatalf("执行支付路由请求失败:%v", err)
}
_ = response.Body.Close()
logged := output.String()
for _, value := range []string{"PAY-SECRET-1", "ORDER-SECRET-1", "signature-secret"} {
if strings.Contains(logged, value) {
t.Fatalf("支付路由泄露原值 %q%s", value, logged)
}
}
if !strings.Contains(logged, `\"present\":true`) || !strings.Contains(logged, `\"status\":\"success\"`) {
t.Fatalf("支付路由缺少存在性摘要或安全结果:%s", logged)
}
}
func TestSanitizeInvalidJSONNeverFallsBackToRawBody(t *testing.T) {
t.Parallel()