Freeze SCUM log-source tailing run contract
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user