package event import ( "bytes" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "strings" "time" "unicode/utf8" ) const ( maxProducerID = 128 maxSourceEventID = 256 maxEventType = 128 maxLocation = 256 maxEvidenceRef = 512 maxAttributes = 48 * 1024 ) type Command struct { ProducerID string `json:"producerId"` SourceEventID string `json:"sourceEventId"` EventType string `json:"eventType"` OccurredAt time.Time `json:"occurredAt"` Location string `json:"location"` Severity string `json:"severity"` EvidenceRef *string `json:"evidenceRef,omitempty"` Attributes map[string]any `json:"attributes"` } type Normalized struct { Command Command Payload []byte Digest string } func Normalize(command Command) (Normalized, error) { command.ProducerID = strings.TrimSpace(command.ProducerID) command.SourceEventID = strings.TrimSpace(command.SourceEventID) command.EventType = strings.TrimSpace(command.EventType) command.Location = strings.TrimSpace(command.Location) command.Severity = strings.ToLower(strings.TrimSpace(command.Severity)) command.OccurredAt = command.OccurredAt.UTC() if !validText(command.ProducerID, maxProducerID) || !validText(command.SourceEventID, maxSourceEventID) || !validText(command.EventType, maxEventType) || !validText(command.Location, maxLocation) || command.OccurredAt.IsZero() { return Normalized{}, ErrInvalid } switch command.Severity { case "low", "medium", "high", "critical": default: return Normalized{}, ErrInvalid } if command.EvidenceRef != nil { value := strings.TrimSpace(*command.EvidenceRef) lower := strings.ToLower(value) if !validText(value, maxEvidenceRef) || strings.Contains(value, "@") || strings.Contains(value, "\\") || strings.HasPrefix(lower, "file:") { return Normalized{}, fmt.Errorf("%w: evidenceRef 必须是安全的逻辑引用", ErrInvalid) } command.EvidenceRef = &value } if command.Attributes == nil { command.Attributes = map[string]any{} } attributes, err := canonicalJSON(command.Attributes) if err != nil || len(attributes) > maxAttributes { return Normalized{}, fmt.Errorf("%w: attributes 无效或过大", ErrInvalid) } // Round trip the attributes so map ordering and nested values have one // deterministic representation before the complete command is hashed. if err = json.Unmarshal(attributes, &command.Attributes); err != nil { return Normalized{}, fmt.Errorf("%w: attributes 无效", ErrInvalid) } payload, err := json.Marshal(command) if err != nil { return Normalized{}, fmt.Errorf("normalize event: %w", err) } digest := sha256.Sum256(payload) return Normalized{Command: command, Payload: payload, Digest: hex.EncodeToString(digest[:])}, nil } func canonicalJSON(value any) ([]byte, error) { raw, err := json.Marshal(value) if err != nil { return nil, err } decoder := json.NewDecoder(bytes.NewReader(raw)) decoder.UseNumber() var normalized any if err = decoder.Decode(&normalized); err != nil { return nil, err } return json.Marshal(normalized) } func validText(value string, max int) bool { return value != "" && utf8.ValidString(value) && utf8.RuneCountInString(value) <= max && !strings.ContainsAny(value, "\x00\r\n") }