Freeze SCUM typed RCON run contract
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
@@ -20,6 +21,9 @@ const (
|
||||
maxSCUMTemplateRows = 1000
|
||||
maxSCUMTemplateBusyTimeoutMS = 1000
|
||||
maxSCUMTemplateValueBytes = 4096
|
||||
maxSCUMRCONPayloadBytes = 4096
|
||||
maxSCUMRCONResponseBytes = 64 * 1024
|
||||
maxSCUMRCONConfirmRecords = 128
|
||||
)
|
||||
|
||||
var scumHashPattern = regexp.MustCompile(`^sha256:[a-fA-F0-9]{64}$|^[a-fA-F0-9]{16,128}$`)
|
||||
@@ -66,6 +70,42 @@ func ValidateSCUMSQLiteTemplateRequest(request domain.SCUMSQLiteTemplateRequest)
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func ValidateSCUMTypedRCONTemplateRequest(request domain.SCUMTypedRCONTemplateRequest) error {
|
||||
var violations []string
|
||||
violations = appendRequired(violations, "requestId", request.RequestID)
|
||||
violations = appendRequired(violations, "jobId", request.JobID)
|
||||
violations = append(violations, validateSCUMBindingIdentity("binding", request.Binding)...)
|
||||
if !validSCUMRCONWriteCapability(request.Capability) {
|
||||
violations = append(violations, "capability must be a typed RCON write capability")
|
||||
}
|
||||
violations = append(violations, validateSCUMTemplateKey("transportKey", request.TransportKey)...)
|
||||
violations = append(violations, validateSCUMTemplateKey("targetKey", request.TargetKey)...)
|
||||
violations = append(violations, validateSCUMTemplateKey("templateKey", request.TemplateKey)...)
|
||||
violations = appendRequired(violations, "adapterVersion", request.AdapterVersion)
|
||||
if request.AdapterVersion != "" && containsSCUMProtectedMaterial(request.AdapterVersion) {
|
||||
violations = append(violations, "adapterVersion contains protected material")
|
||||
}
|
||||
if request.AdapterVersion != "" && request.Binding.AdapterVersion != "" && request.AdapterVersion != request.Binding.AdapterVersion {
|
||||
violations = append(violations, "adapterVersion must match binding.adapterVersion")
|
||||
}
|
||||
if request.RequiredSchemaFingerprint != "" && !validSCUMFingerprint(request.RequiredSchemaFingerprint) {
|
||||
violations = append(violations, "requiredSchemaFingerprint must be a digest/fingerprint")
|
||||
}
|
||||
for _, item := range []struct{ name, value string }{{"assetDigest", request.AssetDigest}, {"payloadDigest", request.PayloadDigest}, {"confirmationDigest", request.ConfirmationDigest}, {"targetIdentityDigest", request.TargetIdentityDigest}} {
|
||||
if !validSCUMDigest(item.value) {
|
||||
violations = append(violations, item.name+" must be sha256 digest")
|
||||
}
|
||||
}
|
||||
violations = append(violations, validateSCUMTemplateKey("idempotencyKey", request.IdempotencyKey)...)
|
||||
if strings.TrimSpace(request.ReviewReason) == "" || len(request.ReviewReason) > 320 || containsSCUMProtectedMaterial(request.ReviewReason) {
|
||||
violations = append(violations, "reviewReason is unsafe")
|
||||
}
|
||||
violations = append(violations, validateSCUMTypedRCONTemplateBounds("bounds", request.Bounds)...)
|
||||
violations = append(violations, validateSCUMValueMap("payload", request.Payload, 64)...)
|
||||
violations = append(violations, validateSCUMJSONSize("payload", request.Payload, request.Bounds.MaxPayloadBytes)...)
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func ValidateSCUMSchemaProbeResult(result domain.SCUMSchemaProbeResult) error {
|
||||
var violations []string
|
||||
violations = appendRequired(violations, "requestId", result.RequestID)
|
||||
@@ -154,6 +194,62 @@ func ValidateSCUMSQLiteTemplateResult(result domain.SCUMSQLiteTemplateResult) er
|
||||
return finish(violations)
|
||||
}
|
||||
|
||||
func ValidateSCUMTypedRCONTemplateResult(result domain.SCUMTypedRCONTemplateResult) 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")
|
||||
}
|
||||
if !validSCUMRCONWriteCapability(result.Capability) {
|
||||
violations = append(violations, "capability must be a typed RCON write capability")
|
||||
}
|
||||
violations = append(violations, validateSCUMTemplateKey("transportKey", result.TransportKey)...)
|
||||
violations = append(violations, validateSCUMTemplateKey("targetKey", result.TargetKey)...)
|
||||
violations = append(violations, validateSCUMTemplateKey("templateKey", result.TemplateKey)...)
|
||||
violations = appendRequired(violations, "adapterVersion", result.AdapterVersion)
|
||||
if result.AdapterVersion != "" && containsSCUMProtectedMaterial(result.AdapterVersion) {
|
||||
violations = append(violations, "adapterVersion contains protected material")
|
||||
}
|
||||
if result.AdapterVersion != "" && result.Binding.AdapterVersion != "" && result.AdapterVersion != result.Binding.AdapterVersion {
|
||||
violations = append(violations, "adapterVersion must match binding.adapterVersion")
|
||||
}
|
||||
if result.SchemaFingerprint != "" && !validSCUMFingerprint(result.SchemaFingerprint) {
|
||||
violations = append(violations, "schemaFingerprint must be a digest/fingerprint")
|
||||
}
|
||||
for _, item := range []struct{ name, value string }{{"assetDigest", result.AssetDigest}, {"payloadDigest", result.PayloadDigest}, {"confirmationDigest", result.ConfirmationDigest}, {"targetIdentityDigest", result.TargetIdentityDigest}, {"resultDigest", result.ResultDigest}} {
|
||||
if !validSCUMDigest(item.value) {
|
||||
violations = append(violations, item.name+" must be sha256 digest")
|
||||
}
|
||||
}
|
||||
if result.ResponseDigest != "" && !validSCUMDigest(result.ResponseDigest) {
|
||||
violations = append(violations, "responseDigest must be sha256 digest")
|
||||
}
|
||||
if result.ConfirmationDigestID != "" && !validSCUMDigest(result.ConfirmationDigestID) {
|
||||
violations = append(violations, "confirmationDigestId must be sha256 digest")
|
||||
}
|
||||
if !validSCUMRCONConfirmationStatus(result.ConfirmationStatus) {
|
||||
violations = append(violations, "confirmationStatus is invalid")
|
||||
}
|
||||
if len(result.SafeSummary) > 320 || containsSCUMProtectedMaterial(result.SafeSummary) {
|
||||
violations = append(violations, "safeSummary is unsafe")
|
||||
}
|
||||
violations = append(violations, validateSCUMSafeError("safeError", result.SafeError)...)
|
||||
violations = append(violations, validateSCUMTypedRCONTemplateBounds("limits", result.Limits)...)
|
||||
if result.Status == domain.SCUMTerminalResultSucceeded {
|
||||
if result.ConfirmationStatus != domain.SCUMRCONConfirmationConfirmed || result.ResponseDigest == "" || result.ConfirmationDigestID == "" {
|
||||
violations = append(violations, "succeeded result requires confirmed response and confirmation digests")
|
||||
}
|
||||
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 ValidateSCUMCapabilityEvidence(evidence domain.SCUMCapabilityEvidence) error {
|
||||
var violations []string
|
||||
if !validSCUMDataCapability(evidence.Capability) {
|
||||
@@ -328,10 +424,27 @@ func validateSCUMSQLiteTemplateBounds(prefix string, value domain.SCUMSQLiteTemp
|
||||
return violations
|
||||
}
|
||||
|
||||
func validateSCUMTypedRCONTemplateBounds(prefix string, value domain.SCUMTypedRCONTemplateBounds) []string {
|
||||
var violations []string
|
||||
if value.MaxPayloadBytes < 1 || value.MaxPayloadBytes > maxSCUMRCONPayloadBytes {
|
||||
violations = append(violations, prefix+".maxPayloadBytes is out of bounds")
|
||||
}
|
||||
if value.TimeoutMS < 1 || value.TimeoutMS > maxSCUMProbeTimeoutMS {
|
||||
violations = append(violations, prefix+".timeoutMs is out of bounds")
|
||||
}
|
||||
if value.MaxResponseBytes < 1 || value.MaxResponseBytes > maxSCUMRCONResponseBytes {
|
||||
violations = append(violations, prefix+".maxResponseBytes is out of bounds")
|
||||
}
|
||||
if value.MaxConfirmRecords < 1 || value.MaxConfirmRecords > maxSCUMRCONConfirmRecords {
|
||||
violations = append(violations, prefix+".maxConfirmRecords is out of bounds")
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
func validateSCUMTemplateKey(prefix, value string) []string {
|
||||
var violations []string
|
||||
violations = appendRequired(violations, prefix, value)
|
||||
if value != "" && (!scumTemplateKeyPattern.MatchString(value) || containsSCUMProtectedMaterial(value) || strings.Contains(value, "..")) {
|
||||
if value != "" && (!scumTemplateKeyPattern.MatchString(value) || strings.Contains(value, "..")) {
|
||||
violations = append(violations, prefix+" is unsafe")
|
||||
}
|
||||
return violations
|
||||
@@ -343,7 +456,8 @@ func validateSCUMValueMap(prefix string, values map[string]any, maxItems int) []
|
||||
violations = append(violations, prefix+" exceeds declared limit")
|
||||
}
|
||||
for key, value := range values {
|
||||
if !scumTemplateKeyPattern.MatchString(key) || containsSCUMProtectedMaterial(key) || strings.Contains(key, "..") {
|
||||
loweredKey := strings.ToLower(key)
|
||||
if !scumTemplateKeyPattern.MatchString(key) || containsSCUMProtectedMaterial(key) || strings.Contains(key, "..") || strings.Contains(loweredKey, "command") || strings.Contains(loweredKey, "rcon") {
|
||||
violations = append(violations, prefix+" key is unsafe")
|
||||
}
|
||||
field := prefix + ".value"
|
||||
@@ -370,6 +484,20 @@ func validateSCUMValue(prefix string, value any) []string {
|
||||
return violations
|
||||
}
|
||||
|
||||
func validateSCUMJSONSize(prefix string, value any, maxBytes int) []string {
|
||||
if maxBytes <= 0 {
|
||||
return []string{prefix + " max byte limit is required"}
|
||||
}
|
||||
payload, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return []string{prefix + " must be JSON serializable"}
|
||||
}
|
||||
if len(payload) > maxBytes {
|
||||
return []string{prefix + " exceeds declared byte limit"}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateSCUMSchemaObjectEvidence(prefix string, value domain.SCUMSchemaObjectEvidence) []string {
|
||||
var violations []string
|
||||
if !validSCUMFingerprint(value.ObjectHash) {
|
||||
@@ -449,6 +577,15 @@ func validSCUMReadCapability(value domain.SCUMDataCapability) bool {
|
||||
}
|
||||
}
|
||||
|
||||
func validSCUMRCONWriteCapability(value domain.SCUMDataCapability) bool {
|
||||
switch value {
|
||||
case domain.SCUMDataCapabilityEconomyCommand, domain.SCUMDataCapabilityGiftCommand:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func validSCUMTerminalResultStatus(value domain.SCUMTerminalResultStatus) bool {
|
||||
switch value {
|
||||
case domain.SCUMTerminalResultSucceeded, domain.SCUMTerminalResultFailed, domain.SCUMTerminalResultCancelled:
|
||||
@@ -458,6 +595,15 @@ func validSCUMTerminalResultStatus(value domain.SCUMTerminalResultStatus) bool {
|
||||
}
|
||||
}
|
||||
|
||||
func validSCUMRCONConfirmationStatus(value domain.SCUMRCONConfirmationStatus) bool {
|
||||
switch value {
|
||||
case domain.SCUMRCONConfirmationConfirmed, domain.SCUMRCONConfirmationFailed, domain.SCUMRCONConfirmationUnknown:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func validSCUMCapabilityEvidenceStatus(value domain.SCUMCapabilityEvidenceStatus) bool {
|
||||
switch value {
|
||||
case domain.SCUMCapabilityEvidenceMissing, domain.SCUMCapabilityEvidenceCompatible, domain.SCUMCapabilityEvidenceIncompatible, domain.SCUMCapabilityEvidenceFailed:
|
||||
@@ -505,7 +651,7 @@ func containsSCUMProtectedMaterial(value string) bool {
|
||||
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) {
|
||||
if strings.HasPrefix(trimmed, "/") || strings.HasPrefix(trimmed, "\\\\") || regexp.MustCompile(`[A-Za-z]:[\\/]`).MatchString(trimmed) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
||||
Reference in New Issue
Block a user