106 lines
3.2 KiB
Go
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")
|
|
}
|