@opensoha/contracts 0.1.26 → 0.1.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/COMPATIBILITY.md +14 -3
  2. package/README.md +8 -9
  3. package/auth/catalog_generated.go +2 -2
  4. package/auth/permission-catalog.json +481 -1
  5. package/compat/consumer-revisions.json +1 -1
  6. package/compat/json-schema-ecosystem-0.1.0.json +1 -1
  7. package/compat/openapi-loose-boundaries-0.1.0.json +1 -1
  8. package/endpointcontrol/decode.go +64 -0
  9. package/endpointcontrol/models.go +138 -0
  10. package/endpointcontrol/models_test.go +51 -0
  11. package/endpointcontrol/protocol.schema.json +397 -0
  12. package/fixtures/json-schema/endpoint-controls/invalid/controls.json +16 -0
  13. package/fixtures/json-schema/endpoint-controls/invalid/missing-channel-decision.json +15 -0
  14. package/fixtures/json-schema/endpoint-controls/invalid/receipt-content.json +36 -0
  15. package/fixtures/json-schema/endpoint-controls/valid/controls.json +16 -0
  16. package/fixtures/json-schema/endpoint-controls/valid/receipt.json +35 -0
  17. package/fixtures/json-schema/network-runtime-protocol/invalid/eap-activate-bad-fingerprint.json +14 -0
  18. package/fixtures/json-schema/network-runtime-protocol/invalid/eap-gateway-request.json +14 -0
  19. package/fixtures/json-schema/network-runtime-protocol/invalid/eap-private-key-request.json +15 -0
  20. package/fixtures/json-schema/network-runtime-protocol/invalid/eap-profile-without-eap-credential.json +22 -0
  21. package/fixtures/json-schema/network-runtime-protocol/invalid/endpoint-dns-observation.json +25 -0
  22. package/fixtures/json-schema/network-runtime-protocol/invalid/gateway-presence.json +22 -0
  23. package/fixtures/json-schema/network-runtime-protocol/invalid/mihomo-protection-zero-version.json +45 -0
  24. package/fixtures/json-schema/network-runtime-protocol/invalid/presence-raw-key.json +23 -0
  25. package/fixtures/json-schema/network-runtime-protocol/invalid/presence-zero-sequence.json +22 -0
  26. package/fixtures/json-schema/network-runtime-protocol/invalid/workload-wide-vpn.json +17 -0
  27. package/fixtures/json-schema/network-runtime-protocol/valid/eap-certificate-activate.json +14 -0
  28. package/fixtures/json-schema/network-runtime-protocol/valid/eap-certificate-request.json +14 -0
  29. package/fixtures/json-schema/network-runtime-protocol/valid/eap-profile-enrollment.json +24 -0
  30. package/fixtures/json-schema/network-runtime-protocol/valid/endpoint-presence.json +22 -0
  31. package/fixtures/json-schema/network-runtime-protocol/valid/ipv6-configuration.json +102 -0
  32. package/fixtures/json-schema/network-runtime-protocol/valid/mihomo-protected-policy.json +45 -0
  33. package/fixtures/json-schema/network-runtime-protocol/valid/site-dns-ipv6.json +25 -0
  34. package/fixtures/json-schema/network-runtime-protocol/valid/workload-direct-ztna.json +20 -0
  35. package/fixtures/openapi/DLPShadowDecision/invalid/step-up-type.json +14 -0
  36. package/fixtures/openapi/DLPShadowDecision/valid/archive-v2.json +14 -0
  37. package/fixtures/openapi/DLPShadowDecision/valid/step-up.json +14 -0
  38. package/fixtures/openapi/DLPShadowInput/invalid/self-reported-result.json +7 -0
  39. package/fixtures/openapi/DLPShadowInput/invalid/too-many-samples.json +14 -0
  40. package/fixtures/openapi/DLPShadowInput/valid/bounded.json +7 -0
  41. package/fixtures/openapi/DockerVolumeDLPAppealInput/invalid/p4-response.json +3 -0
  42. package/fixtures/openapi/DockerVolumeDLPAppealInput/valid/p4-response.json +3 -0
  43. package/fixtures/openapi/DockerVolumeDLPAppealReviewInput/invalid/p4-response.json +5 -0
  44. package/fixtures/openapi/DockerVolumeDLPAppealReviewInput/valid/p4-response.json +5 -0
  45. package/fixtures/openapi/DockerVolumeDLPAuthorizationInput/invalid/p4-response.json +3 -0
  46. package/fixtures/openapi/DockerVolumeDLPAuthorizationInput/valid/p4-response.json +3 -0
  47. package/fixtures/openapi/DockerVolumeDLPContent/invalid/archive-unsupported-channel.json +9 -0
  48. package/fixtures/openapi/DockerVolumeDLPContent/invalid/p4.json +6 -0
  49. package/fixtures/openapi/DockerVolumeDLPContent/invalid/step-up-audit.json +9 -0
  50. package/fixtures/openapi/DockerVolumeDLPContent/invalid/unavailable-engine.json +9 -0
  51. package/fixtures/openapi/DockerVolumeDLPContent/invalid/unknown-data-level.json +12 -0
  52. package/fixtures/openapi/DockerVolumeDLPContent/valid/classified-rule.json +16 -0
  53. package/fixtures/openapi/DockerVolumeDLPContent/valid/p4.json +8 -0
  54. package/fixtures/openapi/DockerVolumeDLPContent/valid/step-up.json +9 -0
  55. package/fixtures/openapi/DockerVolumeDLPDecision/invalid/p4.json +9 -0
  56. package/fixtures/openapi/DockerVolumeDLPDecision/invalid/step-up-type.json +12 -0
  57. package/fixtures/openapi/DockerVolumeDLPDecision/valid/archive-v2.json +12 -0
  58. package/fixtures/openapi/DockerVolumeDLPDecision/valid/p4.json +11 -0
  59. package/fixtures/openapi/DockerVolumeDLPDecision/valid/step-up.json +12 -0
  60. package/fixtures/openapi/DockerVolumeDLPSaveInput/invalid/p4.json +11 -0
  61. package/fixtures/openapi/DockerVolumeDLPSaveInput/valid/p4.json +11 -0
  62. package/fixtures/openapi/DockerVolumeFileReadInput/invalid/p4-response.json +5 -0
  63. package/fixtures/openapi/DockerVolumeFileReadInput/valid/p4-response.json +4 -0
  64. package/fixtures/openapi/EndpointDeviceInput/invalid/reported-management.json +5 -0
  65. package/fixtures/openapi/EndpointDeviceInput/valid/management-state.json +5 -0
  66. package/fixtures/openapi/EndpointDeviceRegistrationInput/invalid/self-promote-management.json +5 -0
  67. package/fixtures/openapi/KubernetesClusterRegistrationRequest/invalid/metric-cluster-value-length.json +3 -0
  68. package/fixtures/openapi/KubernetesClusterRegistrationRequest/invalid/metric-cluster-value-type.json +3 -0
  69. package/fixtures/openapi/KubernetesClusterRegistrationRequest/valid/metric-cluster-alias.json +5 -0
  70. package/fixtures/openapi/KubernetesClusterRegistrationRequest/valid/metric-cluster-reset.json +3 -0
  71. package/fixtures/openapi/KubernetesHelmReleaseValuesUpdateInput/invalid/revision-overflow.json +4 -0
  72. package/fixtures/openapi/KubernetesHelmReleaseValuesUpdateInput/valid/legacy.json +3 -0
  73. package/fixtures/openapi/KubernetesHelmReleaseValuesUpdateInput/valid/observed-revision.json +4 -0
  74. package/fixtures/openapi/KubernetesJobDetail/invalid/execution-observation.json +23 -0
  75. package/fixtures/openapi/KubernetesJobDetail/valid/execution-observation.json +23 -0
  76. package/fixtures/openapi/KubernetesNode/invalid/labels.json +10 -0
  77. package/fixtures/openapi/KubernetesNode/valid/labels.json +11 -0
  78. package/fixtures/openapi/KubernetesPodListEnvelope/invalid/empty-uid.json +13 -0
  79. package/fixtures/openapi/KubernetesPodListEnvelope/invalid/invalid-container-state.json +20 -0
  80. package/fixtures/openapi/KubernetesPodListEnvelope/invalid/missing-container-ready.json +19 -0
  81. package/fixtures/openapi/KubernetesPodListEnvelope/valid/container-statuses.json +67 -0
  82. package/fixtures/openapi/KubernetesPodListEnvelope/valid/running-pod.json +1 -0
  83. package/fixtures/openapi/KubernetesPrometheusDetail/invalid/metric-cluster-value-type.json +4 -0
  84. package/fixtures/openapi/KubernetesPrometheusDetail/valid/metric-cluster-alias.json +5 -0
  85. package/fixtures/openapi/KubernetesResourceGraphEnvelope/invalid/network-edge-source-payload.json +73 -0
  86. package/fixtures/openapi/KubernetesResourceGraphEnvelope/valid/network-edge-facts.json +67 -0
  87. package/fixtures/openapi/KubernetesSecretCertificate/invalid/secret-data.json +1 -0
  88. package/fixtures/openapi/KubernetesSecretCertificate/invalid/status.json +1 -0
  89. package/fixtures/openapi/KubernetesSecretCertificate/valid/available.json +1 -0
  90. package/fixtures/openapi/KubernetesSecretCertificate/valid/missing.json +1 -0
  91. package/fixtures/openapi/ManagedContentPasteInput/invalid/boundary.json +5 -0
  92. package/fixtures/openapi/ManagedContentPasteInput/valid/boundary.json +6 -0
  93. package/fixtures/openapi/ManagedContentPolicy/invalid/self-reported-allow.json +8 -0
  94. package/fixtures/openapi/ManagedContentPolicy/invalid/step-up-audit.json +8 -0
  95. package/fixtures/openapi/ManagedContentPolicy/invalid/unknown-extractor.json +8 -0
  96. package/fixtures/openapi/ManagedContentPolicy/valid/archive-v2.json +8 -0
  97. package/fixtures/openapi/ManagedContentPolicy/valid/network-and-level.json +10 -0
  98. package/fixtures/openapi/ManagedContentPolicy/valid/step-up.json +8 -0
  99. package/fixtures/openapi/ManagedContentReviewInput/invalid/boundary.json +5 -0
  100. package/fixtures/openapi/ManagedContentReviewInput/valid/boundary.json +5 -0
  101. package/fixtures/openapi/ManagedContentSaveInput/invalid/boundary.json +14 -0
  102. package/fixtures/openapi/ManagedContentSaveInput/valid/boundary.json +16 -0
  103. package/fixtures/openapi/ManagedContentUploadInput/invalid/boundary.json +7 -0
  104. package/fixtures/openapi/ManagedContentUploadInput/valid/boundary.json +6 -0
  105. package/fixtures/openapi/NetworkAccessPolicyInput/invalid/unknown-management-state.json +35 -0
  106. package/fixtures/openapi/NetworkAccessPolicyInput/valid/managed-device.json +35 -0
  107. package/fixtures/openapi/NetworkAdmissionPreviewRequest/invalid/deviceId-characters.json +4 -0
  108. package/fixtures/openapi/NetworkAdmissionPreviewRequest/invalid/nasBindingId-characters.json +4 -0
  109. package/fixtures/openapi/NetworkAdmissionPreviewRequest/invalid/subject-override.json +1 -0
  110. package/fixtures/openapi/NetworkAdmissionPreviewRequest/valid/device-nas.json +1 -0
  111. package/fixtures/openapi/NetworkAdmissionPreviewResult/invalid/missing-attributeVersion.json +13 -0
  112. package/fixtures/openapi/NetworkAdmissionPreviewResult/invalid/missing-evaluatedAt.json +13 -0
  113. package/fixtures/openapi/NetworkAdmissionPreviewResult/valid/versioned.json +14 -0
  114. package/fixtures/openapi/NetworkConflictAnalysisRequest/invalid/missing-runtime-ranges.json +1 -0
  115. package/fixtures/openapi/NetworkConflictAnalysisRequest/invalid/unknown-source-type.json +9 -0
  116. package/fixtures/openapi/NetworkConflictAnalysisRequest/valid/empty-runtime-ranges.json +3 -0
  117. package/fixtures/openapi/NetworkConflictAnalysisRequest/{invalid → valid}/ipv6-range.json +1 -1
  118. package/fixtures/openapi/NetworkEAPServerTrust/invalid/no-server-validation.json +5 -0
  119. package/fixtures/openapi/NetworkEAPServerTrust/valid/named-server.json +9 -0
  120. package/fixtures/openapi/NetworkEndpointResponsePolicyInput/invalid/all-sites-implicit.json +45 -0
  121. package/fixtures/openapi/NetworkEndpointResponsePolicyInput/invalid/unbounded-stages.json +54 -0
  122. package/fixtures/openapi/NetworkEndpointResponsePolicyInput/invalid/zero-heartbeat.json +47 -0
  123. package/fixtures/openapi/NetworkEndpointResponsePolicyInput/valid/staged-protection.json +47 -0
  124. package/fixtures/openapi/NetworkEndpointResponseRestoreInput/invalid/missing-reason.json +3 -0
  125. package/fixtures/openapi/NetworkEndpointResponseRestoreInput/valid/expected-current.json +4 -0
  126. package/fixtures/openapi/NetworkPolicyPreviewResult/invalid/attribute-version.json +1 -0
  127. package/fixtures/openapi/NetworkPolicyPreviewResult/valid/admission-version.json +1 -0
  128. package/fixtures/openapi/NetworkPolicySubjects/invalid/duplicate-role.json +1 -0
  129. package/fixtures/openapi/NetworkPolicySubjects/invalid/role-characters.json +5 -0
  130. package/fixtures/openapi/NetworkPolicySubjects/valid/role-bound.json +1 -0
  131. package/fixtures/openapi/NetworkProxyConfigurationInput/invalid/missing-content.json +1 -0
  132. package/fixtures/openapi/NetworkProxyConfigurationPatchInput/invalid/bad-binding-id.json +9 -0
  133. package/fixtures/openapi/NetworkProxyConfigurationPatchInput/invalid/empty-content.json +1 -0
  134. package/fixtures/openapi/NetworkProxyConfigurationPatchInput/valid/binding-only.json +11 -0
  135. package/fixtures/openapi/NetworkProxyConfigurationPatchInput/valid/clear-binding-disabled.json +5 -0
  136. package/fixtures/openapi/NetworkProxyConfigurationPatchInput/valid/enabled.json +1 -0
  137. package/fixtures/openapi/NetworkSiteInput/invalid/unknown-governance.json +6 -0
  138. package/fixtures/openapi/NetworkSiteInput/valid/restricted-unmanaged.json +6 -0
  139. package/gen/go/sohaapi/client.go +1 -1
  140. package/gen/go/sohaapi/client_test.go +13 -0
  141. package/gen/go/sohaapi/kubernetes_workload_optional_test.go +40 -0
  142. package/gen/go/sohaapi/pod_status_compat_test.go +36 -0
  143. package/gen/go/sohaapi/proxy_configuration_test.go +38 -0
  144. package/gen/go/sohaapi/types.go +3222 -260
  145. package/gen/ts/sohaapi/index.ts +15506 -10104
  146. package/helmrelease/runtime/values_upgrade.go +78 -0
  147. package/helmrelease/runtime/values_upgrade_test.go +123 -0
  148. package/helmrelease/values_update.go +31 -0
  149. package/network/README.md +34 -0
  150. package/network/network-radius-accounting.schema.json +4 -1
  151. package/network/network-runtime-protocol.schema.json +2166 -317
  152. package/network/proxy-runtime.schema.json +482 -67
  153. package/openapi/network_control_test.go +64 -0
  154. package/openapi/soha-api.json +50412 -41310
  155. package/openapi/soha-api.yaml +16859 -10699
  156. package/openapi/spec_generated.go +3 -3
  157. package/package.json +6 -3
  158. package/proxytransport/handler.go +271 -0
  159. package/proxytransport/probe.go +30 -0
  160. package/proxytransport/scope.go +89 -0
  161. package/proxytransport/transport_test.go +60 -0
  162. package/resource/agent_profiles.go +9 -15
  163. package/resource/agent_profiles_test.go +9 -3
  164. package/resource/custom_graph_page.go +18 -0
  165. package/resource/models.go +102 -57
  166. package/resource/network_topology.go +22 -0
  167. package/resource/network_topology_projection.go +36 -22
  168. package/resource/network_topology_projection_test.go +4 -0
  169. package/resource/network_topology_test.go +20 -0
  170. package/resource/network_topology_validation.go +5 -0
  171. package/resource/observed_workload.go +18 -0
  172. package/resource/pod_status_test.go +22 -0
  173. package/resource/rbac_graph.go +4 -0
  174. package/resource/rollout_optional_test.go +33 -0
  175. package/resource/runtime/network_relationship_primitives.go +66 -0
  176. package/resource/runtime/network_relationship_primitives_test.go +147 -0
  177. package/resource/runtime/network_topology.go +17 -5
  178. package/resource/runtime/network_topology_builder.go +405 -0
  179. package/resource/runtime/network_topology_builder_backends.go +195 -0
  180. package/resource/runtime/network_topology_builder_gateway_l4_test.go +86 -0
  181. package/resource/runtime/network_topology_builder_ingress.go +302 -0
  182. package/resource/runtime/network_topology_builder_legacy.go +66 -0
  183. package/resource/runtime/network_topology_builder_providers.go +242 -0
  184. package/resource/runtime/network_topology_builder_test.go +186 -0
  185. package/resource/runtime/network_topology_declarations.go +329 -0
  186. package/resource/runtime/resource_edit.go +22 -0
  187. package/resource/runtime/resource_edit_test.go +32 -0
  188. package/resource/runtime/resource_graph_collect.go +325 -0
  189. package/resource/runtime/resource_graph_configuration.go +59 -0
  190. package/resource/runtime/resource_graph_configuration_collect.go +198 -0
  191. package/resource/runtime/resource_graph_configuration_test.go +470 -0
  192. package/resource/runtime/resource_graph_custom_page.go +159 -0
  193. package/resource/runtime/resource_graph_custom_root.go +132 -0
  194. package/resource/runtime/resource_graph_custom_root_test.go +197 -0
  195. package/resource/runtime/resource_graph_enrichment.go +417 -0
  196. package/resource/runtime/resource_graph_enrichment_hpa_test.go +117 -0
  197. package/resource/runtime/resource_graph_enrichment_test.go +201 -0
  198. package/resource/runtime/resource_graph_metadata.go +125 -0
  199. package/resource/runtime/resource_graph_metadata_test.go +204 -0
  200. package/resource/runtime/resource_graph_network.go +224 -0
  201. package/resource/runtime/resource_graph_network_collect.go +174 -0
  202. package/resource/runtime/resource_graph_network_collect_test.go +147 -0
  203. package/resource/runtime/resource_graph_network_test.go +342 -0
  204. package/resource/runtime/resource_graph_rbac.go +278 -0
  205. package/resource/runtime/resource_graph_rbac_test.go +372 -0
  206. package/resource/runtime/resource_graph_rules.go +834 -0
  207. package/resource/runtime/resource_graph_storage.go +144 -0
  208. package/resource/runtime/resource_graph_storage_collect.go +163 -0
  209. package/resource/runtime/resource_graph_storage_test.go +397 -0
  210. package/resource/runtime/resource_graph_test.go +627 -0
  211. package/resource/runtime/secret_certificate.go +44 -0
  212. package/resource/runtime/secret_certificate_test.go +87 -0
  213. package/resource/storage_graph.go +4 -0
