feat(scum): gate live data on schema evidence

This commit is contained in:
npc0-hue
2026-08-11 13:28:36 +08:00
parent e5d26e5420
commit a58da47c60
18 changed files with 1265 additions and 3 deletions
+309
View File
@@ -0,0 +1,309 @@
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
}