package event_ingress import ( "bytes" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "fmt" "io" "math" "regexp" "strconv" "strings" "time" ) var ( ErrInvalidEvent = errors.New("invalid_event") ErrUnsupportedSchema = errors.New("unsupported_schema_version") identifierPattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$`) hexDigestPattern = regexp.MustCompile(`^[a-f0-9]{64}$`) ) func ParseEvent(raw []byte) (ParsedEvent, error) { var event Event decoder := json.NewDecoder(bytes.NewReader(raw)) decoder.DisallowUnknownFields() if err := decoder.Decode(&event); err != nil { return ParsedEvent{}, fmt.Errorf("%w: malformed or unknown member", ErrInvalidEvent) } if err := decoder.Decode(&struct{}{}); !errors.Is(err, io.EOF) { return ParsedEvent{}, fmt.Errorf("%w: trailing JSON value", ErrInvalidEvent) } if event.SchemaVersion == "" { return ParsedEvent{}, ErrInvalidEvent } if event.SchemaVersion != EventSchemaVersion { return ParsedEvent{}, ErrUnsupportedSchema } occurred, err := time.Parse("2006-01-02T15:04:05.000Z", event.OccurredAt) if err != nil || !validID(event.ProducerID) || !validID(event.SourceEventID) || !validID(event.SiteRef) || !validID(event.DeviceRef) || !validID(event.ProfileRef) || !validID(event.Rule.RuleID) || !validID(event.Observation.TrackID) || !validID(event.Region.RegionID) || event.Rule.Version == "" || len(event.Rule.Version) > 64 || event.Model.Name == "" || len(event.Model.Name) > 128 || event.Model.Version == "" || len(event.Model.Version) > 64 || event.Observation.Confidence < 0 || event.Observation.Confidence > 1 || math.IsNaN(event.Observation.Confidence) || math.IsInf(event.Observation.Confidence, 0) { return ParsedEvent{}, ErrInvalidEvent } if event.EventType != "dangerous_area_entered" && event.EventType != "directional_line_crossed" { return ParsedEvent{}, ErrInvalidEvent } if event.Severity != "low" && event.Severity != "medium" && event.Severity != "high" && event.Severity != "critical" { return ParsedEvent{}, ErrInvalidEvent } if event.Observation.Category != "person" && event.Observation.Category != "vehicle" && event.Observation.Category != "other" { return ParsedEvent{}, ErrInvalidEvent } if len(event.Observation.BBoxNormalized) != 0 && len(event.Observation.BBoxNormalized) != 4 { return ParsedEvent{}, ErrInvalidEvent } for _, value := range event.Observation.BBoxNormalized { if value < 0 || value > 1 || math.IsNaN(value) || math.IsInf(value, 0) { return ParsedEvent{}, ErrInvalidEvent } } if (event.EventType == "dangerous_area_entered" && (event.Region.Kind != "area" || event.Region.CrossingDirection != "")) || (event.EventType == "directional_line_crossed" && (event.Region.Kind != "line" || (event.Region.CrossingDirection != "a_to_b" && event.Region.CrossingDirection != "b_to_a"))) { return ParsedEvent{}, ErrInvalidEvent } if event.Evidence == nil || len(event.Evidence) > 8 { return ParsedEvent{}, ErrInvalidEvent } seenEvidence := map[string]bool{} for _, evidence := range event.Evidence { evidenceJSON, marshalErr := json.Marshal(evidence) canonicalEvidence, canonicalErr := canonicalJSON(evidenceJSON) if err := validateEvidence(evidence); err != nil || marshalErr != nil || canonicalErr != nil || seenEvidence[string(canonicalEvidence)] { return ParsedEvent{}, ErrInvalidEvent } seenEvidence[string(canonicalEvidence)] = true } canonical, err := canonicalJSON(raw) if err != nil { return ParsedEvent{}, ErrInvalidEvent } if containsExplicitNull(raw) { return ParsedEvent{}, fmt.Errorf("%w: optional members must be omitted", ErrInvalidEvent) } digest := sha256.Sum256(canonical) return ParsedEvent{Event: event, Canonical: canonical, Digest: hex.EncodeToString(digest[:]), Occurred: occurred}, nil } func validateEvidence(value Evidence) error { if value.SchemaVersion != EvidenceSchemaVersion || !validID(value.EvidenceID) || !validID(value.OwnerID) || (value.Type != "snapshot" && value.Type != "clip") { return ErrInvalidEvent } if _, err := time.Parse(time.RFC3339Nano, value.CapturedAt); err != nil { return ErrInvalidEvent } if _, err := time.Parse(time.RFC3339Nano, value.StatusUpdatedAt); err != nil { return ErrInvalidEvent } if value.ExpiresAt != "" { if _, err := time.Parse(time.RFC3339Nano, value.ExpiresAt); err != nil { return ErrInvalidEvent } } switch value.Status { case "pending", "processing": if value.ContentType != "" || value.Integrity != nil || value.Failure != nil { return ErrInvalidEvent } case "success": if value.Integrity == nil || value.Failure != nil || (value.ContentType != "image/jpeg" && value.ContentType != "image/png" && value.ContentType != "video/mp4") || value.Integrity.Algorithm != "sha256" || !hexDigestPattern.MatchString(value.Integrity.Digest) || value.Integrity.SizeBytes < 0 { return ErrInvalidEvent } case "failed": if value.Failure == nil || value.ContentType != "" || value.Integrity != nil || (value.Failure.Code != "capture_failed" && value.Failure.Code != "processing_failed" && value.Failure.Code != "expired" && value.Failure.Code != "unavailable") { return ErrInvalidEvent } default: return ErrInvalidEvent } return nil } func canonicalJSON(raw []byte) ([]byte, error) { decoder := json.NewDecoder(bytes.NewReader(raw)) decoder.UseNumber() var value any if err := decoder.Decode(&value); err != nil { return nil, err } value, err := normalizeJCSNumbers(value) if err != nil { return nil, err } var buffer bytes.Buffer encoder := json.NewEncoder(&buffer) encoder.SetEscapeHTML(false) if err := encoder.Encode(value); err != nil { return nil, err } canonical := bytes.TrimSuffix(buffer.Bytes(), []byte("\n")) canonical = bytes.ReplaceAll(canonical, []byte(`\u2028`), []byte("\u2028")) canonical = bytes.ReplaceAll(canonical, []byte(`\u2029`), []byte("\u2029")) return canonical, nil } func normalizeJCSNumbers(value any) (any, error) { switch typed := value.(type) { case json.Number: number, err := strconv.ParseFloat(string(typed), 64) if err != nil || math.IsNaN(number) || math.IsInf(number, 0) { return nil, errors.New("JSON number is outside the RFC 8785 domain") } if number == 0 { return float64(0), nil } return number, nil case []any: for index, item := range typed { normalized, err := normalizeJCSNumbers(item) if err != nil { return nil, err } typed[index] = normalized } case map[string]any: for key, item := range typed { normalized, err := normalizeJCSNumbers(item) if err != nil { return nil, err } typed[key] = normalized } } return value, nil } func containsExplicitNull(raw []byte) bool { decoder := json.NewDecoder(bytes.NewReader(raw)) decoder.UseNumber() var value any if decoder.Decode(&value) != nil { return true } return hasNull(value) } func hasNull(value any) bool { switch typed := value.(type) { case nil: return true case []any: for _, item := range typed { if hasNull(item) { return true } } case map[string]any: for _, item := range typed { if hasNull(item) { return true } } } return false } func validID(value string) bool { return identifierPattern.MatchString(value) && !strings.ContainsAny(strings.ToLower(value), "\\/@") }