package audit import ( "errors" "strings" "time" "git.apinb.com/ops/logs/internal/impl" "git.apinb.com/ops/logs/internal/models" ) const ( RiskNormal = "normal" RiskDangerous = "dangerous" ApprovalPending = "pending" ApprovalApproved = "approved" ApprovalRejected = "rejected" ) type Record struct { TraceID string `json:"trace_id,omitempty"` SourceService string `json:"source_service,omitempty"` ActorID string `json:"actor_id,omitempty"` ActorName string `json:"actor_name,omitempty"` Action string `json:"action,omitempty"` ObjectType string `json:"object_type,omitempty"` ObjectID string `json:"object_id,omitempty"` OperationRisk string `json:"operation_risk,omitempty"` ApprovalID string `json:"approval_id,omitempty"` RequestMethod string `json:"request_method,omitempty"` RequestPath string `json:"request_path,omitempty"` ClientIP string `json:"client_ip,omitempty"` BeforeJSON string `json:"before_json,omitempty"` AfterJSON string `json:"after_json,omitempty"` Result string `json:"result,omitempty"` ErrorMessage string `json:"error_message,omitempty"` } type ApprovalRequest struct { RequestID string `json:"request_id,omitempty"` SourceService string `json:"source_service,omitempty"` Action string `json:"action,omitempty"` ObjectType string `json:"object_type,omitempty"` ObjectID string `json:"object_id,omitempty"` RequesterID string `json:"requester_id,omitempty"` RequesterName string `json:"requester_name,omitempty"` Reason string `json:"reason,omitempty"` BeforeJSON string `json:"before_json,omitempty"` AfterJSON string `json:"after_json,omitempty"` Status string `json:"status,omitempty"` ReviewerID string `json:"reviewer_id,omitempty"` ReviewerName string `json:"reviewer_name,omitempty"` ReviewComment string `json:"review_comment,omitempty"` ReviewedAt *time.Time `json:"reviewed_at,omitempty"` } func NormalizeRecord(record Record) Record { record.TraceID = strings.TrimSpace(record.TraceID) record.SourceService = strings.TrimSpace(record.SourceService) record.ActorID = strings.TrimSpace(record.ActorID) record.ActorName = strings.TrimSpace(record.ActorName) record.Action = strings.TrimSpace(record.Action) record.ObjectType = strings.TrimSpace(record.ObjectType) record.ObjectID = strings.TrimSpace(record.ObjectID) record.OperationRisk = strings.TrimSpace(strings.ToLower(record.OperationRisk)) record.ApprovalID = strings.TrimSpace(record.ApprovalID) record.RequestMethod = strings.TrimSpace(strings.ToUpper(record.RequestMethod)) record.RequestPath = strings.TrimSpace(record.RequestPath) record.ClientIP = strings.TrimSpace(record.ClientIP) record.Result = strings.TrimSpace(record.Result) if record.Result == "" { record.Result = "success" } if record.OperationRisk == "" { if IsDangerousOperation(record.Action, record.ObjectType) { record.OperationRisk = RiskDangerous } else { record.OperationRisk = RiskNormal } } return record } func ValidateRecord(record Record) error { record = NormalizeRecord(record) if record.SourceService == "" { return errors.New("source_service is required") } if record.ActorID == "" { return errors.New("actor_id is required") } if record.Action == "" { return errors.New("action is required") } if record.ObjectType == "" { return errors.New("object_type is required") } if record.ObjectID == "" { return errors.New("object_id is required") } if record.OperationRisk != RiskNormal && record.OperationRisk != RiskDangerous { return errors.New("operation_risk must be normal or dangerous") } if record.OperationRisk == RiskDangerous && record.ApprovalID == "" { return errors.New("approval_id is required for dangerous operation") } return nil } func IsDangerousOperation(action, objectType string) bool { key := strings.ToLower(strings.TrimSpace(action) + " " + strings.TrimSpace(objectType)) dangerWords := []string{ "notification_policy", "notification policy", "silence_policy", "suppression", "escalation_policy", "automation_script", "script.execute", "script.rollback", } for _, word := range dangerWords { if strings.Contains(key, word) { return true } } return false } func SaveRecord(record Record) (models.AuditLog, error) { record = NormalizeRecord(record) if err := ValidateRecord(record); err != nil { return models.AuditLog{}, err } row := models.AuditLog{ TraceID: record.TraceID, SourceService: record.SourceService, ActorID: record.ActorID, ActorName: record.ActorName, Action: record.Action, ObjectType: record.ObjectType, ObjectID: record.ObjectID, OperationRisk: record.OperationRisk, ApprovalID: record.ApprovalID, RequestMethod: record.RequestMethod, RequestPath: record.RequestPath, ClientIP: record.ClientIP, BeforeJSON: record.BeforeJSON, AfterJSON: record.AfterJSON, Result: record.Result, ErrorMessage: record.ErrorMessage, } if err := impl.DBService.Create(&row).Error; err != nil { return models.AuditLog{}, err } return row, nil } func NormalizeApproval(req ApprovalRequest) ApprovalRequest { req.RequestID = strings.TrimSpace(req.RequestID) req.SourceService = strings.TrimSpace(req.SourceService) req.Action = strings.TrimSpace(req.Action) req.ObjectType = strings.TrimSpace(req.ObjectType) req.ObjectID = strings.TrimSpace(req.ObjectID) req.RequesterID = strings.TrimSpace(req.RequesterID) req.RequesterName = strings.TrimSpace(req.RequesterName) req.Status = strings.TrimSpace(strings.ToLower(req.Status)) req.ReviewerID = strings.TrimSpace(req.ReviewerID) req.ReviewerName = strings.TrimSpace(req.ReviewerName) if req.Status == "" { req.Status = ApprovalPending } return req } func ValidateApprovalRequest(req ApprovalRequest) error { req = NormalizeApproval(req) if req.SourceService == "" { return errors.New("source_service is required") } if req.Action == "" { return errors.New("action is required") } if req.ObjectType == "" { return errors.New("object_type is required") } if req.ObjectID == "" { return errors.New("object_id is required") } if req.RequesterID == "" { return errors.New("requester_id is required") } if !IsDangerousOperation(req.Action, req.ObjectType) { return errors.New("operation is not classified as dangerous") } return nil } func Transition(req ApprovalRequest, nextStatus, reviewerID, comment string) (ApprovalRequest, error) { req = NormalizeApproval(req) nextStatus = strings.TrimSpace(strings.ToLower(nextStatus)) reviewerID = strings.TrimSpace(reviewerID) if req.Status != ApprovalPending { return ApprovalRequest{}, errors.New("only pending approval can be reviewed") } if nextStatus != ApprovalApproved && nextStatus != ApprovalRejected { return ApprovalRequest{}, errors.New("next status must be approved or rejected") } if reviewerID == "" { return ApprovalRequest{}, errors.New("reviewer_id is required") } now := time.Now() req.Status = nextStatus req.ReviewerID = reviewerID req.ReviewComment = strings.TrimSpace(comment) req.ReviewedAt = &now return req, nil }