Files
yovision/Bell/server/app/bell/event/normalize.go
T

106 lines
3.2 KiB
Go

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")
}