feat(scum): gate live data on schema evidence
This commit is contained in:
@@ -157,6 +157,7 @@ func ValidateGamePlugin(plugin domain.GamePlugin) error {
|
||||
violations = append(violations, validateRuntimeProfileCapabilityDeclarations(plugin.RuntimeProfiles, plugin.RequiredRunCapabilities)...)
|
||||
violations = append(violations, validateRuntimeLogEventPermissionDeclarations("runtimeProfiles.logEvents", plugin.RuntimeProfiles, plugin.DeclaredPermissions)...)
|
||||
violations = append(violations, validateGameClientBridgeManifest("gameClientBridge", plugin.GameClientBridge, plugin.DeclaredPermissions, plugin.Pages, plugin.RuntimeProfiles)...)
|
||||
violations = append(violations, validateSCUMLiveDataManifest("scumLiveData", plugin.SCUMLiveData, plugin.RequiredRunCapabilities, plugin.RemoteAccess, plugin.RuntimeProfiles)...)
|
||||
violations = append(violations, validateMapTrajectoryDeclaration("mapTrajectories", plugin.MapTrajectories)...)
|
||||
violations = append(violations, validatePluginCreateFields("createFields", plugin.CreateFields)...)
|
||||
violations = append(violations, validatePluginAssetFiles("lifecycleAssets", plugin.LifecycleAssets)...)
|
||||
@@ -229,6 +230,7 @@ func ValidateGamePluginManifestRegistration(registration domain.GamePluginManife
|
||||
violations = append(violations, validateRuntimeProfileCapabilityDeclarations(manifest.RuntimeProfiles, manifest.Capabilities)...)
|
||||
violations = append(violations, validateRuntimeLogEventPermissionDeclarations("manifest.runtimeProfiles.logEvents", manifest.RuntimeProfiles, manifest.Permissions)...)
|
||||
violations = append(violations, validateGameClientBridgeManifest("manifest.gameClientBridge", manifest.GameClientBridge, manifest.Permissions, manifest.Pages, manifest.RuntimeProfiles)...)
|
||||
violations = append(violations, validateSCUMLiveDataManifest("manifest.scumLiveData", manifest.SCUMLiveData, manifest.Capabilities, manifest.RemoteAccess, manifest.RuntimeProfiles)...)
|
||||
violations = append(violations, validateMapTrajectoryDeclaration("manifest.mapTrajectories", manifest.MapTrajectories)...)
|
||||
violations = append(violations, validatePluginAssetFileDeclarations("manifest.assetFiles", manifest.AssetFiles)...)
|
||||
violations = append(violations, validatePluginAssetFiles("assetFiles", registration.AssetFiles)...)
|
||||
@@ -2199,7 +2201,7 @@ func validPluginRunCapability(capability string) bool {
|
||||
domain.JobCapabilityRemoteRsyncRead, domain.JobCapabilityRemoteRsyncWrite,
|
||||
domain.JobCapabilityRemoteRunFilesRead, domain.JobCapabilityRemoteRunFilesWrite,
|
||||
domain.JobCapabilityRemoteRunProcessStart, domain.JobCapabilityRemoteRunProcessStop,
|
||||
domain.JobCapabilityRemoteRunDBMySQLQuery, domain.JobCapabilityRemoteRunDBSQLiteQuery,
|
||||
domain.JobCapabilityRemoteRunDBMySQLQuery, domain.JobCapabilityRemoteRunDBSQLiteProbe, domain.JobCapabilityRemoteRunDBSQLiteQuery,
|
||||
domain.JobCapabilityRemoteRunLogsTransfer, domain.JobCapabilityRemoteRunRCONCommand,
|
||||
domain.JobCapabilityRemoteRunProtectedSQL, domain.JobCapabilityRemoteRunProtectedRCON, domain.JobCapabilityRemoteRunProgram,
|
||||
domain.JobCapabilityRunSelfUpdate, domain.JobCapabilityDependenciesCheck, domain.JobCapabilityDependenciesInstall,
|
||||
@@ -2233,6 +2235,7 @@ func remoteCapabilityRequiresInputRef(capability string) bool {
|
||||
domain.JobCapabilityRemoteRsyncWrite,
|
||||
domain.JobCapabilityRemoteRunFilesWrite,
|
||||
domain.JobCapabilityRemoteRunDBMySQLQuery,
|
||||
domain.JobCapabilityRemoteRunDBSQLiteProbe,
|
||||
domain.JobCapabilityRemoteRunDBSQLiteQuery,
|
||||
domain.JobCapabilityRemoteRunRCONCommand, domain.JobCapabilityRemoteRunProtectedSQL,
|
||||
domain.JobCapabilityRemoteRunProtectedRCON, domain.JobCapabilityRemoteRunProgram:
|
||||
|
||||
@@ -295,6 +295,25 @@ func TestValidateGamePluginManifestRegistrationRejectsUnsafeCapabilitiesAndPermi
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateGamePluginManifestRegistrationValidatesSCUMLiveDataGate(t *testing.T) {
|
||||
registration := validGamePluginManifestRegistration()
|
||||
registration.Manifest.ID = "game.scum"
|
||||
registration.Manifest.Capabilities = append(registration.Manifest.Capabilities, domain.JobCapabilityRemoteRunDBSQLiteProbe)
|
||||
registration.Manifest.RemoteAccess = domain.GamePluginRemoteAccess{Methods: []string{"run"}, RunCapabilities: []string{domain.JobCapabilityRemoteRunDBSQLiteProbe}}
|
||||
registration.Manifest.RuntimeProfiles.TransportProfiles = []domain.RuntimeTransportProfile{{Key: "scum-database", Kind: "sqlite", TargetKey: "scum-database", Capabilities: []string{domain.JobCapabilityRemoteRunDBSQLiteProbe}}}
|
||||
registration.Manifest.SCUMLiveData = domain.SCUMLiveDataManifest{SchemaVersion: "1", Probe: domain.SCUMSchemaProbeDeclaration{Capability: domain.JobCapabilityRemoteRunDBSQLiteProbe, TargetKey: "scum-database", Bounds: domain.DefaultSCUMSchemaProbeBounds()}, CapabilityGates: []domain.SCUMLiveDataCapabilityGateDeclaration{{Capability: domain.SCUMDataCapabilityPlayerRead, Gate: domain.SCUMCapabilityGateDisabled, AdapterVersion: "scum-live-data-v0", EvidenceStatus: domain.SCUMCapabilityEvidenceMissing, SafeReason: "等待当前服务证据。"}}}
|
||||
|
||||
if err := ValidateGamePluginManifestRegistration(registration); err != nil {
|
||||
t.Fatalf("expected disabled live-data gate to validate, got %v", err)
|
||||
}
|
||||
|
||||
registration.Manifest.SCUMLiveData.CapabilityGates[0].Gate = domain.SCUMCapabilityGateEnabled
|
||||
err := ValidateGamePluginManifestRegistration(registration)
|
||||
if err == nil || !strings.Contains(err.Error(), "evidenceStatus must be compatible") || !strings.Contains(err.Error(), "requiredSchemaFingerprint") {
|
||||
t.Fatalf("expected enabled gate evidence violations, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateServerInstanceDependencies(t *testing.T) {
|
||||
instance := domain.ServerInstance{
|
||||
ID: "server-1",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
)
|
||||
|
||||
const scumProbeHash = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
|
||||
func TestValidateSCUMSchemaProbeRequestAllowsBoundedGenericProbe(t *testing.T) {
|
||||
request := domain.SCUMSchemaProbeRequest{RequestID: "probe-1", JobID: "job-1", Binding: validatorSCUMBinding(), Bounds: domain.DefaultSCUMSchemaProbeBounds(), RequestedAt: time.Now()}
|
||||
|
||||
if err := ValidateSCUMSchemaProbeRequest(request); err != nil {
|
||||
t.Fatalf("expected valid probe request, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSCUMSchemaProbeRequestRejectsHostPathsAndLooseBounds(t *testing.T) {
|
||||
request := domain.SCUMSchemaProbeRequest{RequestID: "probe-1", JobID: "job-1", Binding: validatorSCUMBinding(), Bounds: domain.DefaultSCUMSchemaProbeBounds()}
|
||||
request.Binding.DatabaseIdentity = `C:\SCUM\Saved\SaveFiles\SCUM.db`
|
||||
request.Bounds.MaxSampleRows = 25
|
||||
|
||||
err := ValidateSCUMSchemaProbeRequest(request)
|
||||
if err == nil || !strings.Contains(err.Error(), "protected connection material") || !strings.Contains(err.Error(), "maxSampleRows") {
|
||||
t.Fatalf("expected protected material and bound violations, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSCUMSchemaProbeResultRejectsRawSQLAndRows(t *testing.T) {
|
||||
result := domain.SCUMSchemaProbeResult{
|
||||
RequestID: "probe-1",
|
||||
JobID: "job-1",
|
||||
Binding: validatorSCUMBinding(),
|
||||
Status: domain.SCUMCapabilityEvidenceCompatible,
|
||||
SchemaFingerprint: scumProbeHash,
|
||||
ObservedAt: time.Now(),
|
||||
ResultDigest: scumProbeHash,
|
||||
Limits: domain.DefaultSCUMSchemaProbeBounds(),
|
||||
Objects: []domain.SCUMSchemaObjectEvidence{{
|
||||
ObjectHash: scumProbeHash,
|
||||
Kind: "table",
|
||||
NameFingerprint: "select * from players",
|
||||
DeclaredColumns: []domain.SCUMSchemaColumnEvidence{{NameFingerprint: scumProbeHash, DeclaredType: "TEXT", Ordinal: 1}},
|
||||
SampleFingerprints: []string{"{\"raw\":\"row\"}"},
|
||||
}},
|
||||
}
|
||||
|
||||
err := ValidateSCUMSchemaProbeResult(result)
|
||||
if err == nil || !strings.Contains(err.Error(), "nameFingerprint must be redacted") || !strings.Contains(err.Error(), "sampleFingerprints") {
|
||||
t.Fatalf("expected redaction violations, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSCUMCapabilityEvidenceRequiresSafeCurrentServiceIdentity(t *testing.T) {
|
||||
evidence := domain.SCUMCapabilityEvidence{Capability: domain.SCUMDataCapabilityPlayerRead, Status: domain.SCUMCapabilityEvidenceCompatible, Binding: validatorSCUMBinding(), AdapterVersion: "adapter-1", SchemaFingerprint: scumProbeHash, ProbeResultDigest: scumProbeHash, AssetDigests: []string{scumProbeHash}, SafeError: domain.SCUMSafeError{Code: domain.SCUMSafeErrorNone}}
|
||||
if err := ValidateSCUMCapabilityEvidence(evidence); err != nil {
|
||||
t.Fatalf("expected valid evidence, got %v", err)
|
||||
}
|
||||
|
||||
evidence.SafeError = domain.SCUMSafeError{Code: domain.SCUMSafeErrorProbeFailed, Message: "sqlite:///private/tmp/SCUM.db locked"}
|
||||
if err := ValidateSCUMCapabilityEvidence(evidence); err == nil || !strings.Contains(err.Error(), "protected material") {
|
||||
t.Fatalf("expected safe error redaction violation, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func validatorSCUMBinding() domain.SCUMBindingIdentity {
|
||||
return domain.SCUMBindingIdentity{ServerInstanceID: "server-1", RunBindingID: "binding-1", RunEndpointID: "run-1", PluginID: "game.scum", PluginVersion: "0.1.6", AdapterVersion: "adapter-1", GameVersion: "scum-1", DatabaseIdentity: "db-fingerprint-1"}
|
||||
}
|
||||
Reference in New Issue
Block a user