@@ -0,0 +1,138 @@
1
+ // Package endpointcontrol owns the bounded native endpoint-channel protocol.
2
+ package endpointcontrol
3
+
4
+ import (
5
+ "bytes"
6
+ "encoding/json"
7
+ "errors"
8
+ "io"
9
+ "regexp"
10
+ "time"
11
+ )
12
+
13
+ const Version = "endpoint-controls/v1alpha1"
14
+ const Capability = "windows-channel-controls-v1"
15
+
16
+ type Controls struct {
17
+ USB bool `json:"usb"`
18
+ Print bool `json:"print"`
19
+ Clipboard bool `json:"clipboard"`
20
+ }
21
+ type Policy struct {
22
+ DeviceID string `json:"deviceId"`
23
+ Revision int64 `json:"revision"`
24
+ PublishedVersion int64 `json:"publishedVersion"`
25
+ Draft Controls `json:"draft"`
26
+ Published *Controls `json:"published,omitempty"`
27
+ UpdatedAt time.Time `json:"updatedAt"`
28
+ }
29
+ type SaveInput struct {
30
+ ExpectedRevision int64 `json:"expectedRevision"`
31
+ Draft Controls `json:"draft"`
32
+ }
33
+ type PublishInput struct {
34
+ ExpectedRevision int64 `json:"expectedRevision"`
35
+ }
36
+ type Desired struct {
37
+ Configured bool `json:"configured"`
38
+ SchemaVersion string `json:"schemaVersion"`
39
+ DeviceID string `json:"deviceId"`
40
+ RuntimeID string `json:"runtimeId"`
41
+ CredentialID string `json:"credentialId"`
42
+ PolicyVersion int64 `json:"policyVersion"`
43
+ Controls Controls `json:"controls"`
44
+ Challenge string `json:"challenge"`
45
+ IssuedAt time.Time `json:"issuedAt"`
46
+ ValidUntil time.Time `json:"validUntil"`
47
+ }
48
+ type Channel struct {
49
+ Name string `json:"name"`
50
+ Scope string `json:"scope"`
51
+ State string `json:"state"`
52
+ Targets int `json:"targets"`
53
+ Controlled int `json:"controlled"`
54
+ ReasonCode string `json:"reasonCode"`
55
+ }
56
+ type Receipt struct {
57
+ SchemaVersion string `json:"schemaVersion"`
58
+ DeviceID string `json:"deviceId"`
59
+ RuntimeID string `json:"runtimeId"`
60
+ CredentialID string `json:"credentialId"`
61
+ PolicyVersion int64 `json:"policyVersion"`
62
+ Challenge string `json:"challenge"`
63
+ ObservedAt time.Time `json:"observedAt"`
64
+ Channels []Channel `json:"channels"`
65
+ }
66
+ type View struct {
67
+ Policy Policy `json:"policy"`
68
+ Receipt *Observation `json:"receipt,omitempty"`
69
+ ReceivedAt *time.Time `json:"receivedAt,omitempty"`
70
+ Fresh bool `json:"fresh"`
71
+ RuntimeCount int `json:"runtimeCount"`
72
+ PendingRuntimeCount int `json:"pendingRuntimeCount"`
73
+ }
74
+ type Observation struct {
75
+ RuntimeID string `json:"runtimeId"`
76
+ CredentialID string `json:"credentialId"`
77
+ PolicyVersion int64 `json:"policyVersion"`
78
+ ObservedAt time.Time `json:"observedAt"`
79
+ Channels []Channel `json:"channels"`
80
+ }
81
+
82
+ var identifier = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$`)
83
+ var challenge = regexp.MustCompile(`^[A-Za-z0-9_-]{43}$`)
84
+ var reason = regexp.MustCompile(`^[a-z][a-z0-9_]{0,63}$`)
85
+
86
+ func Decode(raw []byte, out any) error {
87
+ if len(raw) > 32768 {
88
+ return errors.New("endpoint channel message too large")
89
+ }
90
+ d := json.NewDecoder(bytes.NewReader(raw))
91
+ d.DisallowUnknownFields()
92
+ if err := d.Decode(out); err != nil {
93
+ return err
94
+ }
95
+ if d.Decode(new(any)) != io.EOF {
96
+ return errors.New("trailing channel message")
97
+ }
98
+ return nil
99
+ }
100
+ func ValidDesired(d Desired, now time.Time) bool {
101
+ return (d.Configured && d.PolicyVersion > 0 || !d.Configured && d.PolicyVersion == 0) && validIdentity(d.SchemaVersion, d.DeviceID, d.RuntimeID, d.CredentialID, d.PolicyVersion, d.Challenge) && !d.IssuedAt.After(now.Add(30*time.Second)) && d.ValidUntil.After(now) && d.ValidUntil.After(d.IssuedAt) && d.ValidUntil.Sub(d.IssuedAt) <= time.Minute
102
+ }
103
+ func ValidReceipt(r Receipt, now time.Time) bool {
104
+ if !validIdentity(r.SchemaVersion, r.DeviceID, r.RuntimeID, r.CredentialID, r.PolicyVersion, r.Challenge) || len(r.Channels) != 3 || r.ObservedAt.Before(now.Add(-time.Minute)) || r.ObservedAt.After(now.Add(30*time.Second)) {
105
+ return false
106
+ }
107
+ seen := map[string]bool{}
108
+ for _, c := range r.Channels {
109
+ if seen[c.Name] || !ValidChannel(c) {
110
+ return false
111
+ }
112
+ seen[c.Name] = true
113
+ }
114
+ return seen["usb"] && seen["print"] && seen["clipboard"]
115
+ }
116
+ func validIdentity(version, device, runtime, credential string, revision int64, nonce string) bool {
117
+ return version == Version && identifier.MatchString(device) && identifier.MatchString(runtime) && identifier.MatchString(credential) && revision >= 0 && challenge.MatchString(nonce)
118
+ }
119
+ func ValidChannel(c Channel) bool {
120
+ scopes := map[string]string{"usb": "current_usb_volumes", "print": "local_new_print_jobs", "clipboard": "enrolled_console_win32"}
121
+ if scopes[c.Name] == "" || c.Scope != scopes[c.Name] || c.Targets < 0 || c.Targets > 256 || c.Controlled < 0 || c.Controlled > c.Targets || !reason.MatchString(c.ReasonCode) {
122
+ return false
123
+ }
124
+ if c.State == "controlled" && (c.Targets == 0 || c.Controlled != c.Targets) {
125
+ return false
126
+ }
127
+ if (c.State == "off" || c.State == "unconfigured" || c.State == "not_present") && c.Controlled != 0 {
128
+ return false
129
+ }
130
+ if (c.State == "unconfigured" || c.State == "not_present") && c.Targets != 0 {
131
+ return false
132
+ }
133
+ switch c.State {
134
+ case "off", "unconfigured", "not_present", "controlled", "partial", "failed", "unsupported", "conflict":
135
+ return true
136
+ }
137
+ return false
138
+ }
@@ -0,0 +1,51 @@
1
+ package endpointcontrol
2
+
3
+ import (
4
+ "testing"
5
+ "time"
6
+ )
7
+
8
+ func TestNativeReceiptCannotClaimContradictoryControl(t *testing.T) {
9
+ cases := []Channel{
10
+ {Name: "usb", Scope: "current_usb_volumes", State: "controlled", Targets: 1, Controlled: 0, ReasonCode: "native_scope_only"},
11
+ {Name: "usb", Scope: "current_usb_volumes", State: "controlled", Targets: 0, Controlled: 0, ReasonCode: "native_scope_only"},
12
+ {Name: "clipboard", Scope: "current_usb_volumes", State: "controlled", Targets: 1, Controlled: 1, ReasonCode: "native_scope_only"},
13
+ {Name: "print", Scope: "local_new_print_jobs", State: "off", Targets: 1, Controlled: 1, ReasonCode: "disabled"},
14
+ }
15
+ for _, c := range cases {
16
+ if ValidChannel(c) {
17
+ t.Fatalf("contradictory control accepted: %+v", c)
18
+ }
19
+ }
20
+ if !ValidChannel(Channel{Name: "clipboard", Scope: "enrolled_console_win32", State: "controlled", Targets: 1, Controlled: 1, ReasonCode: "native_scope_only"}) {
21
+ t.Fatal("bounded native control rejected")
22
+ }
23
+ }
24
+ func TestUnconfiguredIsNotAPublishedRelease(t *testing.T) {
25
+ now := time.Now().UTC()
26
+ d := Desired{SchemaVersion: Version, DeviceID: "d", RuntimeID: "r", CredentialID: "c", Challenge: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", IssuedAt: now, ValidUntil: now.Add(time.Minute)}
27
+ if !ValidDesired(d, now) {
28
+ t.Fatal("unconfigured envelope rejected")
29
+ }
30
+ d.PolicyVersion = 1
31
+ if ValidDesired(d, now) {
32
+ t.Fatal("positive unpublished revision accepted")
33
+ }
34
+ d.Configured = true
35
+ if !ValidDesired(d, now) {
36
+ t.Fatal("explicit published release rejected")
37
+ }
38
+ }
39
+
40
+ func TestMissingBooleanCannotDecodeAsRelease(t *testing.T) {
41
+ var controls Controls
42
+ for _, raw := range []string{`{}`, `{"usb":false,"print":false}`, `{"usb":false,"print":false,"clipboard":null}`} {
43
+ if Decode([]byte(raw), &controls) == nil {
44
+ t.Fatal("incomplete channel decisions accepted", raw)
45
+ }
46
+ }
47
+ var desired Desired
48
+ if Decode([]byte(`{"configured":true,"schemaVersion":"endpoint-controls/v1alpha1","deviceId":"device","runtimeId":"runtime","credentialId":"credential","policyVersion":99,"challenge":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","issuedAt":"2026-10-28T00:00:00Z","validUntil":"2026-10-28T00:01:00Z"}`), &desired) == nil {
49
+ t.Fatal("missing controls became release")
50
+ }
51
+ }
@@ -0,0 +1,397 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://opensoha.dev/contracts/endpointcontrol/protocol.schema.json",
4
+ "title": "Endpoint channel controls",
5
+ "oneOf": [
6
+ {
7
+ "$ref": "#/$defs/EndpointChannelDesired"
8
+ },
9
+ {
10
+ "$ref": "#/$defs/EndpointChannelReport"
11
+ }
12
+ ],
13
+ "$defs": {
14
+ "EndpointChannelControls": {
15
+ "type": "object",
16
+ "additionalProperties": false,
17
+ "required": [
18
+ "usb",
19
+ "print",
20
+ "clipboard"
21
+ ],
22
+ "properties": {
23
+ "usb": {
24
+ "type": "boolean"
25
+ },
26
+ "print": {
27
+ "type": "boolean"
28
+ },
29
+ "clipboard": {
30
+ "type": "boolean"
31
+ }
32
+ }
33
+ },
34
+ "EndpointChannelReceipt": {
35
+ "type": "object",
36
+ "additionalProperties": false,
37
+ "required": [
38
+ "name",
39
+ "scope",
40
+ "state",
41
+ "targets",
42
+ "controlled",
43
+ "reasonCode"
44
+ ],
45
+ "properties": {
46
+ "name": {
47
+ "type": "string",
48
+ "enum": [
49
+ "usb",
50
+ "print",
51
+ "clipboard"
52
+ ]
53
+ },
54
+ "scope": {
55
+ "type": "string",
56
+ "enum": [
57
+ "current_usb_volumes",
58
+ "local_new_print_jobs",
59
+ "enrolled_console_win32"
60
+ ]
61
+ },
62
+ "state": {
63
+ "type": "string",
64
+ "enum": [
65
+ "off",
66
+ "controlled",
67
+ "partial",
68
+ "failed",
69
+ "unsupported",
70
+ "conflict",
71
+ "unconfigured",
72
+ "not_present"
73
+ ]
74
+ },
75
+ "targets": {
76
+ "type": "integer",
77
+ "minimum": 0,
78
+ "maximum": 256
79
+ },
80
+ "controlled": {
81
+ "type": "integer",
82
+ "minimum": 0,
83
+ "maximum": 256
84
+ },
85
+ "reasonCode": {
86
+ "type": "string",
87
+ "pattern": "^[a-z][a-z0-9_]{0,63}$"
88
+ }
89
+ }
90
+ },
91
+ "EndpointChannelPolicy": {
92
+ "type": "object",
93
+ "additionalProperties": false,
94
+ "required": [
95
+ "deviceId",
96
+ "revision",
97
+ "publishedVersion",
98
+ "draft",
99
+ "updatedAt"
100
+ ],
101
+ "properties": {
102
+ "deviceId": {
103
+ "type": "string",
104
+ "minLength": 1,
105
+ "maxLength": 128,
106
+ "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"
107
+ },
108
+ "revision": {
109
+ "type": "integer",
110
+ "format": "int64",
111
+ "minimum": 0
112
+ },
113
+ "publishedVersion": {
114
+ "type": "integer",
115
+ "format": "int64",
116
+ "minimum": 0
117
+ },
118
+ "draft": {
119
+ "$ref": "#/$defs/EndpointChannelControls"
120
+ },
121
+ "published": {
122
+ "$ref": "#/$defs/EndpointChannelControls"
123
+ },
124
+ "updatedAt": {
125
+ "type": "string",
126
+ "format": "date-time"
127
+ }
128
+ }
129
+ },
130
+ "EndpointChannelDesired": {
131
+ "type": "object",
132
+ "additionalProperties": false,
133
+ "required": [
134
+ "schemaVersion",
135
+ "deviceId",
136
+ "runtimeId",
137
+ "credentialId",
138
+ "policyVersion",
139
+ "challenge",
140
+ "controls",
141
+ "issuedAt",
142
+ "validUntil",
143
+ "configured"
144
+ ],
145
+ "properties": {
146
+ "schemaVersion": {
147
+ "type": "string",
148
+ "const": "endpoint-controls/v1alpha1"
149
+ },
150
+ "deviceId": {
151
+ "type": "string",
152
+ "minLength": 1,
153
+ "maxLength": 128,
154
+ "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"
155
+ },
156
+ "runtimeId": {
157
+ "type": "string",
158
+ "minLength": 1,
159
+ "maxLength": 128,
160
+ "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"
161
+ },
162
+ "credentialId": {
163
+ "type": "string",
164
+ "minLength": 1,
165
+ "maxLength": 128,
166
+ "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"
167
+ },
168
+ "policyVersion": {
169
+ "type": "integer",
170
+ "format": "int64",
171
+ "minimum": 0
172
+ },
173
+ "challenge": {
174
+ "type": "string",
175
+ "pattern": "^[A-Za-z0-9_-]{43}$"
176
+ },
177
+ "controls": {
178
+ "$ref": "#/$defs/EndpointChannelControls"
179
+ },
180
+ "issuedAt": {
181
+ "type": "string",
182
+ "format": "date-time"
183
+ },
184
+ "validUntil": {
185
+ "type": "string",
186
+ "format": "date-time"
187
+ },
188
+ "configured": {
189
+ "type": "boolean",
190
+ "description": "False means unconfigured; never a release. Release requires a higher explicit published version."
191
+ }
192
+ }
193
+ },
194
+ "EndpointChannelReport": {
195
+ "type": "object",
196
+ "additionalProperties": false,
197
+ "required": [
198
+ "schemaVersion",
199
+ "deviceId",
200
+ "runtimeId",
201
+ "credentialId",
202
+ "policyVersion",
203
+ "challenge",
204
+ "observedAt",
205
+ "channels"
206
+ ],
207
+ "properties": {
208
+ "schemaVersion": {
209
+ "type": "string",
210
+ "const": "endpoint-controls/v1alpha1"
211
+ },
212
+ "deviceId": {
213
+ "type": "string",
214
+ "minLength": 1,
215
+ "maxLength": 128,
216
+ "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"
217
+ },
218
+ "runtimeId": {
219
+ "type": "string",
220
+ "minLength": 1,
221
+ "maxLength": 128,
222
+ "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"
223
+ },
224
+ "credentialId": {
225
+ "type": "string",
226
+ "minLength": 1,
227
+ "maxLength": 128,
228
+ "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"
229
+ },
230
+ "policyVersion": {
231
+ "type": "integer",
232
+ "format": "int64",
233
+ "minimum": 0
234
+ },
235
+ "challenge": {
236
+ "type": "string",
237
+ "pattern": "^[A-Za-z0-9_-]{43}$"
238
+ },
239
+ "observedAt": {
240
+ "type": "string",
241
+ "format": "date-time"
242
+ },
243
+ "channels": {
244
+ "type": "array",
245
+ "minItems": 3,
246
+ "maxItems": 3,
247
+ "items": {
248
+ "$ref": "#/$defs/EndpointChannelReceipt"
249
+ }
250
+ }
251
+ }
252
+ },
253
+ "EndpointChannelView": {
254
+ "type": "object",
255
+ "additionalProperties": false,
256
+ "required": [
257
+ "policy",
258
+ "fresh",
259
+ "runtimeCount",
260
+ "pendingRuntimeCount"
261
+ ],
262
+ "properties": {
263
+ "policy": {
264
+ "$ref": "#/$defs/EndpointChannelPolicy"
265
+ },
266
+ "receipt": {
267
+ "$ref": "#/$defs/EndpointChannelObservation"
268
+ },
269
+ "receivedAt": {
270
+ "type": "string",
271
+ "format": "date-time"
272
+ },
273
+ "fresh": {
274
+ "type": "boolean"
275
+ },
276
+ "runtimeCount": {
277
+ "type": "integer",
278
+ "minimum": 0
279
+ },
280
+ "pendingRuntimeCount": {
281
+ "type": "integer",
282
+ "minimum": 0
283
+ }
284
+ }
285
+ },
286
+ "EndpointChannelSaveInput": {
287
+ "type": "object",
288
+ "additionalProperties": false,
289
+ "required": [
290
+ "expectedRevision",
291
+ "draft"
292
+ ],
293
+ "properties": {
294
+ "expectedRevision": {
295
+ "type": "integer",
296
+ "format": "int64",
297
+ "minimum": 0
298
+ },
299
+ "draft": {
300
+ "$ref": "#/$defs/EndpointChannelControls"
301
+ }
302
+ }
303
+ },
304
+ "EndpointChannelPublishInput": {
305
+ "type": "object",
306
+ "additionalProperties": false,
307
+ "required": [
308
+ "expectedRevision"
309
+ ],
310
+ "properties": {
311
+ "expectedRevision": {
312
+ "type": "integer",
313
+ "format": "int64",
314
+ "minimum": 1
315
+ }
316
+ }
317
+ },
318
+ "EndpointChannelPolicyEnvelope": {
319
+ "type": "object",
320
+ "additionalProperties": false,
321
+ "required": [
322
+ "data"
323
+ ],
324
+ "properties": {
325
+ "data": {
326
+ "$ref": "#/$defs/EndpointChannelPolicy"
327
+ }
328
+ }
329
+ },
330
+ "EndpointChannelDesiredEnvelope": {
331
+ "type": "object",
332
+ "additionalProperties": false,
333
+ "required": [
334
+ "data"
335
+ ],
336
+ "properties": {
337
+ "data": {
338
+ "$ref": "#/$defs/EndpointChannelDesired"
339
+ }
340
+ }
341
+ },
342
+ "EndpointChannelViewEnvelope": {
343
+ "type": "object",
344
+ "additionalProperties": false,
345
+ "required": [
346
+ "data"
347
+ ],
348
+ "properties": {
349
+ "data": {
350
+ "$ref": "#/$defs/EndpointChannelView"
351
+ }
352
+ }
353
+ },
354
+ "EndpointChannelObservation": {
355
+ "type": "object",
356
+ "additionalProperties": false,
357
+ "required": [
358
+ "runtimeId",
359
+ "credentialId",
360
+ "policyVersion",
361
+ "observedAt",
362
+ "channels"
363
+ ],
364
+ "properties": {
365
+ "runtimeId": {
366
+ "type": "string",
367
+ "minLength": 1,
368
+ "maxLength": 128,
369
+ "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"
370
+ },
371
+ "credentialId": {
372
+ "type": "string",
373
+ "minLength": 1,
374
+ "maxLength": 128,
375
+ "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"
376
+ },
377
+ "policyVersion": {
378
+ "type": "integer",
379
+ "format": "int64",
380
+ "minimum": 0
381
+ },
382
+ "observedAt": {
383
+ "type": "string",
384
+ "format": "date-time"
385
+ },
386
+ "channels": {
387
+ "type": "array",
388
+ "minItems": 3,
389
+ "maxItems": 3,
390
+ "items": {
391
+ "$ref": "#/$defs/EndpointChannelReceipt"
392
+ }
393
+ }
394
+ }
395
+ }
396
+ }
397
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "schemaVersion": "endpoint-controls/v1alpha1",
3
+ "deviceId": "device",
4
+ "runtimeId": "endpoint",
5
+ "credentialId": "credential",
6
+ "policyVersion": 1,
7
+ "challenge": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
8
+ "controls": {
9
+ "usb": true,
10
+ "print": true,
11
+ "clipboard": true
12
+ },
13
+ "issuedAt": "2026-10-28T00:00:00Z",
14
+ "validUntil": "2026-10-28T00:01:00Z",
15
+ "content": "must not transport file content"
16
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "schemaVersion": "endpoint-controls/v1alpha1",
3
+ "deviceId": "device",
4
+ "runtimeId": "endpoint",
5
+ "credentialId": "credential",
6
+ "policyVersion": 1,
7
+ "challenge": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
8
+ "controls": {
9
+ "usb": true,
10
+ "print": true
11
+ },
12
+ "issuedAt": "2026-10-28T00:00:00Z",
13
+ "validUntil": "2026-10-28T00:01:00Z",
14
+ "configured": true
15
+ }
@@ -0,0 +1,36 @@
1
+ {
2
+ "schemaVersion": "endpoint-controls/v1alpha1",
3
+ "deviceId": "device",
4
+ "runtimeId": "endpoint",
5
+ "credentialId": "credential",
6
+ "policyVersion": 1,
7
+ "challenge": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
8
+ "observedAt": "2026-10-28T00:00:00Z",
9
+ "channels": [
10
+ {
11
+ "name": "usb",
12
+ "scope": "current_usb_volumes",
13
+ "state": "controlled",
14
+ "targets": 1,
15
+ "controlled": 1,
16
+ "reasonCode": "native_scope_only"
17
+ },
18
+ {
19
+ "name": "print",
20
+ "scope": "local_new_print_jobs",
21
+ "state": "controlled",
22
+ "targets": 1,
23
+ "controlled": 1,
24
+ "reasonCode": "native_scope_only"
25
+ },
26
+ {
27
+ "name": "clipboard",
28
+ "scope": "enrolled_console_win32",
29
+ "state": "controlled",
30
+ "targets": 1,
31
+ "controlled": 1,
32
+ "reasonCode": "native_scope_only"
33
+ }
34
+ ],
35
+ "clipboardText": "forbidden"
36
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "schemaVersion": "endpoint-controls/v1alpha1",
3
+ "deviceId": "device",
4
+ "runtimeId": "endpoint",
5
+ "credentialId": "credential",
6
+ "policyVersion": 1,
7
+ "challenge": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
8
+ "controls": {
9
+ "usb": true,
10
+ "print": true,
11
+ "clipboard": true
12
+ },
13
+ "issuedAt": "2026-10-28T00:00:00Z",
14
+ "validUntil": "2026-10-28T00:01:00Z",
15
+ "configured": true
16
+ }