package validator import ( "fmt" "regexp" "strings" "browser.local/platform/domain" ) const ( maxSCUMProbeObjects = 512 maxSCUMProbeColumnsPerObject = 256 maxSCUMProbeIndexesPerObject = 128 maxSCUMProbeForeignKeys = 128 maxSCUMProbeSamples = 3 maxSCUMProbeTimeoutMS = 10000 maxSCUMProbeResultBytes = 1024 * 1024 ) var scumHashPattern = regexp.MustCompile(`^sha256:[a-fA-F0-9]{64}$|^[a-fA-F0-9]{16,128}$`) func ValidateSCUMSchemaProbeRequest(request domain.SCUMSchemaProbeRequest) error { var violations []string violations = appendRequired(violations, "requestId", request.RequestID) violations = appendRequired(violations, "jobId", request.JobID) violations = append(violations, validateSCUMBindingIdentity("binding", request.Binding)...) violations = append(violations, validateSCUMSchemaProbeBounds("bounds", request.Bounds)...) return finish(violations) } func ValidateSCUMSchemaProbeResult(result domain.SCUMSchemaProbeResult) error { var violations []string violations = appendRequired(violations, "requestId", result.RequestID) violations = appendRequired(violations, "jobId", result.JobID) violations = append(violations, validateSCUMBindingIdentity("binding", result.Binding)...) if !validSCUMCapabilityEvidenceStatus(result.Status) { violations = append(violations, "status is invalid") } if result.SchemaFingerprint != "" && !validSCUMFingerprint(result.SchemaFingerprint) { violations = append(violations, "schemaFingerprint must be a digest/fingerprint") } if result.ResultDigest != "" && !validSCUMFingerprint(result.ResultDigest) { violations = append(violations, "resultDigest must be a digest/fingerprint") } violations = append(violations, validateSCUMSafeError("safeError", result.SafeError)...) violations = append(violations, validateSCUMSchemaProbeBounds("limits", result.Limits)...) if len(result.Objects) > result.Limits.MaxObjects && result.Limits.MaxObjects > 0 { violations = append(violations, "objects exceeds declared limit") } for i, object := range result.Objects { field := fmt.Sprintf("objects[%d]", i) violations = append(violations, validateSCUMSchemaObjectEvidence(field, object)...) } return finish(violations) } func ValidateSCUMCapabilityEvidence(evidence domain.SCUMCapabilityEvidence) error { var violations []string if !validSCUMDataCapability(evidence.Capability) { violations = append(violations, "capability is invalid") } if !validSCUMCapabilityEvidenceStatus(evidence.Status) { violations = append(violations, "status is invalid") } violations = append(violations, validateSCUMBindingIdentity("binding", evidence.Binding)...) violations = appendRequired(violations, "adapterVersion", evidence.AdapterVersion) if evidence.SchemaFingerprint != "" && !validSCUMFingerprint(evidence.SchemaFingerprint) { violations = append(violations, "schemaFingerprint must be a digest/fingerprint") } if evidence.ProbeResultDigest != "" && !validSCUMFingerprint(evidence.ProbeResultDigest) { violations = append(violations, "probeResultDigest must be a digest/fingerprint") } for i, digest := range evidence.AssetDigests { if !validSCUMDigest(digest) { violations = append(violations, fmt.Sprintf("assetDigests[%d] must be sha256 digest", i)) } } violations = append(violations, validateSCUMSafeError("safeError", evidence.SafeError)...) return finish(violations) } func validateSCUMLiveDataManifest(prefix string, value domain.SCUMLiveDataManifest, capabilities []string, remoteAccess domain.GamePluginRemoteAccess, runtimeProfiles domain.GamePluginRuntimeProfiles) []string { if value.SchemaVersion == "" && value.Probe.Capability == "" && len(value.CapabilityGates) == 0 { return nil } var violations []string if value.SchemaVersion != "1" { violations = append(violations, prefix+".schemaVersion must be 1") } if value.Probe.Capability != domain.JobCapabilityRemoteRunDBSQLiteProbe { violations = append(violations, prefix+".probe.capability must be "+domain.JobCapabilityRemoteRunDBSQLiteProbe) } if !containsString(capabilities, domain.JobCapabilityRemoteRunDBSQLiteProbe) || !containsString(remoteAccess.RunCapabilities, domain.JobCapabilityRemoteRunDBSQLiteProbe) { violations = append(violations, prefix+".probe requires declared remote.run.db.sqlite.probe capability") } probeTransportFound := false for _, transport := range runtimeProfiles.TransportProfiles { if transport.Key != value.Probe.TargetKey && transport.TargetKey != value.Probe.TargetKey { continue } probeTransportFound = true if transport.Kind != "sqlite" || !containsString(transport.Capabilities, domain.JobCapabilityRemoteRunDBSQLiteProbe) { violations = append(violations, prefix+".probe.targetKey must reference sqlite transport with remote.run.db.sqlite.probe") } } if !probeTransportFound { violations = append(violations, prefix+".probe.targetKey must reference a declared transport") } violations = append(violations, validateSCUMSchemaProbeBounds(prefix+".probe.bounds", value.Probe.Bounds)...) seen := map[domain.SCUMDataCapability]struct{}{} for i, gate := range value.CapabilityGates { field := fmt.Sprintf("%s.capabilityGates[%d]", prefix, i) if !validSCUMDataCapability(gate.Capability) { violations = append(violations, field+".capability is invalid") } if _, exists := seen[gate.Capability]; exists { violations = append(violations, field+".capability is duplicated") } seen[gate.Capability] = struct{}{} if gate.Gate != domain.SCUMCapabilityGateDisabled && gate.Gate != domain.SCUMCapabilityGateEnabled { violations = append(violations, field+".gate is invalid") } violations = appendRequired(violations, field+".adapterVersion", gate.AdapterVersion) if !validSCUMCapabilityEvidenceStatus(gate.EvidenceStatus) { violations = append(violations, field+".evidenceStatus is invalid") } if containsSCUMProtectedMaterial(gate.SafeReason) || len(gate.SafeReason) > 240 || strings.TrimSpace(gate.SafeReason) == "" { violations = append(violations, field+".safeReason is unsafe") } if gate.Gate == domain.SCUMCapabilityGateEnabled { if gate.EvidenceStatus != domain.SCUMCapabilityEvidenceCompatible { violations = append(violations, field+".evidenceStatus must be compatible when enabled") } if !validSCUMFingerprint(gate.RequiredSchemaFingerprint) { violations = append(violations, field+".requiredSchemaFingerprint is required when enabled") } if len(gate.RequiredAssetDigests) == 0 { violations = append(violations, field+".requiredAssetDigests is required when enabled") } } if gate.Gate == domain.SCUMCapabilityGateDisabled && gate.EvidenceStatus == domain.SCUMCapabilityEvidenceCompatible { violations = append(violations, field+".evidenceStatus must not claim compatibility while disabled") } for digestIndex, digest := range gate.RequiredAssetDigests { if !validSCUMDigest(digest) { violations = append(violations, fmt.Sprintf("%s.requiredAssetDigests[%d] must be sha256 digest", field, digestIndex)) } } } return violations } func validateSCUMBindingIdentity(prefix string, value domain.SCUMBindingIdentity) []string { var violations []string for _, item := range []struct{ name, value string }{{"serverInstanceId", value.ServerInstanceID}, {"runBindingId", value.RunBindingID}, {"runEndpointId", value.RunEndpointID}, {"pluginId", value.PluginID}, {"pluginVersion", value.PluginVersion}, {"adapterVersion", value.AdapterVersion}, {"databaseIdentity", value.DatabaseIdentity}} { violations = appendRequired(violations, prefix+"."+item.name, item.value) if containsSCUMProtectedMaterial(item.value) { violations = append(violations, prefix+"."+item.name+" contains protected connection material") } } if value.GameVersion != "" && containsSCUMProtectedMaterial(value.GameVersion) { violations = append(violations, prefix+".gameVersion contains protected connection material") } return violations } func validateSCUMSchemaProbeBounds(prefix string, value domain.SCUMSchemaProbeBounds) []string { var violations []string if value.MaxObjects < 1 || value.MaxObjects > maxSCUMProbeObjects { violations = append(violations, prefix+".maxObjects is out of bounds") } if value.MaxColumnsPerObject < 1 || value.MaxColumnsPerObject > maxSCUMProbeColumnsPerObject { violations = append(violations, prefix+".maxColumnsPerObject is out of bounds") } if value.MaxIndexesPerObject < 0 || value.MaxIndexesPerObject > maxSCUMProbeIndexesPerObject { violations = append(violations, prefix+".maxIndexesPerObject is out of bounds") } if value.MaxForeignKeys < 0 || value.MaxForeignKeys > maxSCUMProbeForeignKeys { violations = append(violations, prefix+".maxForeignKeys is out of bounds") } if value.MaxCardinalityReads < 0 || value.MaxCardinalityReads > maxSCUMProbeObjects { violations = append(violations, prefix+".maxCardinalityReads is out of bounds") } if value.MaxSampleRows < 0 || value.MaxSampleRows > maxSCUMProbeSamples { violations = append(violations, prefix+".maxSampleRows is out of bounds") } if value.TimeoutMS < 1 || value.TimeoutMS > maxSCUMProbeTimeoutMS { violations = append(violations, prefix+".timeoutMs is out of bounds") } if value.MaxResultBytes < 1 || value.MaxResultBytes > maxSCUMProbeResultBytes { violations = append(violations, prefix+".maxResultBytes is out of bounds") } return violations } func validateSCUMSchemaObjectEvidence(prefix string, value domain.SCUMSchemaObjectEvidence) []string { var violations []string if !validSCUMFingerprint(value.ObjectHash) { violations = append(violations, prefix+".objectHash must be a digest/fingerprint") } if value.Kind != "table" && value.Kind != "view" && value.Kind != "index" && value.Kind != "trigger" { violations = append(violations, prefix+".kind is invalid") } if !validSCUMFingerprint(value.NameFingerprint) || containsSCUMProtectedMaterial(value.NameFingerprint) { violations = append(violations, prefix+".nameFingerprint must be redacted") } for i, column := range value.DeclaredColumns { field := fmt.Sprintf("%s.declaredColumns[%d]", prefix, i) if !validSCUMFingerprint(column.NameFingerprint) || containsSCUMProtectedMaterial(column.NameFingerprint) { violations = append(violations, field+".nameFingerprint must be redacted") } if containsSCUMProtectedMaterial(column.DeclaredType) { violations = append(violations, field+".declaredType contains protected material") } } for i, index := range value.Indexes { field := fmt.Sprintf("%s.indexes[%d]", prefix, i) if !validSCUMFingerprint(index.NameFingerprint) || containsSCUMProtectedMaterial(index.NameFingerprint) { violations = append(violations, field+".nameFingerprint must be redacted") } for columnIndex, hash := range index.ColumnHashes { if !validSCUMFingerprint(hash) { violations = append(violations, fmt.Sprintf("%s.columnHashes[%d] must be a digest/fingerprint", field, columnIndex)) } } } for i, fk := range value.ForeignKeys { field := fmt.Sprintf("%s.foreignKeys[%d]", prefix, i) for _, item := range []struct{ name, value string }{{"fromColumnHash", fk.FromColumnHash}, {"toObjectHash", fk.ToObjectHash}, {"toColumnHash", fk.ToColumnHash}} { if !validSCUMFingerprint(item.value) { violations = append(violations, field+"."+item.name+" must be a digest/fingerprint") } } } for i, sample := range value.SampleFingerprints { if !validSCUMFingerprint(sample) || containsSCUMProtectedMaterial(sample) { violations = append(violations, fmt.Sprintf("%s.sampleFingerprints[%d] must be a redacted fingerprint", prefix, i)) } } return violations } func validateSCUMSafeError(prefix string, value domain.SCUMSafeError) []string { var violations []string if !validSCUMSafeErrorCode(value.Code) { violations = append(violations, prefix+".code is invalid") } if containsSCUMProtectedMaterial(value.Message) { violations = append(violations, prefix+".message contains protected material") } if len(value.Message) > 320 { violations = append(violations, prefix+".message is too long") } return violations } func validSCUMDataCapability(value domain.SCUMDataCapability) bool { switch value { case domain.SCUMDataCapabilitySchemaProbe, domain.SCUMDataCapabilityPlayerRead, domain.SCUMDataCapabilityPlayerDetailRead, domain.SCUMDataCapabilitySquadRead, domain.SCUMDataCapabilitySquadMemberRead, domain.SCUMDataCapabilityVehicleRead, domain.SCUMDataCapabilityFlagRead, domain.SCUMDataCapabilityPositionRead, domain.SCUMDataCapabilityProfileXMLWrite, domain.SCUMDataCapabilityEconomyCommand, domain.SCUMDataCapabilityGiftCommand: return true default: return false } } func validSCUMCapabilityEvidenceStatus(value domain.SCUMCapabilityEvidenceStatus) bool { switch value { case domain.SCUMCapabilityEvidenceMissing, domain.SCUMCapabilityEvidenceCompatible, domain.SCUMCapabilityEvidenceIncompatible, domain.SCUMCapabilityEvidenceFailed: return true default: return false } } func validSCUMSafeErrorCode(value domain.SCUMSafeErrorCode) bool { switch value { case "", domain.SCUMSafeErrorNone, domain.SCUMSafeErrorProbeExecutorAbsent, domain.SCUMSafeErrorProbeMissing, domain.SCUMSafeErrorProbeFailed, domain.SCUMSafeErrorSchemaIncompatible, domain.SCUMSafeErrorBindingMismatch, domain.SCUMSafeErrorAdapterMismatch, domain.SCUMSafeErrorFingerprintMismatch, domain.SCUMSafeErrorDigestMismatch, domain.SCUMSafeErrorEvidenceExpired, domain.SCUMSafeErrorInvalidProbePayload: return true default: return false } } func validSCUMFingerprint(value string) bool { return scumHashPattern.MatchString(value) } func validSCUMDigest(value string) bool { return regexp.MustCompile(`^sha256:[a-fA-F0-9]{64}$`).MatchString(value) } func containsSCUMProtectedMaterial(value string) bool { trimmed := strings.TrimSpace(value) lowered := strings.ToLower(trimmed) if trimmed == "" { return false } if strings.Contains(lowered, "select ") || strings.Contains(lowered, "insert into") || strings.Contains(lowered, "update ") || strings.Contains(lowered, "delete from") || strings.Contains(lowered, "pragma ") || strings.Contains(lowered, "attach database") { return true } if strings.Contains(lowered, "dsn") || strings.Contains(lowered, "password") || strings.Contains(lowered, "credential") || strings.Contains(lowered, "token") || strings.Contains(lowered, "socket") || strings.Contains(lowered, "rcon") { return true } if strings.HasPrefix(lowered, "sqlite://") || strings.HasPrefix(lowered, "mysql://") || strings.HasPrefix(lowered, "file://") || strings.HasPrefix(lowered, "tcp://") || strings.HasPrefix(lowered, "unix://") { return true } if strings.HasPrefix(trimmed, "/") || strings.HasPrefix(trimmed, "\\\\") || regexp.MustCompile(`^[A-Za-z]:[\\/]`).MatchString(trimmed) { return true } return false }