Freeze SCUM log-source tailing run contract

This commit is contained in:
npc0-hue
2026-08-13 14:37:16 +08:00
parent 41c109128d
commit 4f55c71de5
18 changed files with 627 additions and 6 deletions
+158
View File
@@ -3,6 +3,7 @@ package validator
import (
"encoding/json"
"fmt"
"net"
"regexp"
"strings"
@@ -26,6 +27,10 @@ const (
maxSCUMRCONConfirmRecords = 128
maxSCUMMutationPayloadBytes = 4096
maxSCUMMutationReadbackBytes = 64 * 1024
maxSCUMParsedLogEvents = 1024
maxSCUMParsedLogPayloadBytes = 64 * 1024
maxSCUMParsedLogLineBytes = 64 * 1024
maxSCUMParsedLogResultBytes = 1024 * 1024
)
var scumHashPattern = regexp.MustCompile(`^sha256:[a-fA-F0-9]{64}$|^[a-fA-F0-9]{16,128}$`)
@@ -342,6 +347,74 @@ func ValidateSCUMGuardedMutationResult(result domain.SCUMGuardedMutationResult)
return finish(violations)
}
func ValidateSCUMParsedLogBatchResult(result domain.SCUMParsedLogBatchResult) error {
var violations []string
violations = appendRequired(violations, "requestId", result.RequestID)
violations = appendRequired(violations, "jobId", result.JobID)
violations = append(violations, validateSCUMBindingIdentity("binding", result.Binding)...)
if !validSCUMTerminalResultStatus(result.Status) {
violations = append(violations, "status is invalid")
}
for _, item := range []struct{ name, value string }{{"sourceKey", result.SourceKey}, {"streamKey", result.StreamKey}, {"parserKey", result.ParserKey}, {"parserVersion", result.ParserVersion}, {"adapterVersion", result.AdapterVersion}} {
violations = append(violations, validateSCUMTemplateKey(item.name, item.value)...)
if containsSCUMProtectedMaterial(item.value) || containsSCUMNetworkMaterial(item.value) {
violations = append(violations, item.name+" contains protected material")
}
}
for _, item := range []struct{ name, value string }{{"assetDigest", result.AssetDigest}, {"parserDigest", result.ParserDigest}, {"resultDigest", result.ResultDigest}} {
if !validSCUMDigest(item.value) {
violations = append(violations, item.name+" must be sha256 digest")
}
}
violations = append(violations, validateSCUMParsedLogCursor("firstCursor", result.FirstCursor)...)
violations = append(violations, validateSCUMParsedLogCursor("lastCursor", result.LastCursor)...)
if !validSCUMLogTailState(result.TailState) {
violations = append(violations, "tailState is invalid")
}
if result.EventCount != len(result.Events) {
violations = append(violations, "eventCount must match returned events")
}
violations = append(violations, validateSCUMParsedLogBatchBounds("limits", result.Limits)...)
if len(result.Events) > result.Limits.MaxEvents && result.Limits.MaxEvents > 0 {
violations = append(violations, "events exceeds declared limit")
}
if len(result.SafeSummary) > 320 || containsSCUMProtectedMaterial(result.SafeSummary) || containsSCUMNetworkMaterial(result.SafeSummary) || containsSCUMRawXML(result.SafeSummary) {
violations = append(violations, "safeSummary is unsafe")
}
violations = append(violations, validateSCUMSafeError("safeError", result.SafeError)...)
if containsSCUMNetworkMaterial(result.SafeError.Message) || containsSCUMRawXML(result.SafeError.Message) {
violations = append(violations, "safeError.message is unsafe")
}
seenLogical := map[string]struct{}{}
seenTransport := map[string]struct{}{}
for i, event := range result.Events {
field := fmt.Sprintf("events[%d]", i)
violations = append(violations, validateSCUMParsedLogEvent(field, event, result)...)
if event.LogicalEventDigest != "" {
if _, exists := seenLogical[event.LogicalEventDigest]; exists {
violations = append(violations, field+".logicalEventDigest is duplicated")
}
seenLogical[event.LogicalEventDigest] = struct{}{}
}
transport := fmt.Sprintf("%s|%s|%d", event.Cursor.SourceIdentityDigest, event.Cursor.StreamGeneration, event.Cursor.Sequence)
if _, exists := seenTransport[transport]; exists {
violations = append(violations, field+".cursor is duplicated")
}
seenTransport[transport] = struct{}{}
}
if result.EventCount > 0 && result.LastCursor.Sequence < result.FirstCursor.Sequence {
violations = append(violations, "lastCursor.sequence must not be before firstCursor.sequence")
}
if result.Status == domain.SCUMTerminalResultSucceeded {
if result.SafeError.Code != "" && result.SafeError.Code != domain.SCUMSafeErrorNone {
violations = append(violations, "succeeded result must not carry an error code")
}
} else if result.SafeError.Code == "" || result.SafeError.Code == domain.SCUMSafeErrorNone {
violations = append(violations, "non-succeeded result requires a safe error code")
}
return finish(violations)
}
func scumGuardedMutationRequestDigests(request domain.SCUMGuardedMutationRequest) []struct{ name, value string } {
return []struct{ name, value string }{{"assetDigest", request.AssetDigest}, {"targetIdentityDigest", request.TargetIdentityDigest}, {"expectedRowDigest", request.ExpectedRowDigest}, {"expectedValueDigest", request.ExpectedValueDigest}, {"expectedXmlDigest", request.ExpectedXMLDigest}, {"patchDigest", request.PatchDigest}, {"backupEvidenceDigest", request.BackupEvidenceDigest}, {"offlineEvidenceDigest", request.OfflineEvidenceDigest}, {"dangerConfirmationDigest", request.DangerConfirmationDigest}, {"readbackExpectationDigest", request.ReadbackExpectationDigest}}
}
@@ -567,6 +640,62 @@ func validateSCUMGuardedMutationBounds(prefix string, value domain.SCUMGuardedMu
return violations
}
func validateSCUMParsedLogBatchBounds(prefix string, value domain.SCUMParsedLogBatchBounds) []string {
var violations []string
if value.MaxEvents < 0 || value.MaxEvents > maxSCUMParsedLogEvents {
violations = append(violations, prefix+".maxEvents is out of bounds")
}
if value.MaxPayloadBytes < 1 || value.MaxPayloadBytes > maxSCUMParsedLogPayloadBytes {
violations = append(violations, prefix+".maxPayloadBytes is out of bounds")
}
if value.MaxLineBytes < 1 || value.MaxLineBytes > maxSCUMParsedLogLineBytes {
violations = append(violations, prefix+".maxLineBytes is out of bounds")
}
if value.MaxResultBytes < 1 || value.MaxResultBytes > maxSCUMParsedLogResultBytes {
violations = append(violations, prefix+".maxResultBytes is out of bounds")
}
return violations
}
func validateSCUMParsedLogCursor(prefix string, value domain.SCUMParsedLogCursor) []string {
var violations []string
if !validSCUMFingerprint(value.SourceIdentityDigest) || containsSCUMProtectedMaterial(value.SourceIdentityDigest) || containsSCUMNetworkMaterial(value.SourceIdentityDigest) {
violations = append(violations, prefix+".sourceIdentityDigest must be a redacted fingerprint")
}
if !validSCUMFingerprint(value.StreamGeneration) || containsSCUMProtectedMaterial(value.StreamGeneration) || containsSCUMNetworkMaterial(value.StreamGeneration) {
violations = append(violations, prefix+".streamGeneration must be a redacted fingerprint")
}
return violations
}
func validateSCUMParsedLogEvent(prefix string, event domain.SCUMParsedLogEvent, result domain.SCUMParsedLogBatchResult) []string {
var violations []string
violations = append(violations, validateSCUMTemplateKey(prefix+".eventType", event.EventType)...)
violations = append(violations, validateSCUMParsedLogCursor(prefix+".cursor", event.Cursor)...)
for _, item := range []struct{ name, value string }{{"logicalEventDigest", event.LogicalEventDigest}, {"eventDigest", event.EventDigest}, {"payloadDigest", event.PayloadDigest}} {
if !validSCUMDigest(item.value) {
violations = append(violations, prefix+"."+item.name+" must be sha256 digest")
}
}
if result.EventCount > 0 && event.Cursor.Sequence < result.FirstCursor.Sequence || result.EventCount > 0 && event.Cursor.Sequence > result.LastCursor.Sequence {
violations = append(violations, prefix+".cursor.sequence is outside batch range")
}
if event.Cursor.SourceIdentityDigest != result.FirstCursor.SourceIdentityDigest || event.Cursor.StreamGeneration != result.FirstCursor.StreamGeneration {
violations = append(violations, prefix+".cursor must match batch source identity and generation")
}
violations = append(violations, validateSCUMValueMap(prefix+".payload", event.Payload, 64)...)
violations = append(violations, validateSCUMJSONSize(prefix+".payload", event.Payload, result.Limits.MaxPayloadBytes)...)
for key, value := range event.Payload {
if strings.Contains(strings.ToLower(key), "raw") || containsSCUMProtectedMaterial(key) || containsSCUMNetworkMaterial(key) {
violations = append(violations, prefix+".payload key is unsafe")
}
if text, ok := value.(string); ok && (containsSCUMProtectedMaterial(text) || containsSCUMNetworkMaterial(text) || containsSCUMRawXML(text)) {
violations = append(violations, prefix+".payload value is unsafe")
}
}
return violations
}
func validateSCUMTemplateKey(prefix, value string) []string {
var violations []string
violations = appendRequired(violations, prefix, value)
@@ -765,6 +894,15 @@ func validSCUMMutationReadbackStatus(value domain.SCUMMutationReadbackStatus) bo
}
}
func validSCUMLogTailState(value domain.SCUMLogTailState) bool {
switch value {
case domain.SCUMLogTailAdvanced, domain.SCUMLogTailRotated, domain.SCUMLogTailTruncated, domain.SCUMLogTailRestarted, domain.SCUMLogTailPartial, domain.SCUMLogTailReplayed:
return true
default:
return false
}
}
func validSCUMCapabilityEvidenceStatus(value domain.SCUMCapabilityEvidenceStatus) bool {
switch value {
case domain.SCUMCapabilityEvidenceMissing, domain.SCUMCapabilityEvidenceCompatible, domain.SCUMCapabilityEvidenceIncompatible, domain.SCUMCapabilityEvidenceFailed:
@@ -821,3 +959,23 @@ func containsSCUMProtectedMaterial(value string) bool {
}
return false
}
func containsSCUMNetworkMaterial(value string) bool {
trimmed := strings.TrimSpace(value)
lowered := strings.ToLower(trimmed)
if trimmed == "" {
return false
}
if strings.Contains(lowered, "ip=") || strings.Contains(lowered, "addr=") || strings.Contains(lowered, "endpoint=") || strings.Contains(lowered, "port=") {
return true
}
if regexp.MustCompile(`\b(?:\d{1,3}\.){3}\d{1,3}\b`).MatchString(trimmed) {
return true
}
for _, token := range regexp.MustCompile(`[\s,;()\[\]{}'"]+`).Split(trimmed, -1) {
if strings.Contains(token, ":") && net.ParseIP(strings.Trim(token, "<>")) != nil {
return true
}
}
return false
}