package validator import ( "encoding/hex" "fmt" "net" "net/url" "regexp" "strings" "browser.local/platform/domain" ) var ( runtimeLogEventSchemaRefPattern = regexp.MustCompile(`^[A-Za-z0-9_./-]+\.json$`) runtimeLogEventTypePattern = regexp.MustCompile(`^[a-z0-9][a-z0-9._-]{0,119}$`) ) func ValidateGamePluginRuntimeProfiles(profiles domain.GamePluginRuntimeProfiles) error { profiles = domain.CopyGamePluginRuntimeProfiles(profiles) var violations []string lifecycleKeys := map[string]struct{}{} transportKeys := map[string]struct{}{} managerKeys := map[string]struct{}{} discoveryKeys := map[string]struct{}{} dependencyKeys := map[string]struct{}{} installPlanKeys := map[string]struct{}{} logSourceKeys := map[string]struct{}{} logSourceRetentions := map[string]int{} logEventKeys := map[string]struct{}{} logEventTypes := map[string]struct{}{} for i, probe := range profiles.Discovery { prefix := fmt.Sprintf("runtimeProfiles.discovery[%d]", i) violations = append(violations, validateProfileKey(prefix+".key", probe.Key)...) violations = append(violations, recordRuntimeProfileKey(discoveryKeys, prefix+".key", probe.Key)...) violations = append(violations, validateProfileKey(prefix+".targetKey", probe.TargetKey)...) if !oneOf(probe.Kind, "file.exists", "command.version", "service.status", "port.open", "steam.app", "docker.container") { violations = append(violations, prefix+".kind is invalid") } violations = append(violations, validateRuntimePlatforms(prefix+".platforms", probe.Platforms)...) violations = append(violations, validateSafeRuntimeValue(prefix+".expected", probe.Expected)...) } for i, profile := range profiles.LifecycleProfiles { prefix := fmt.Sprintf("runtimeProfiles.lifecycleProfiles[%d]", i) violations = append(violations, validateProfileKey(prefix+".key", profile.Key)...) violations = append(violations, recordRuntimeProfileKey(lifecycleKeys, prefix+".key", profile.Key)...) if !oneOf(profile.Mode, "local-process", "hosted-ftp-rcon", "ftp-only", "custom-client") { violations = append(violations, prefix+".mode is invalid") } if len(profile.Capabilities) == 0 { violations = append(violations, prefix+".capabilities must not be empty") } for j, capability := range profile.Capabilities { if !validPluginRunCapability(capability) { violations = append(violations, fmt.Sprintf("%s.capabilities[%d] is not allowed", prefix, j)) } } violations = append(violations, duplicateViolations(prefix+".capabilities", profile.Capabilities)...) violations = append(violations, validateLifecycleActionsOptional(profile.ActionRefs)...) for j, key := range profile.TransportKeys { violations = append(violations, validateProfileKey(fmt.Sprintf("%s.transportKeys[%d]", prefix, j), key)...) } violations = append(violations, duplicateViolations(prefix+".transportKeys", profile.TransportKeys)...) if profile.ClientManagerRef != "" { violations = append(violations, validateProfileKey(prefix+".clientManagerRef", profile.ClientManagerRef)...) } violations = append(violations, validateRuntimePlatforms(prefix+".platforms", profile.Platforms)...) } for i, probe := range profiles.DependencyProbes { prefix := fmt.Sprintf("runtimeProfiles.dependencyProbes[%d]", i) violations = append(violations, validateProfileKey(prefix+".key", probe.Key)...) violations = append(violations, recordRuntimeProfileKey(dependencyKeys, prefix+".key", probe.Key)...) violations = append(violations, validateProfileKey(prefix+".targetKey", probe.TargetKey)...) if !oneOf(probe.Kind, "command.version", "service.exists", "port.available", "steam.app", "java.version", "docker.available", "package.installed", "file.exists") { violations = append(violations, prefix+".kind is invalid") } violations = append(violations, validateSafeRuntimeValue(prefix+".minimumVersion", probe.MinimumVersion)...) violations = append(violations, validateRuntimePlatforms(prefix+".platforms", probe.Platforms)...) } for i, plan := range profiles.InstallPlans { prefix := fmt.Sprintf("runtimeProfiles.installPlans[%d]", i) violations = append(violations, validateProfileKey(prefix+".key", plan.Key)...) violations = append(violations, recordRuntimeProfileKey(installPlanKeys, prefix+".key", plan.Key)...) violations = appendRequired(violations, prefix+".title", plan.Title) violations = append(violations, validateSafeRuntimeValue(prefix+".title", plan.Title)...) if len(plan.Steps) == 0 { violations = append(violations, prefix+".steps must not be empty") } if len(plan.Steps) > 64 { violations = append(violations, prefix+".steps must not exceed 64") } for j, step := range plan.Steps { stepPrefix := fmt.Sprintf("%s.steps[%d]", prefix, j) if !oneOf(step.Type, "package", "verified-download", "steamcmd-app", "manual") { violations = append(violations, stepPrefix+".type is invalid") } violations = append(violations, validateProfileKey(stepPrefix+".targetKey", step.TargetKey)...) for field, value := range map[string]string{"packageManager": step.PackageManager, "packageName": step.PackageName, "version": step.Version} { violations = append(violations, validateSafeRuntimeValue(stepPrefix+"."+field, value)...) } if step.DownloadRef != "" { parsed, err := url.Parse(step.DownloadRef) host := "" if parsed != nil { host = strings.ToLower(parsed.Hostname()) } ip := net.ParseIP(host) if err != nil || parsed.Scheme != "https" || parsed.Host == "" || parsed.User != nil || parsed.Fragment != "" || host == "localhost" || strings.HasSuffix(host, ".localhost") || ip != nil && (ip.IsLoopback() || ip.IsPrivate() || ip.IsUnspecified() || ip.IsLinkLocalUnicast()) { violations = append(violations, stepPrefix+".downloadRef must be a credential-free HTTPS URL") } } if step.Checksum != "" { encoded := strings.TrimPrefix(step.Checksum, "sha256:") if !strings.HasPrefix(step.Checksum, "sha256:") || len(encoded) != 64 { violations = append(violations, stepPrefix+".checksum is invalid") } else if _, err := hex.DecodeString(encoded); err != nil { violations = append(violations, stepPrefix+".checksum is invalid") } } switch step.Type { case "package": if !oneOf(step.PackageManager, "winget", "choco", "scoop", "apt", "yum", "dnf", "pacman", "zypper", "brew") { violations = append(violations, stepPrefix+".packageManager is unsupported for package step") } if !regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9_.:+@/-]{0,119}$`).MatchString(step.PackageName) { violations = append(violations, stepPrefix+".packageName is invalid") } case "verified-download": if step.DownloadRef == "" || step.Checksum == "" { violations = append(violations, stepPrefix+" requires downloadRef and checksum") } case "steamcmd-app": if step.PackageManager != "" && step.PackageManager != "steamcmd" || !regexp.MustCompile(`^[0-9]{1,12}$`).MatchString(step.PackageName) { violations = append(violations, stepPrefix+" requires a numeric Steam app and steamcmd adapter") } case "manual": if step.DownloadRef != "" || step.Checksum != "" || step.PackageName != "" { violations = append(violations, stepPrefix+" manual step cannot contain machine execution fields") } } } violations = append(violations, validateRuntimePlatforms(prefix+".platforms", plan.Platforms)...) } for i, source := range profiles.LogSources { prefix := fmt.Sprintf("runtimeProfiles.logSources[%d]", i) violations = append(violations, validateProfileKey(prefix+".key", source.Key)...) violations = append(violations, recordRuntimeProfileKey(logSourceKeys, prefix+".key", source.Key)...) if source.Key != "" { logSourceRetentions[source.Key] = source.RetentionDays } if !oneOf(source.Kind, "process.stdout", "process.stderr", "file.tail", "ftp.poll", "sql.query", "client-manager") { violations = append(violations, prefix+".kind is invalid") } if source.TargetKey != "" { violations = append(violations, validateProfileKey(prefix+".targetKey", source.TargetKey)...) } violations = append(violations, validateProfileKey(prefix+".streamKey", source.StreamKey)...) if source.CursorKind != "" && !oneOf(source.CursorKind, "sequence", "offset", "fingerprint", "ftp-listing", "sql-cursor") { violations = append(violations, prefix+".cursorKind is invalid") } if source.RetentionDays < 0 || source.RetentionDays > 365 { violations = append(violations, prefix+".retentionDays is invalid") } } if len(profiles.LogEvents) > 128 { violations = append(violations, "runtimeProfiles.logEvents must not exceed 128") } for i, event := range profiles.LogEvents { prefix := fmt.Sprintf("runtimeProfiles.logEvents[%d]", i) violations = append(violations, validateProfileKey(prefix+".key", event.Key)...) violations = append(violations, recordRuntimeProfileKey(logEventKeys, prefix+".key", event.Key)...) if strings.TrimSpace(event.Title) == "" || len([]rune(event.Title)) > 80 { violations = append(violations, prefix+".title is invalid") } violations = append(violations, validateSafeRuntimeValue(prefix+".title", event.Title)...) violations = append(violations, validateProfileKey(prefix+".sourceKey", event.SourceKey)...) if _, exists := logSourceKeys[event.SourceKey]; !exists { violations = append(violations, prefix+".sourceKey must reference a declared runtime log source") } if !runtimeLogEventTypePattern.MatchString(event.EventType) { violations = append(violations, prefix+".eventType is invalid") } violations = append(violations, recordRuntimeProfileKey(logEventTypes, prefix+".eventType", event.EventType)...) if hasUnsafeRuntimeLogEventSemantics(event.EventType) { violations = append(violations, prefix+".eventType contains unsafe operation semantics") } if !validPluginPermission(event.Permission) { violations = append(violations, prefix+".permission is not allowed") } if len(event.SchemaRef) > 240 || !runtimeLogEventSchemaRefPattern.MatchString(event.SchemaRef) || !safeRelativeJSONRef(event.SchemaRef) { violations = append(violations, prefix+".schemaRef must be a bounded safe relative JSON reference") } if event.RetentionDays < 1 || event.RetentionDays > 365 { violations = append(violations, prefix+".retentionDays is invalid") } if sourceRetention, exists := logSourceRetentions[event.SourceKey]; exists && sourceRetention > 0 && event.RetentionDays > sourceRetention { violations = append(violations, prefix+".retentionDays must not exceed the source retention") } if !oneOf(string(event.Severity), string(domain.RuntimeLogEventSeverityInfo), string(domain.RuntimeLogEventSeverityNotice), string(domain.RuntimeLogEventSeverityWarning), string(domain.RuntimeLogEventSeverityCritical)) { violations = append(violations, prefix+".severity is invalid") } } for i, transport := range profiles.TransportProfiles { prefix := fmt.Sprintf("runtimeProfiles.transportProfiles[%d]", i) violations = append(violations, validateProfileKey(prefix+".key", transport.Key)...) violations = append(violations, recordRuntimeProfileKey(transportKeys, prefix+".key", transport.Key)...) if !oneOf(transport.Kind, "file", "ftp", "rsync", "mysql", "sqlite", "rcon") { violations = append(violations, prefix+".kind is invalid") } if transport.TargetKey != "" { violations = append(violations, validateProfileKey(prefix+".targetKey", transport.TargetKey)...) } if len(transport.Capabilities) == 0 { violations = append(violations, prefix+".capabilities must not be empty") } for j, capability := range transport.Capabilities { if !validPluginRunCapability(capability) { violations = append(violations, fmt.Sprintf("%s.capabilities[%d] is not allowed", prefix, j)) } } violations = append(violations, duplicateViolations(prefix+".capabilities", transport.Capabilities)...) } for i, manager := range profiles.ClientManagers { prefix := fmt.Sprintf("runtimeProfiles.clientManagers[%d]", i) violations = append(violations, validateProfileKey(prefix+".key", manager.Key)...) violations = append(violations, recordRuntimeProfileKey(managerKeys, prefix+".key", manager.Key)...) parsed, err := url.Parse(manager.RepositoryURL) if err != nil || parsed.Scheme != "https" || parsed.Host == "" || parsed.User != nil || parsed.RawQuery != "" || parsed.Fragment != "" || !strings.HasSuffix(parsed.Path, ".git") { violations = append(violations, prefix+".repository.url must be a credential-free HTTPS .git URL") } if !oneOf(manager.RevisionPolicy, "pinned", "branch", "tag") { violations = append(violations, prefix+".repository.revisionPolicy is invalid") } switch manager.RevisionPolicy { case "pinned": if manager.Revision == "" { violations = append(violations, prefix+".repository.revision is required for pinned policy") } case "branch": if manager.Branch == "" { violations = append(violations, prefix+".repository.branch is required for branch policy") } case "tag": if manager.Tag == "" { violations = append(violations, prefix+".repository.tag is required for tag policy") } } if !oneOf(manager.BuildSystem, "go", "npm", "cargo", "make") { violations = append(violations, prefix+".build.system is invalid") } if len(manager.SupportedTargets) == 0 { violations = append(violations, prefix+".supportedTargets must not be empty") } if len(manager.OutputArtifacts) == 0 { violations = append(violations, prefix+".outputArtifacts must not be empty") } for field, value := range map[string]string{"displayName": manager.DisplayName, "branch": manager.Branch, "tag": manager.Tag, "revision": manager.Revision, "workspaceRef": manager.WorkspaceRef, "entryRef": manager.EntryRef} { violations = append(violations, validateSafeRuntimeValue(prefix+"."+field, value)...) } targets := map[string]struct{}{} for j, target := range manager.SupportedTargets { if !validPluginSupportedOS(target.OS) || !oneOf(target.Arch, "amd64", "arm64") { violations = append(violations, fmt.Sprintf("%s.supportedTargets[%d] is invalid", prefix, j)) } targetKey := target.OS + "/" + target.Arch if _, exists := targets[targetKey]; exists { violations = append(violations, fmt.Sprintf("%s.supportedTargets[%d] is duplicated", prefix, j)) } targets[targetKey] = struct{}{} } configKeys := map[string]struct{}{} for j, config := range manager.ConfigTemplates { violations = append(violations, validateProfileKey(fmt.Sprintf("%s.configTemplates[%d].key", prefix, j), config.Key)...) violations = append(violations, recordRuntimeProfileKey(configKeys, fmt.Sprintf("%s.configTemplates[%d].key", prefix, j), config.Key)...) violations = append(violations, validateSafeRuntimeValue(prefix+".configTemplates.templateRef", config.TemplateRef)...) violations = append(violations, validateSafeRuntimeValue(prefix+".configTemplates.outputRef", config.OutputRef)...) } for j, output := range manager.OutputArtifacts { violations = append(violations, validateSafeRuntimeValue(fmt.Sprintf("%s.outputArtifacts[%d]", prefix, j), output)...) } violations = append(violations, duplicateViolations(prefix+".outputArtifacts", manager.OutputArtifacts)...) if manager.Deployment.Mode != "" { if manager.Deployment.Mode != "run-supervised" { violations = append(violations, prefix+".deployment.mode is invalid") } if !validSemanticVersion(manager.Version) { violations = append(violations, prefix+".version must be semantic when deployment is declared") } violations = append(violations, validateSafeRelativeRuntimePath(prefix+".deployment.executableRef", manager.Deployment.ExecutableRef)...) if !containsString(manager.OutputArtifacts, manager.Deployment.ExecutableRef) { violations = append(violations, prefix+".deployment.executableRef must name an output artifact") } for j, argument := range manager.Deployment.Arguments { if !regexp.MustCompile(`^[A-Za-z0-9_./:=@+-]{1,120}$`).MatchString(argument) { violations = append(violations, fmt.Sprintf("%s.deployment.arguments[%d] is invalid", prefix, j)) } violations = append(violations, validateSafeRuntimeValue(fmt.Sprintf("%s.deployment.arguments[%d]", prefix, j), argument)...) } if len(manager.Deployment.RequiredRunCapabilities) == 0 || !containsString(manager.Deployment.RequiredRunCapabilities, domain.JobCapabilityClientManagerDeploy) { violations = append(violations, prefix+".deployment.requiredRunCapabilities must include client-manager.deploy") } for j, capability := range manager.Deployment.RequiredRunCapabilities { if !oneOf(capability, domain.JobCapabilityClientManagerDeploy, domain.JobCapabilityClientManagerControl, domain.JobCapabilityClientManagerUpdate, domain.JobCapabilityClientManagerRollback, domain.JobCapabilityClientManagerUninstall) { violations = append(violations, fmt.Sprintf("%s.deployment.requiredRunCapabilities[%d] is invalid", prefix, j)) } } violations = append(violations, duplicateViolations(prefix+".deployment.requiredRunCapabilities", manager.Deployment.RequiredRunCapabilities)...) if len(manager.Lifecycle.Actions) == 0 || manager.Lifecycle.StartupTimeoutSeconds < 1 || manager.Lifecycle.StartupTimeoutSeconds > 300 || manager.Lifecycle.StopTimeoutSeconds < 1 || manager.Lifecycle.StopTimeoutSeconds > 120 { violations = append(violations, prefix+".lifecycle actions and bounded timeouts are required") } for j, action := range manager.Lifecycle.Actions { if !oneOf(action, "start", "stop", "restart", "status", "update", "rollback", "uninstall") { violations = append(violations, fmt.Sprintf("%s.lifecycle.actions[%d] is invalid", prefix, j)) } } violations = append(violations, duplicateViolations(prefix+".lifecycle.actions", manager.Lifecycle.Actions)...) if containsAny(manager.Lifecycle.Actions, []string{"start", "stop", "restart", "status"}) && !containsString(manager.Deployment.RequiredRunCapabilities, domain.JobCapabilityClientManagerControl) { violations = append(violations, prefix+".lifecycle control actions require client-manager.control") } if containsString(manager.Lifecycle.Actions, "update") && !containsString(manager.Deployment.RequiredRunCapabilities, domain.JobCapabilityClientManagerUpdate) { violations = append(violations, prefix+".lifecycle update requires client-manager.update") } if containsString(manager.Lifecycle.Actions, "rollback") && !containsString(manager.Deployment.RequiredRunCapabilities, domain.JobCapabilityClientManagerRollback) { violations = append(violations, prefix+".lifecycle rollback requires client-manager.rollback") } if containsString(manager.Lifecycle.Actions, "uninstall") && !containsString(manager.Deployment.RequiredRunCapabilities, domain.JobCapabilityClientManagerUninstall) { violations = append(violations, prefix+".lifecycle uninstall requires client-manager.uninstall") } if !oneOf(manager.Health.Mode, "component-heartbeat", "process") || manager.Health.IntervalSeconds < 5 || manager.Health.IntervalSeconds > 300 || manager.Health.DegradedAfterSeconds < manager.Health.IntervalSeconds*2 || manager.Health.OfflineAfterSeconds <= manager.Health.DegradedAfterSeconds || manager.Health.OfflineAfterSeconds > 3600 { violations = append(violations, prefix+".health mode and thresholds are invalid") } for j, capability := range manager.Health.RequiredCapabilities { if !oneOf(capability, "component.register", "component.heartbeat", "component.health", "component.control", "game-client.bridge", "logs.stream") { violations = append(violations, fmt.Sprintf("%s.health.requiredCapabilities[%d] is invalid", prefix, j)) } } if manager.Health.Mode == "component-heartbeat" && !containsAny(manager.Health.RequiredCapabilities, []string{"component.register"}) || manager.Health.Mode == "component-heartbeat" && !containsString(manager.Health.RequiredCapabilities, "component.heartbeat") || manager.Health.Mode == "component-heartbeat" && !containsString(manager.Health.RequiredCapabilities, "component.health") { violations = append(violations, prefix+".health component-heartbeat requires register, heartbeat, and health capabilities") } minimum, minimumOK := semanticVersionTuple(manager.Compatibility.MinimumVersion) maximum, maximumOK := semanticVersionTuple(manager.Compatibility.MaximumVersion) version, _ := semanticVersionTuple(manager.Version) if manager.Compatibility.MinimumVersion != "" && !minimumOK || manager.Compatibility.MaximumVersion != "" && !maximumOK || minimumOK && maximumOK && compareSemanticVersion(minimum, maximum) > 0 || minimumOK && compareSemanticVersion(version, minimum) < 0 || maximumOK && compareSemanticVersion(version, maximum) > 0 { violations = append(violations, prefix+".compatibility version bounds are invalid") } if manager.UpdatePolicy.Strategy != "manual-staged" || !manager.UpdatePolicy.RequireApproval || !manager.UpdatePolicy.RetainPrevious || manager.UpdatePolicy.HealthConfirmationSeconds < manager.Health.IntervalSeconds || manager.UpdatePolicy.HealthConfirmationSeconds > 600 { violations = append(violations, prefix+".updatePolicy must be approved, staged, health checked, and retain previous") } } } for i, profile := range profiles.LifecycleProfiles { for _, key := range profile.TransportKeys { if _, ok := transportKeys[key]; !ok { violations = append(violations, fmt.Sprintf("runtimeProfiles.lifecycleProfiles[%d].transportKeys references undeclared transport %q", i, key)) } } if profile.ClientManagerRef != "" { if _, ok := managerKeys[profile.ClientManagerRef]; !ok { violations = append(violations, fmt.Sprintf("runtimeProfiles.lifecycleProfiles[%d].clientManagerRef references undeclared client manager", i)) } } } return finish(violations) } func validateProfileKey(field, value string) []string { if strings.TrimSpace(value) == "" { return []string{field + " is required"} } if !validDistributionLogicalKey(value) { return []string{field + " is invalid"} } return nil } func validateRuntimePlatforms(field string, platforms []string) []string { var violations []string for i, platform := range platforms { if !validPluginSupportedOS(platform) { violations = append(violations, fmt.Sprintf("%s[%d] is invalid", field, i)) } } return append(violations, duplicateViolations(field, platforms)...) } func validateSafeRuntimeValue(field, value string) []string { if value == "" { return nil } lowered := strings.ToLower(value) if strings.HasPrefix(value, "/") || strings.HasPrefix(value, `\`) || strings.Contains(value, "://") || containsUnsafeRuntimeSecret(value) || looksLikeRawHostPath(value) || strings.Contains(value, "..") || strings.ContainsAny(value, "\r\n") || strings.Contains(lowered, "bash -c") || strings.Contains(lowered, "powershell -") || strings.Contains(lowered, "cmd.exe") || strings.Contains(lowered, "curl |") { return []string{field + " contains unsafe runtime content"} } return nil } func hasUnsafeRuntimeLogEventSemantics(eventType string) bool { lowered := strings.ToLower(strings.TrimSpace(eventType)) tokens := strings.FieldsFunc(lowered, func(char rune) bool { return char == '.' || char == '_' || char == '-' || char == '/' }) unsafeTokens := map[string]struct{}{ "apikey": {}, "credential": {}, "credentials": {}, "eval": {}, "exec": {}, "execute": {}, "password": {}, "powershell": {}, "script": {}, "secret": {}, "shell": {}, "socket": {}, "terminal": {}, "token": {}, } for _, token := range tokens { if _, unsafe := unsafeTokens[token]; unsafe { return true } } for index := 0; index+1 < len(tokens); index++ { pair := tokens[index] + "." + tokens[index+1] switch pair { case "absolute.path", "access.key", "api.key", "component.key", "database.query", "direct.socket", "file.path", "host.path", "private.key", "raw.path", "run.direct", "run.socket", "unix.socket": return true } } tokenSet := make(map[string]struct{}, len(tokens)) for _, token := range tokens { tokenSet[token] = struct{}{} } if _, hasSQL := tokenSet["sql"]; hasSQL { for _, token := range []string{"query", "statement", "raw"} { if _, unsafe := tokenSet[token]; unsafe { return true } } } return false } func recordRuntimeProfileKey(seen map[string]struct{}, field, key string) []string { if key == "" { return nil } if _, exists := seen[key]; exists { return []string{field + " is duplicated"} } seen[key] = struct{}{} return nil } func validateRuntimeProfileCapabilityDeclarations(profiles domain.GamePluginRuntimeProfiles, declared []string) []string { declaredSet := map[string]struct{}{} for _, capability := range declared { declaredSet[capability] = struct{}{} } var violations []string check := func(field string, capabilities []string) { for i, capability := range capabilities { if _, ok := declaredSet[capability]; !ok { violations = append(violations, fmt.Sprintf("%s[%d] must also be declared in manifest capabilities", field, i)) } } } for i, profile := range profiles.LifecycleProfiles { check(fmt.Sprintf("runtimeProfiles.lifecycleProfiles[%d].capabilities", i), profile.Capabilities) } for i, transport := range profiles.TransportProfiles { check(fmt.Sprintf("runtimeProfiles.transportProfiles[%d].capabilities", i), transport.Capabilities) } for i, manager := range profiles.ClientManagers { check(fmt.Sprintf("runtimeProfiles.clientManagers[%d].deployment.requiredRunCapabilities", i), manager.Deployment.RequiredRunCapabilities) } return violations } func validateRuntimeLogEventPermissionDeclarations(field string, profiles domain.GamePluginRuntimeProfiles, declared []string) []string { declaredSet := make(map[string]struct{}, len(declared)) for _, permission := range declared { declaredSet[permission] = struct{}{} } var violations []string for i, event := range profiles.LogEvents { if _, exists := declaredSet[event.Permission]; !exists { violations = append(violations, fmt.Sprintf("%s[%d].permission must be declared by the plugin", field, i)) } } return violations } func validateLifecycleActionsOptional(actions domain.PluginLifecycleActions) []string { var violations []string for field, value := range map[string]string{"install": actions.Install, "start": actions.Start, "stop": actions.Stop, "restart": actions.Restart, "status": actions.Status} { if value != "" && !safeRelativeJSONRef(value) { violations = append(violations, "runtime actionRefs."+field+" must be a safe relative JSON reference") } } return violations } func oneOf(value string, allowed ...string) bool { for _, candidate := range allowed { if value == candidate { return true } } return false } func validateSafeRelativeRuntimePath(field, value string) []string { if strings.TrimSpace(value) == "" || strings.HasPrefix(value, "/") || strings.HasPrefix(value, `\`) || strings.Contains(value, "..") || strings.Contains(value, "://") || strings.ContainsAny(value, "\r\n|;&`$<>") || len(value) >= 2 && value[1] == ':' || !regexp.MustCompile(`^[A-Za-z0-9_./-]{1,160}$`).MatchString(value) { return []string{field + " must be a safe relative path"} } return nil } func validSemanticVersion(value string) bool { _, ok := semanticVersionTuple(value) return ok } func semanticVersionTuple(value string) ([3]int, bool) { match := regexp.MustCompile(`^(\d+)\.(\d+)\.(\d+)(?:-[0-9A-Za-z.-]+)?$`).FindStringSubmatch(value) if match == nil { return [3]int{}, false } var result [3]int for i := 0; i < 3; i++ { if _, err := fmt.Sscanf(match[i+1], "%d", &result[i]); err != nil { return [3]int{}, false } } return result, true } func compareSemanticVersion(left, right [3]int) int { for i := 0; i < 3; i++ { if left[i] < right[i] { return -1 } if left[i] > right[i] { return 1 } } return 0 }