固化七月迭代审计治理进展以隔离线上热修
Constraint: 切换 main 前必须保存当前七月分支全部项目进展,套餐生效提案仅属于 Iteration/7-11。 Rejected: 将七月套餐修复直接移植到 main | 两个分支的可靠投递架构不同。 Confidence: medium Scope-risk: broad Directive: 不得将本提交整体 cherry-pick 到 main;main 套餐热修必须基于其纯 Asynq 代码独立实施。 Tested: git diff --check;openspec validate fix-package-activation-starvation --strict。 Not-tested: 按用户要求未运行自动化测试;go build ./... 因当前审计改造中的 Enterprise 模型字面量和 role.recordFailure 参数类型错误未通过。
This commit is contained in:
@@ -35,6 +35,7 @@ type Entry struct {
|
||||
PrimaryResource string `json:"primary_resource,omitempty"`
|
||||
AffectedResource string `json:"affected_resource,omitempty"`
|
||||
ActorSource string `json:"actor_source"`
|
||||
Visibility string `json:"visibility"`
|
||||
Transaction string `json:"transaction"`
|
||||
FailureStrategy string `json:"failure_strategy"`
|
||||
SensitivePolicy string `json:"sensitive_policy"`
|
||||
@@ -46,7 +47,10 @@ type Entry struct {
|
||||
// Scan 扫描当前仓库中对外 HTTP、Asynq Worker 和定时任务注册入口。
|
||||
func Scan(root string) ([]Entry, error) {
|
||||
var entries []Entry
|
||||
files := []string{"internal/routes", "internal/application", "internal/domain", "internal/service", "pkg/queue", "cmd/worker"}
|
||||
files := []string{
|
||||
"internal/routes", "internal/application", "internal/domain", "internal/service",
|
||||
"internal/handler", "internal/infrastructure", "internal/polling", "pkg/queue", "cmd/worker",
|
||||
}
|
||||
for _, directory := range files {
|
||||
err := filepath.Walk(filepath.Join(root, directory), func(path string, info os.FileInfo, walkErr error) error {
|
||||
if walkErr != nil {
|
||||
@@ -108,7 +112,10 @@ func scanFile(root, path string) ([]Entry, error) {
|
||||
if identifier, ok := call.Fun.(*ast.Ident); ok && identifier.Name == "Register" && len(call.Args) >= 7 {
|
||||
method, methodOK := stringLiteral(call.Args[3])
|
||||
pathSuffix, pathOK := stringLiteral(call.Args[4])
|
||||
if methodOK && pathOK {
|
||||
if !pathOK {
|
||||
pathSuffix = expression(call.Args[4])
|
||||
}
|
||||
if methodOK {
|
||||
entry := classifyHTTP(relative, position.Line, method, pathSuffix, expression(call.Args[5]), routeSummary(call.Args[6]))
|
||||
entries = append(entries, entry)
|
||||
}
|
||||
@@ -129,6 +136,12 @@ func scanFile(root, path string) ([]Entry, error) {
|
||||
entries = append(entries, classifySchedule(relative, position.Line, taskType, schedule))
|
||||
}
|
||||
}
|
||||
case "LogOperation":
|
||||
entries = append(entries, classifyLegacyWriter(relative, position.Line, expression(call.Fun)))
|
||||
case "Start", "Complete", "RecordInbound":
|
||||
if isIntegrationLogCall(relative, expression(selector.X)) {
|
||||
entries = append(entries, classifyIntegrationLog(relative, position.Line, expression(call.Fun)))
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
@@ -136,7 +149,7 @@ func scanFile(root, path string) ([]Entry, error) {
|
||||
strings.HasPrefix(relative, "internal/service/") {
|
||||
for _, declaration := range file.Decls {
|
||||
function, ok := declaration.(*ast.FuncDecl)
|
||||
if !ok || function.Recv == nil || !isBusinessMethod(function.Name.Name) {
|
||||
if !ok || function.Recv == nil || !isBusinessMethod(function) {
|
||||
continue
|
||||
}
|
||||
position := set.Position(function.Pos())
|
||||
@@ -154,14 +167,15 @@ func classifyHTTP(file string, line int, method, path, handler, summary string)
|
||||
entry := Entry{
|
||||
Key: fmt.Sprintf("http:%s:%d:%s:%s", file, line, method, path), Kind: "http",
|
||||
CodeEntry: fmt.Sprintf("%s:%d %s", file, line, handler), Owner: owner,
|
||||
Method: method, Path: path, Summary: summary, ActorSource: httpActorSource(file),
|
||||
Method: method, Path: path, Summary: summary, ActorSource: httpActorSource(file, path, handler),
|
||||
DomainLedger: ledgerDecision(owner), IntegrationLog: integrationDecision(file, path),
|
||||
Outbox: "按用例是否存在提交后可靠副作用决定;无可靠副作用时 N/A",
|
||||
Visibility: httpVisibility(file, path, handler),
|
||||
SensitivePolicy: "禁止字段删除;手机号、IP、ICCID、金额和第三方单号按权限脱敏;单字段 16KB 上限",
|
||||
BeforeAfterPolicy: "写操作保存脱敏后的直接字段变化;批量命令保存摘要和权威明细引用",
|
||||
TestSeam: "真实 Fiber + Application/Service 公共用例 + PostgreSQL 事实;覆盖门禁静态比对本入口",
|
||||
}
|
||||
if method == "GET" && !isSensitiveRead(file, path, summary) {
|
||||
if isReadOnlyHTTP(method, path) && !isSensitiveRead(file, path, summary) {
|
||||
entry.AuditEvent = "N/A"
|
||||
entry.Transaction = "N/A"
|
||||
entry.FailureStrategy = "Access Log 记录统一错误;普通读取不创建业务审计"
|
||||
@@ -192,6 +206,7 @@ func classifyWorker(file string, line int, taskType, handler string) Entry {
|
||||
Category: categoryFor(owner), Risk: riskFor(owner, taskType, handler),
|
||||
PrimaryResource: owner, AffectedResource: "任务载荷定位的直接业务资源",
|
||||
ActorSource: "system_task/asynq", Transaction: "业务状态变化、领域流水和 Audit Event 按用例原子提交",
|
||||
Visibility: "内部系统入口;外部主体只读取对应业务安全投影",
|
||||
FailureStrategy: "Worker 返回错误由公共重试恢复;终态失败保存中文安全摘要,禁止裸 goroutine 审计",
|
||||
SensitivePolicy: "不记录完整任务载荷、文件内容、外部正文、凭证或签名 URL",
|
||||
BeforeAfterPolicy: "状态变化保存直接前后值;无业务变化时仅保留 Integration Log",
|
||||
@@ -206,6 +221,7 @@ func classifySchedule(file string, line int, taskType, schedule string) Entry {
|
||||
CodeEntry: fmt.Sprintf("%s:%d", file, line), Owner: workerOwner(taskType), Summary: "按 " + schedule + " 调度 " + taskType,
|
||||
AuditEvent: "N/A", DomainLedger: "N/A", IntegrationLog: "N/A", Outbox: "N/A",
|
||||
ActorSource: "system_task/scheduled_job", Transaction: "N/A",
|
||||
Visibility: "内部系统入口,不直接对用户展示",
|
||||
FailureStrategy: "调度注册失败阻止 Worker 启动;执行结果由对应 Worker 入口负责",
|
||||
SensitivePolicy: "调度日志仅记录任务类型与安全时间信息",
|
||||
BeforeAfterPolicy: "N/A:调度入口不修改业务事实",
|
||||
@@ -230,6 +246,7 @@ func classifyBusinessMethod(file string, line int, method string) Entry {
|
||||
ActionCode: actionCode(owner, method), Risk: riskFor(owner, file, method),
|
||||
PrimaryResource: owner, AffectedResource: "完整用例直接修改或引用的资源",
|
||||
ActorSource: "由调用入口传入操作者与来源快照",
|
||||
Visibility: "由完整用例决定平台完整视图、主体安全投影或 internal_only",
|
||||
SensitivePolicy: "禁止字段删除;受控字段脱敏;批量明细留在领域任务或制品",
|
||||
BeforeAfterPolicy: "完整用例保存脱敏后的直接业务变化;Domain 方法由 Application 投影",
|
||||
TestSeam: "Application/Service 公共方法 + PostgreSQL 事实;Domain 使用纯领域测试;覆盖门禁静态比对本入口",
|
||||
@@ -254,6 +271,45 @@ func classifyBusinessMethod(file string, line int, method string) Entry {
|
||||
return entry
|
||||
}
|
||||
|
||||
func classifyLegacyWriter(file string, line int, call string) Entry {
|
||||
return Entry{
|
||||
Key: fmt.Sprintf("legacy_writer:%s:%d:%s", file, line, call), Kind: "legacy_writer",
|
||||
CodeEntry: fmt.Sprintf("%s:%d %s", file, line, call), Owner: filepath.Base(filepath.Dir(file)),
|
||||
Summary: "调用旧 Operation Log Writer", AuditEvent: "必须迁移到统一 Audit Event 后停写旧表",
|
||||
DomainLedger: "既有业务表仍是权威事实,旧 Operation Log 不是 Domain Ledger",
|
||||
IntegrationLog: "N/A:旧 Writer 仅记录内部操作;实际外部交互由 Integration Log 单独记录",
|
||||
Outbox: "由原完整用例决定,旧 Writer 不得替代 Outbox",
|
||||
ActionCode: actionCode(filepath.Base(filepath.Dir(file)), call), ActionName: "迁移旧审计写入",
|
||||
Category: categoryFor(file), Risk: riskFor(file, call, ""), PrimaryResource: filepath.Base(filepath.Dir(file)),
|
||||
AffectedResource: "按原完整业务用例登记实际资源", ActorSource: "沿用原调用入口真实操作者",
|
||||
Visibility: "旧表仅保留平台历史入口;新事件按 Registry 生成主体安全投影",
|
||||
Transaction: "迁移后关键成功与业务事实同事务,旧异步 Writer 停写",
|
||||
FailureStrategy: "迁移后业务回滚的 failed/denied 使用独立短事务;禁止裸 goroutine",
|
||||
SensitivePolicy: "迁移时删除密码、Token、Secret、私钥、Cookie、签名 URL 等安全凭据",
|
||||
BeforeAfterPolicy: "按资源保存本次直接变化,不复制旧单体 JSON",
|
||||
TestSeam: "静态调用归零扫描 + 对应业务入口与数据库抽样核对",
|
||||
}
|
||||
}
|
||||
|
||||
func classifyIntegrationLog(file string, line int, call string) Entry {
|
||||
return Entry{
|
||||
Key: fmt.Sprintf("integration_log:%s:%d:%s", file, line, call), Kind: "integration_log",
|
||||
CodeEntry: fmt.Sprintf("%s:%d %s", file, line, call), Owner: filepath.Base(filepath.Dir(file)),
|
||||
Summary: "记录外部交互尝试或终态", AuditEvent: "N/A",
|
||||
DomainLedger: "N/A:Integration Log 只记录外部交互事实,不替代内部业务表",
|
||||
IntegrationLog: "必须:保存实际请求、未发送裁决、入站回调或终态安全摘要",
|
||||
Outbox: "存在提交后可靠副作用时由业务用例另行登记;本调用点不替代 Outbox",
|
||||
ActorSource: "external_system 或发起外呼的真实 Application/Worker/Callback",
|
||||
Visibility: "仅平台内部调查完整可见;代理/企业不得读取外部交互细节",
|
||||
Transaction: "按外部尝试生命周期写入;内部状态变化另由业务事务记录 Audit Event",
|
||||
FailureStrategy: "保留真实 failed/unknown/not_sent 结果,不把记录失败伪装成业务成功",
|
||||
SensitivePolicy: "请求、响应和 metadata 写入前删除凭据,历史读取再次清理",
|
||||
BeforeAfterPolicy: "N/A:保存外部尝试结构化摘要和本地状态是否变化",
|
||||
TestSeam: "Integration Log 数据抽样 + 调用链 correlation/series/attempt 核对",
|
||||
NAReason: "该入口只记录外部交互事实;只有改变内部业务事实时才由业务用例另写 Audit Event",
|
||||
}
|
||||
}
|
||||
|
||||
func routeSummary(expr ast.Expr) string {
|
||||
composite, ok := expr.(*ast.CompositeLit)
|
||||
if !ok {
|
||||
@@ -305,13 +361,18 @@ func expression(expr ast.Expr) string {
|
||||
return value.Value
|
||||
case *ast.CallExpr:
|
||||
return expression(value.Fun)
|
||||
case *ast.BinaryExpr:
|
||||
return expression(value.X) + value.Op.String() + expression(value.Y)
|
||||
default:
|
||||
return fmt.Sprintf("%T", expr)
|
||||
}
|
||||
}
|
||||
|
||||
func httpActorSource(file string) string {
|
||||
func httpActorSource(file, path, handler string) string {
|
||||
text := strings.ToLower(file + " " + path + " " + handler)
|
||||
switch {
|
||||
case strings.Contains(text, "callback") || strings.Contains(path, "/carriers/"):
|
||||
return "external_system/callback"
|
||||
case strings.HasSuffix(file, "personal.go"):
|
||||
return "personal_customer/personal_api"
|
||||
case strings.HasSuffix(file, "order.go"):
|
||||
@@ -321,14 +382,46 @@ func httpActorSource(file string) string {
|
||||
}
|
||||
}
|
||||
|
||||
func httpVisibility(file, path, handler string) string {
|
||||
text := strings.ToLower(file + " " + path + " " + handler)
|
||||
switch {
|
||||
case strings.Contains(text, "callback"):
|
||||
return "外部回调入口;只记录内部完整事实,不直接向外部主体展示"
|
||||
case strings.Contains(text, "/audit"):
|
||||
return "仅超级管理员和平台账号可见"
|
||||
case strings.Contains(text, "enterprise"):
|
||||
return "企业认证上下文范围内可见;内部审计字段不可见"
|
||||
case strings.Contains(text, "personal"):
|
||||
return "当前个人客户本人范围内可见"
|
||||
default:
|
||||
return "按认证账号类型和现有数据权限可见;审计调查另按平台/主体投影隔离"
|
||||
}
|
||||
}
|
||||
|
||||
func isIntegrationLogCall(file, receiver string) bool {
|
||||
if strings.Contains(file, "/integrationlog/") {
|
||||
return false
|
||||
}
|
||||
receiver = strings.ToLower(receiver)
|
||||
return strings.Contains(receiver, "integration")
|
||||
}
|
||||
|
||||
func isSensitiveRead(file, path, summary string) bool {
|
||||
text := strings.ToLower(file + " " + path + " " + summary)
|
||||
for _, marker := range []string{"download", "export", "realname-link", "实名", "敏感", "完整", "operation-password"} {
|
||||
for _, marker := range []string{"download", "realname-link", "realname/link", "实名链接", "敏感", "realtime-status"} {
|
||||
if strings.Contains(text, marker) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
return strings.HasSuffix(file, "wecom.go") && path == "/applications" ||
|
||||
strings.HasSuffix(file, "export_task.go") && path == "/:id"
|
||||
}
|
||||
|
||||
func isReadOnlyHTTP(method, path string) bool {
|
||||
if method == "GET" {
|
||||
return true
|
||||
}
|
||||
return strings.Contains(path, "purchase-check") || strings.Contains(path, "verify-asset")
|
||||
}
|
||||
|
||||
func integrationDecision(file, path string) string {
|
||||
@@ -412,7 +505,11 @@ func workerOwner(taskType string) string {
|
||||
return normalize(strings.TrimPrefix(taskType, "constants.TaskType"))
|
||||
}
|
||||
|
||||
func isBusinessMethod(name string) bool {
|
||||
func isBusinessMethod(function *ast.FuncDecl) bool {
|
||||
name := function.Name.Name
|
||||
if strings.HasPrefix(name, "Set") && !hasContextParameter(function) {
|
||||
return false
|
||||
}
|
||||
for _, prefix := range []string{
|
||||
"Create", "Update", "Delete", "Set", "Assign", "Remove", "Cancel", "Reject", "Approve",
|
||||
"Import", "Allocate", "Recall", "Stop", "Resume", "Bind", "Unbind", "Reset", "Activate",
|
||||
@@ -429,6 +526,19 @@ func isBusinessMethod(name string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func hasContextParameter(function *ast.FuncDecl) bool {
|
||||
if function.Type.Params == nil {
|
||||
return false
|
||||
}
|
||||
for _, field := range function.Type.Params.List {
|
||||
selector, ok := field.Type.(*ast.SelectorExpr)
|
||||
if ok && expression(selector) == "context.Context" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func normalize(value string) string {
|
||||
value = strings.Trim(value, "\"")
|
||||
var output []rune
|
||||
|
||||
Reference in New Issue
Block a user