Files
weichenandCursor bcb122ac53 feat(events): fail-closed scope, static registry, inventory, and replay digest
Reject empty environment/project, disable FAV consumers at register time, and keep historical classification/repair/replay as audited application use cases with no raw SQL entrypoint.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-15 08:52:30 +00:00

122 lines
3.1 KiB
Go

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
}