// Package audit 提供统一审计事件的只读调查查询。 package audit import ( "context" "time" "github.com/bytedance/sonic" "gorm.io/datatypes" "gorm.io/gorm" "github.com/break/junhong_cmp_fiber/internal/model" "github.com/break/junhong_cmp_fiber/pkg/constants" "github.com/break/junhong_cmp_fiber/pkg/errors" "github.com/break/junhong_cmp_fiber/pkg/middleware" ) // EventFilter 定义平台全局事件列表的受控组合筛选。 type EventFilter struct { CreatedFrom *time.Time CreatedTo *time.Time Action string Category string ActorKind string ActorID string Source string Result string Risk string ScopeType string ScopeID string ResourceType string ResourceID string ResourceKey string RequestID string CorrelationID string Page int PageSize int } // EventPage 是平台全局事件稳定分页结果。 type EventPage struct { Total int64 `json:"total"` Page int `json:"page"` PageSize int `json:"page_size"` Items []EventView `json:"items"` } // EventView 是不暴露 GORM Model 的审计事件投影。 type EventView struct { EventID string `json:"event_id"` OccurredAt time.Time `json:"occurred_at"` Category string `json:"category"` ActionCode string `json:"action_code"` ActionName string `json:"action_name"` Summary string `json:"summary"` ActorKind string `json:"actor_kind"` ActorID string `json:"actor_id"` ActorName string `json:"actor_name"` ActorShopID *uint `json:"actor_shop_id,omitempty"` ActorShopName string `json:"actor_shop_name"` ActorEnterpriseID *uint `json:"actor_enterprise_id,omitempty"` ActorEnterpriseName string `json:"actor_enterprise_name"` Source string `json:"source"` RequestPath string `json:"request_path"` RequestMethod string `json:"request_method"` IPAddress string `json:"ip_address"` UserAgent string `json:"user_agent"` ScopeType string `json:"scope_type"` ScopeID string `json:"scope_id"` ScopeName string `json:"scope_name"` Result string `json:"result"` RiskLevel string `json:"risk_level"` ErrorCode string `json:"error_code"` ErrorSummary string `json:"error_summary"` RequestID string `json:"request_id"` CorrelationID string `json:"correlation_id"` ParentEventID string `json:"parent_event_id"` BatchTotal int `json:"batch_total"` SuccessCount int `json:"success_count"` FailCount int `json:"fail_count"` Metadata map[string]any `json:"metadata"` ContentHash string `json:"content_hash"` CreatedAt time.Time `json:"created_at"` Resources []ResourceView `json:"resources"` InvestigationRefs InvestigationRefs `json:"investigation_refs"` } // InvestigationRefs 是平台调查视角间唯一允许使用的稳定跳转引用。 type InvestigationRefs struct { EventID *string `json:"event_id"` ActorRef *ActorRef `json:"actor_ref"` ResourceRefs []InvestigationResourceRef `json:"resource_refs"` RequestID *string `json:"request_id"` CorrelationID *string `json:"correlation_id"` IntegrationRefs []IntegrationRef `json:"integration_refs"` } // ActorRef 是操作者时间线的稳定引用。 type ActorRef struct { Kind string `json:"kind"` ID string `json:"id"` } // InvestigationResourceRef 是通用资源时间线的稳定引用。 type InvestigationResourceRef struct { ResourceType string `json:"resource_type"` ResourceID *string `json:"resource_id"` ResourceKey string `json:"resource_key"` DisplayName string `json:"display_name"` } // IntegrationRef 是 Integration 详情的稳定引用。 type IntegrationRef struct { IntegrationID string `json:"integration_id"` } // ResourceView 是事件发生时独立资源身份与变化的只读投影。 type ResourceView struct { ResourceType string `json:"resource_type"` ResourceID *string `json:"resource_id,omitempty"` ResourceKey string `json:"resource_key"` DisplayName string `json:"display_name"` Relation string `json:"relation"` Role string `json:"role"` IdentitySnapshot map[string]any `json:"identity_snapshot"` BeforeData map[string]any `json:"before_data"` AfterData map[string]any `json:"after_data"` SubjectVisibility string `json:"subject_visibility"` SubjectSummary string `json:"subject_summary"` SubjectData map[string]any `json:"subject_data"` SortOrder int `json:"sort_order"` CreatedAt time.Time `json:"created_at"` } // Query 提供平台统一审计事件列表与详情读取。 type Query struct { db *gorm.DB } // New 创建统一审计事件 Query。 func New(db *gorm.DB) *Query { return &Query{db: db} } // List 查询平台范围的全局审计事件。 func (q *Query) List(ctx context.Context, filter EventFilter) (*EventPage, error) { if err := q.authorize(ctx); err != nil { return nil, err } if !validEventFilter(filter) { return nil, errors.New(errors.CodeInvalidParam) } filter.Page, filter.PageSize = normalizePage(filter.Page, filter.PageSize) query := q.applyFilters(q.db.WithContext(ctx).Model(&model.AuditEvent{}), filter) var total int64 if err := query.Count(&total).Error; err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "统计审计事件失败") } rows := make([]model.AuditEvent, 0, filter.PageSize) if err := query.Order("occurred_at DESC, id DESC"). Offset((filter.Page - 1) * filter.PageSize).Limit(filter.PageSize).Find(&rows).Error; err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询审计事件失败") } items, err := q.project(ctx, rows) if err != nil { return nil, err } return &EventPage{Total: total, Page: filter.Page, PageSize: filter.PageSize, Items: items}, nil } func validEventFilter(filter EventFilter) bool { return validOptionalValue(filter.Result, constants.AuditResultSuccess, constants.AuditResultFailed, constants.AuditResultDenied, constants.AuditResultPartial, constants.AuditResultUnknown) && validOptionalValue(filter.Risk, constants.AuditRiskLow, constants.AuditRiskNormal, constants.AuditRiskHigh, constants.AuditRiskCritical) && validOptionalValue(filter.Source, constants.AuditSourceAdminAPI, constants.AuditSourcePersonalAPI, constants.AuditSourceOpenAPI, constants.AuditSourceWorker, constants.AuditSourceScheduler, constants.AuditSourceCallback) && (filter.ActorKind == "" || validActorKind(filter.ActorKind)) && filter.Page >= 0 && filter.PageSize >= 0 && filter.PageSize <= constants.MaxPageSize } func validOptionalValue(value string, allowed ...string) bool { if value == "" { return true } for _, candidate := range allowed { if value == candidate { return true } } return false } // Get 查询平台范围的单个稳定审计事件详情。 func (q *Query) Get(ctx context.Context, eventID string) (*EventView, error) { if err := q.authorize(ctx); err != nil { return nil, err } if eventID == "" { return nil, errors.New(errors.CodeInvalidParam) } var row model.AuditEvent if err := q.db.WithContext(ctx).Where("event_id = ?", eventID).First(&row).Error; err != nil { if err == gorm.ErrRecordNotFound { return nil, errors.New(errors.CodeNotFound, "审计事件不存在") } return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询审计事件详情失败") } items, err := q.project(ctx, []model.AuditEvent{row}) if err != nil { return nil, err } return &items[0], nil } func (q *Query) authorize(ctx context.Context) error { if q == nil || q.db == nil { return errors.New(errors.CodeServiceUnavailable, "审计查询能力未配置") } userType := middleware.GetUserTypeFromContext(ctx) if userType != constants.UserTypeSuperAdmin && userType != constants.UserTypePlatform { return errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在") } return nil } func (q *Query) applyFilters(query *gorm.DB, filter EventFilter) *gorm.DB { if filter.CreatedFrom != nil { query = query.Where("occurred_at >= ?", filter.CreatedFrom.UTC()) } if filter.CreatedTo != nil { query = query.Where("occurred_at < ?", filter.CreatedTo.UTC()) } for column, value := range map[string]string{ "action_code": filter.Action, "category": filter.Category, "actor_kind": filter.ActorKind, "actor_id": filter.ActorID, "source": filter.Source, "result": filter.Result, "risk_level": filter.Risk, "scope_type": filter.ScopeType, "scope_id": filter.ScopeID, "request_id": filter.RequestID, "correlation_id": filter.CorrelationID, } { if value != "" { query = query.Where(column+" = ?", value) } } if filter.ResourceType != "" || filter.ResourceID != "" || filter.ResourceKey != "" { resource := q.db.Table("tb_audit_event_resource AS aer").Select("1"). Where("aer.audit_event_id = tb_audit_event.id") if filter.ResourceType != "" { resource = resource.Where("aer.resource_type = ?", filter.ResourceType) } if filter.ResourceID != "" { resource = resource.Where("aer.resource_id = ?", filter.ResourceID) } if filter.ResourceKey != "" { resource = resource.Where("aer.resource_key = ?", filter.ResourceKey) } query = query.Where("EXISTS (?)", resource) } return query } func (q *Query) project(ctx context.Context, rows []model.AuditEvent) ([]EventView, error) { items := make([]EventView, len(rows)) if len(rows) == 0 { return items, nil } ids := make([]uint, 0, len(rows)) for _, row := range rows { ids = append(ids, row.ID) } var resources []model.AuditEventResource if err := q.db.WithContext(ctx).Where("audit_event_id IN ?", ids). Order("audit_event_id ASC, sort_order ASC, id ASC").Find(&resources).Error; err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "批量查询审计事件资源失败") } resourcesByEvent := make(map[uint][]ResourceView, len(rows)) for _, resource := range resources { view, err := projectResource(resource) if err != nil { return nil, err } resourcesByEvent[resource.AuditEventID] = append(resourcesByEvent[resource.AuditEventID], view) } for index, row := range rows { metadata, err := decodeObject(row.Metadata) if err != nil { return nil, err } items[index] = EventView{ EventID: row.EventID, OccurredAt: row.OccurredAt, Category: row.Category, ActionCode: row.ActionCode, ActionName: row.ActionName, Summary: row.Summary, ActorKind: row.ActorKind, ActorID: row.ActorID, ActorName: row.ActorName, ActorShopID: row.ActorShopID, ActorShopName: row.ActorShopName, ActorEnterpriseID: row.ActorEnterpriseID, ActorEnterpriseName: row.ActorEnterpriseName, Source: row.Source, RequestPath: row.RequestPath, RequestMethod: row.RequestMethod, IPAddress: row.IPAddress, UserAgent: row.UserAgent, ScopeType: row.ScopeType, ScopeID: row.ScopeID, ScopeName: row.ScopeName, Result: row.Result, RiskLevel: row.RiskLevel, ErrorCode: row.ErrorCode, ErrorSummary: row.ErrorSummary, RequestID: row.RequestID, CorrelationID: row.CorrelationID, ParentEventID: row.ParentEventID, BatchTotal: row.BatchTotal, SuccessCount: row.SuccessCount, FailCount: row.FailCount, Metadata: metadata, ContentHash: row.ContentHash, CreatedAt: row.CreatedAt, Resources: resourcesByEvent[row.ID], } if items[index].Resources == nil { items[index].Resources = []ResourceView{} } items[index].InvestigationRefs = investigationRefs(row, items[index].Resources) } return items, nil } func investigationRefs(event model.AuditEvent, resources []ResourceView) InvestigationRefs { refs := InvestigationRefs{ EventID: stringPointer(event.EventID), ActorRef: investigationActorRef(event.ActorKind, event.ActorID), ResourceRefs: make([]InvestigationResourceRef, 0, len(resources)), RequestID: stringPointer(event.RequestID), CorrelationID: stringPointer(event.CorrelationID), IntegrationRefs: []IntegrationRef{}, } for _, resource := range resources { refs.ResourceRefs = append(refs.ResourceRefs, InvestigationResourceRef{ ResourceType: resource.ResourceType, ResourceID: resource.ResourceID, ResourceKey: resource.ResourceKey, DisplayName: resource.DisplayName, }) } return refs } func investigationActorRef(kind, id string) *ActorRef { if id == "" { return nil } switch kind { case constants.AuditActorAccount, constants.AuditActorOpenAPI, constants.AuditActorSystemTask, constants.AuditActorScheduledJob, constants.AuditActorExternalSystem: return &ActorRef{Kind: kind, ID: id} default: return nil } } func stringPointer(value string) *string { if value == "" { return nil } return &value } func projectResource(row model.AuditEventResource) (ResourceView, error) { identity, err := decodeObject(row.IdentitySnapshot) if err != nil { return ResourceView{}, err } before, err := decodeObject(row.BeforeData) if err != nil { return ResourceView{}, err } after, err := decodeObject(row.AfterData) if err != nil { return ResourceView{}, err } subject, err := decodeObject(row.SubjectData) if err != nil { return ResourceView{}, err } return ResourceView{ ResourceType: row.ResourceType, ResourceID: row.ResourceID, ResourceKey: row.ResourceKey, DisplayName: row.DisplayName, Relation: row.Relation, Role: row.Role, IdentitySnapshot: identity, BeforeData: before, AfterData: after, SubjectVisibility: row.SubjectVisibility, SubjectSummary: row.SubjectSummary, SubjectData: subject, SortOrder: row.SortOrder, CreatedAt: row.CreatedAt, }, nil } func decodeObject(value datatypes.JSON) (map[string]any, error) { result := map[string]any{} if len(value) == 0 { return result, nil } if err := sonic.Unmarshal(value, &result); err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "解析审计结构化字段失败") } return result, nil } func normalizePage(page, pageSize int) (int, int) { if page < 1 { page = 1 } if pageSize < 1 { pageSize = constants.DefaultPageSize } if pageSize > constants.MaxPageSize { pageSize = constants.MaxPageSize } return page, pageSize }