package events import ( "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "strings" ) const ClassificationSafe = "safe" const ClassificationRepair = "repair" const ClassificationQuarantine = "quarantine" const ClassificationExpire = "expire" // InventoryRow is a read-only classification of one envelope/delivery. type InventoryRow struct { EventID string Consumer string EventName string Environment string ProjectID int64 SchemaVersion int PayloadDigest string Classification string Reason string } func ClassifyEnvelope(env Envelope, consumer string) InventoryRow { row := InventoryRow{ EventID: env.EventID, Consumer: consumer, EventName: env.EventName, Environment: env.Environment, ProjectID: env.ProjectID, SchemaVersion: env.SchemaVersion, PayloadDigest: env.PayloadDigest, } if strings.TrimSpace(env.Environment) == "" || env.ProjectID <= 0 { row.Classification = ClassificationQuarantine row.Reason = "missing_scope" return row } c, ok := contractByName(env.EventName) if !ok { row.Classification = ClassificationQuarantine row.Reason = "unknown_event" return row } if !c.Enabled { row.Classification = ClassificationQuarantine row.Reason = "disabled_event" return row } if consumer != "" && (consumer == ConsumerFAV || !containsName(c.Consumers, consumer)) { row.Classification = ClassificationQuarantine row.Reason = "unknown_or_disabled_consumer" return row } if env.SchemaVersion != c.SchemaVersion { row.Classification = ClassificationQuarantine row.Reason = "unknown_payload_schema" return row } row.Classification = ClassificationSafe row.Reason = "ok" return row } // RepairRecord is an audited in-place field fix. Callers must persist it; // this package never deletes envelopes. type RepairRecord struct { EventID string FieldName string OldDigest string NewDigest string Operator string Reason string ApprovalRef string } func DigestText(v string) string { sum := sha256.Sum256([]byte(v)) return hex.EncodeToString(sum[:]) } func ValidateRepair(r RepairRecord) error { if r.EventID == "" || r.FieldName == "" || r.Operator == "" || r.Reason == "" || r.ApprovalRef == "" { return ErrIntegrityFailure } if r.OldDigest == "" || r.NewDigest == "" || r.OldDigest == r.NewDigest { return ErrIntegrityFailure } return nil } func SanitizeSummary(fields map[string]string, allowlist []string) string { out := map[string]string{} for _, k := range allowlist { if v, ok := fields[k]; ok { out[k] = v } } b, _ := json.Marshal(out) return string(b) } func InventoryJSON(rows []InventoryRow) ([]byte, error) { type wire struct { EventID string `json:"event_id"` Consumer string `json:"consumer"` Classification string `json:"classification"` Reason string `json:"reason"` PayloadDigest string `json:"payload_digest"` } w := make([]wire, 0, len(rows)) for _, r := range rows { w = append(w, wire{r.EventID, r.Consumer, r.Classification, r.Reason, r.PayloadDigest}) } b, err := json.Marshal(w) if err != nil { return nil, fmt.Errorf("inventory: %w", err) } return b, nil }