From e1c3d7f9830172c8aedb09dc792ecba052ed7d6a Mon Sep 17 00:00:00 2001 From: Gudge Date: Fri, 14 Aug 2026 09:17:28 -0700 Subject: [PATCH] Add 0.8 one-shot configuration contract This PR adds the mutable 0.8.0-alpha one-shot configuration contract. Details * Add independently owned stable and recursively closed experimental wire types for the development contract. * Model 0.8 containment values, ProcessContainer denial capture, Windows Sandbox, WSLC, and telemetry syntax. * Add comprehensive structural, fixture, compatibility, and adjacent-version boundary coverage while preserving the published contracts. Tests * `cargo fmt --all -- --check` * `cargo test -p mxc_config_contract` (322 tests passed) * `cargo clippy -p mxc_config_contract --all-targets -- -D warnings` Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/dev/experimental.rs | 109 +++ src/core/mxc_config_contract/src/dev/mod.rs | 124 ++++ .../mxc_config_contract/src/dev/network.rs | 70 ++ .../mxc_config_contract/src/dev/one_shot.rs | 93 +++ .../mxc_config_contract/src/dev/primitives.rs | 113 +++ .../mxc_config_contract/src/dev/stable.rs | 218 ++++++ src/core/mxc_config_contract/src/lib.rs | 1 + .../tests/v0_6_0_alpha/root.rs | 8 +- .../tests/v0_7_0_alpha/root.rs | 8 +- .../mxc_config_contract/tests/v0_8_0_alpha.rs | 21 + .../tests/v0_8_0_alpha/annotations.rs | 128 ++++ .../tests/v0_8_0_alpha/common.rs | 39 + .../tests/v0_8_0_alpha/enums.rs | 316 +++++++++ .../tests/v0_8_0_alpha/experimental.rs | 11 + .../tests/v0_8_0_alpha/experimental/root.rs | 90 +++ .../experimental/test_and_telemetry.rs | 174 +++++ .../experimental/windows_sandbox.rs | 118 +++ .../tests/v0_8_0_alpha/experimental/wslc.rs | 264 +++++++ .../tests/v0_8_0_alpha/fixtures.rs | 91 +++ .../fixtures/invalid/incomplete_lxc.json | 9 + .../seatbelt_invalid_launch_method.json | 10 + .../invalid/seatbelt_unknown_field.json | 10 + .../fixtures/invalid/state_aware.json | 8 + .../invalid/unknown_nested_field.json | 9 + .../fixtures/invalid/unknown_root_field.json | 7 + .../fixtures/valid/annotations.json | 10 + .../fixtures/valid/app_container_alias.json | 12 + .../v0_8_0_alpha/fixtures/valid/complete.json | 60 ++ .../valid/empty_optional_objects.json | 14 + .../fixtures/valid/experimental.json | 9 + .../fixtures/valid/macos_sandbox_alias.json | 11 + .../v0_8_0_alpha/fixtures/valid/minimal.json | 6 + .../fixtures/valid/proxy_builtin.json | 11 + .../fixtures/valid/proxy_localhost.json | 11 + .../fixtures/valid/proxy_url.json | 11 + .../fixtures/valid/seatbelt_complete.json | 18 + .../fixtures/valid/seatbelt_minimal.json | 8 + .../tests/v0_8_0_alpha/network.rs | 178 +++++ .../tests/v0_8_0_alpha/one_shot.rs | 669 ++++++++++++++++++ .../tests/v0_8_0_alpha/optional_fields.rs | 408 +++++++++++ .../tests/v0_8_0_alpha/seatbelt.rs | 225 ++++++ .../tests/version_boundaries.rs | 6 + .../tests/version_boundaries/annotations.rs | 40 +- .../tests/version_boundaries/common.rs | 45 +- .../tests/version_boundaries/compatibility.rs | 95 ++- .../tests/version_boundaries/containment.rs | 18 + .../tests/version_boundaries/experimental.rs | 24 + .../version_boundaries/process_container.rs | 14 + .../tests/version_boundaries/seatbelt.rs | 66 +- 49 files changed, 3908 insertions(+), 110 deletions(-) create mode 100644 src/core/mxc_config_contract/src/dev/experimental.rs create mode 100644 src/core/mxc_config_contract/src/dev/mod.rs create mode 100644 src/core/mxc_config_contract/src/dev/network.rs create mode 100644 src/core/mxc_config_contract/src/dev/one_shot.rs create mode 100644 src/core/mxc_config_contract/src/dev/primitives.rs create mode 100644 src/core/mxc_config_contract/src/dev/stable.rs create mode 100644 src/core/mxc_config_contract/tests/v0_8_0_alpha.rs create mode 100644 src/core/mxc_config_contract/tests/v0_8_0_alpha/annotations.rs create mode 100644 src/core/mxc_config_contract/tests/v0_8_0_alpha/common.rs create mode 100644 src/core/mxc_config_contract/tests/v0_8_0_alpha/enums.rs create mode 100644 src/core/mxc_config_contract/tests/v0_8_0_alpha/experimental.rs create mode 100644 src/core/mxc_config_contract/tests/v0_8_0_alpha/experimental/root.rs create mode 100644 src/core/mxc_config_contract/tests/v0_8_0_alpha/experimental/test_and_telemetry.rs create mode 100644 src/core/mxc_config_contract/tests/v0_8_0_alpha/experimental/windows_sandbox.rs create mode 100644 src/core/mxc_config_contract/tests/v0_8_0_alpha/experimental/wslc.rs create mode 100644 src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures.rs create mode 100644 src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/invalid/incomplete_lxc.json create mode 100644 src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/invalid/seatbelt_invalid_launch_method.json create mode 100644 src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/invalid/seatbelt_unknown_field.json create mode 100644 src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/invalid/state_aware.json create mode 100644 src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/invalid/unknown_nested_field.json create mode 100644 src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/invalid/unknown_root_field.json create mode 100644 src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/valid/annotations.json create mode 100644 src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/valid/app_container_alias.json create mode 100644 src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/valid/complete.json create mode 100644 src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/valid/empty_optional_objects.json create mode 100644 src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/valid/experimental.json create mode 100644 src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/valid/macos_sandbox_alias.json create mode 100644 src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/valid/minimal.json create mode 100644 src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/valid/proxy_builtin.json create mode 100644 src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/valid/proxy_localhost.json create mode 100644 src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/valid/proxy_url.json create mode 100644 src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/valid/seatbelt_complete.json create mode 100644 src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/valid/seatbelt_minimal.json create mode 100644 src/core/mxc_config_contract/tests/v0_8_0_alpha/network.rs create mode 100644 src/core/mxc_config_contract/tests/v0_8_0_alpha/one_shot.rs create mode 100644 src/core/mxc_config_contract/tests/v0_8_0_alpha/optional_fields.rs create mode 100644 src/core/mxc_config_contract/tests/v0_8_0_alpha/seatbelt.rs create mode 100644 src/core/mxc_config_contract/tests/version_boundaries/containment.rs create mode 100644 src/core/mxc_config_contract/tests/version_boundaries/experimental.rs create mode 100644 src/core/mxc_config_contract/tests/version_boundaries/process_container.rs diff --git a/src/core/mxc_config_contract/src/dev/experimental.rs b/src/core/mxc_config_contract/src/dev/experimental.rs new file mode 100644 index 000000000..2f8d3dfb0 --- /dev/null +++ b/src/core/mxc_config_contract/src/dev/experimental.rs @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use super::primitives::OptionalField; +use std::num::NonZeroU16; + +/// Placeholder feature used to exercise experimental configuration plumbing. +#[derive(Debug, serde::Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct TestFeature { + /// The message for the test feature. + #[serde(default)] + pub message: OptionalField, +} + +/// One-shot telemetry override. +#[derive(Debug, serde::Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct Telemetry { + /// Whether telemetry is enabled. + #[serde(default)] + pub enabled: OptionalField, +} + +/// Compatibility settings accepted for one-shot Windows Sandbox requests. +#[derive(Debug, serde::Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct OneShotWindowsSandbox { + /// Idle timeout before teardown, in milliseconds. + #[serde(default)] + pub idle_timeout_ms: OptionalField, + /// Legacy idle-timeout field retained for compatibility. + #[serde(default)] + pub idle_timeout: OptionalField, + /// Optional daemon named-pipe override. + #[serde(default)] + pub daemon_pipe_name: OptionalField, +} + +#[rustfmt::skip] +string_enum! { +/// Transport protocol for a WSLC port mapping. +#[derive(Debug)] +pub enum TransportProtocol { + /// TCP transport. + Tcp => ["tcp"], +} +} + +/// A host-to-container WSLC port mapping. +#[derive(Debug, serde::Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct PortMapping { + /// Non-zero TCP port on the Windows host. + pub windows_port: NonZeroU16, + /// Non-zero TCP port inside the container. + pub container_port: NonZeroU16, + /// Optional transport protocol. Only TCP is currently supported. + #[serde(default)] + pub protocol: OptionalField, +} + +/// One-shot WSLC backend settings. +#[derive(Debug, serde::Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct OneShotWslc { + /// Target operating system inside the container. + #[serde(default)] + pub target_os: OptionalField, + /// Container image reference. + #[serde(default)] + pub image: OptionalField, + /// Path to a local image tarball to import. + #[serde(default)] + pub image_tar_path: OptionalField, + /// Requested virtual CPU count. + #[serde(default)] + pub cpu_count: OptionalField, + /// Requested memory limit in megabytes. + #[serde(default)] + pub memory_mb: OptionalField, + /// Whether GPU passthrough is enabled. + #[serde(default)] + pub gpu: OptionalField, + /// Optional storage path override. + #[serde(default)] + pub storage_path: OptionalField, + /// Optional host-to-container TCP port mappings. + #[serde(default)] + pub port_mappings: OptionalField>, +} + +/// Experimental settings. +#[derive(Debug, serde::Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct OneShotExperimental { + /// Optional placeholder test feature. + #[serde(default)] + pub test: OptionalField, + /// Optional one-shot Windows Sandbox compatibility settings. + #[serde(rename = "windows_sandbox", default)] + pub windows_sandbox: OptionalField, + /// Optional one-shot WSLC backend settings. + #[serde(default)] + pub wslc: OptionalField, + /// Optional telemetry override. + #[serde(default)] + pub telemetry: OptionalField, +} diff --git a/src/core/mxc_config_contract/src/dev/mod.rs b/src/core/mxc_config_contract/src/dev/mod.rs new file mode 100644 index 000000000..78626ba09 --- /dev/null +++ b/src/core/mxc_config_contract/src/dev/mod.rs @@ -0,0 +1,124 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Wire types for the mutable `0.8.0-alpha` configuration contract. +//! +//! These types validate the JSON structure and value constraints of the +//! in-development contract. They preserve omitted optional fields for a later +//! adapter to default and normalize. + +// Serde's default fieldless-enum deserializer also accepts externally +// tagged object forms such as {"process": null}. Published contracts accept +// only string values. This macro generates a string-only deserializer while +// supporting explicit compatibility aliases. +macro_rules! string_enum { + ( + $(#[$enum_meta:meta])* + $vis:vis enum $name:ident { + $( + $(#[$variant_meta:meta])* + $variant:ident => [ + $canonical:literal + $(, $alias:literal)* + ] + ),+ $(,)? + } + ) => { + $(#[$enum_meta])* + $vis enum $name { + $( + $(#[$variant_meta])* + $variant, + )+ + } + + impl $name { + const WIRE_VALUES: &'static [&'static str] = &[ + $( + $canonical, + $($alias,)* + )+ + ]; + } + + impl<'de> serde::Deserialize<'de> for $name { + fn deserialize( + deserializer: D, + ) -> Result + where + D: serde::Deserializer<'de>, + { + struct StringEnumVisitor; + + impl<'de> serde::de::Visitor<'de> + for StringEnumVisitor + { + type Value = $name; + + fn expecting( + &self, + formatter: &mut std::fmt::Formatter<'_>, + ) -> std::fmt::Result { + write!( + formatter, + "a valid {} string", + stringify!($name) + ) + } + + fn visit_str( + self, + value: &str, + ) -> Result + where + E: serde::de::Error, + { + match value { + $( + $canonical + $(| $alias)* + => Ok($name::$variant), + )+ + _ => Err(E::unknown_variant( + value, + $name::WIRE_VALUES, + )), + } + } + } + + deserializer.deserialize_str(StringEnumVisitor) + } + } + }; +} + +#[rustfmt::skip] +string_enum! { +/// The exact version marker accepted by this contract. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Version { + /// The development `0.8.0-alpha` contract. + V0_8_0Alpha => ["0.8.0-alpha"], +} +} + +/// The development `0.8.0-alpha` configuration contract. +mod experimental; +mod network; +mod one_shot; +mod primitives; +mod stable; + +pub use experimental::{ + OneShotExperimental, OneShotWindowsSandbox, OneShotWslc, PortMapping, Telemetry, TestFeature, + TransportProtocol, +}; +pub use network::{DefaultNetworkPolicy, Network, NetworkEnforcementMode, NetworkProxy}; +pub use one_shot::{Containment as OneShotContainment, Request as OneShotRequest}; +pub use primitives::{NonEmptyString, OptionalField, True}; +pub use stable::{ + CaptureDenials, CaptureDenialsMode, Fallback, Filesystem, LaunchMethod, Lifecycle, Lxc, + Process, ProcessContainer, ProcessContainerUi, ProcessContainerUiIsolation, Seatbelt, Ui, + UiClipboard, +}; diff --git a/src/core/mxc_config_contract/src/dev/network.rs b/src/core/mxc_config_contract/src/dev/network.rs new file mode 100644 index 000000000..b4a78d399 --- /dev/null +++ b/src/core/mxc_config_contract/src/dev/network.rs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::num::NonZeroU16; + +use super::primitives::{OptionalField, True}; + +#[rustfmt::skip] +string_enum! { +/// The default outbound network policy. +#[derive(Debug)] +pub enum DefaultNetworkPolicy { + /// Allow outbound network access by default. + Allow => ["allow"], + /// Block outbound network access by default. + Block => ["block"], +} +} + +#[rustfmt::skip] +string_enum! { +/// The mechanism used to enforce network policy. +#[derive(Debug)] +pub enum NetworkEnforcementMode { + /// Enforce policy through containment capabilities. + Capabilities => ["capabilities"], + /// Enforce policy through host firewall rules. + Firewall => ["firewall"], + /// Enforce policy through both capabilities and firewall rules. + Both => ["both"], +} +} + +/// One of the proxy configurations accepted by the `0.8.0-alpha` contract. +#[derive(Debug, serde::Deserialize)] +pub enum NetworkProxy { + /// Connect to an existing proxy on a non-zero localhost TCP port. + #[serde(rename = "localhost")] + Localhost(NonZeroU16), + /// Start and use MXC's built-in test proxy. + #[serde(rename = "builtinTestServer")] + BuiltinTestServer(True), + /// Connect through the supplied proxy URL. + #[serde(rename = "url")] + Url(String), +} + +/// Network access policy shared by the stable containment backends. +#[derive(Debug, serde::Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct Network { + /// Optional default network posture. + #[serde(default)] + pub default_policy: OptionalField, + /// Optional network enforcement mechanism. + #[serde(default)] + pub enforcement_mode: OptionalField, + /// Optional hosts allowed when the default policy blocks access. + #[serde(default)] + pub allowed_hosts: OptionalField>, + /// Optional hosts blocked when the default policy allows access. + #[serde(default)] + pub blocked_hosts: OptionalField>, + /// Optional permission to bind and accept local network connections. + #[serde(default)] + pub allow_local_network: OptionalField, + /// Optional proxy configuration. + #[serde(default)] + pub proxy: OptionalField, +} diff --git a/src/core/mxc_config_contract/src/dev/one_shot.rs b/src/core/mxc_config_contract/src/dev/one_shot.rs new file mode 100644 index 000000000..3b09d7550 --- /dev/null +++ b/src/core/mxc_config_contract/src/dev/one_shot.rs @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use super::experimental::OneShotExperimental; +use super::network::Network; +use super::primitives::OptionalField; +use super::stable::{ + Fallback, Filesystem, Lifecycle, Lxc, Process, ProcessContainer, Seatbelt, Ui, +}; +use crate::dev::Version; + +#[rustfmt::skip] +string_enum! { +/// Containment selections available in `0.8.0-alpha`. +#[derive(Debug)] +pub enum Containment { + // Stable-candidate values. + /// Select the host's native process-containment backend. + Process => ["process"], + /// Select the Windows ProcessContainer backend. + ProcessContainer => ["processcontainer", "appcontainer"], + /// Select the Linux LXC backend. + Lxc => ["lxc"], + /// Select the Linux Bubblewrap backend. + Bubblewrap => ["bubblewrap"], + /// Select the macOS Seatbelt backend. + Seatbelt => ["seatbelt", "macos_sandbox"], + + // Development-only values. + /// Select the host's VM-class containment backend. + Vm => ["vm"], + /// Select the Windows Sandbox backend. + WindowsSandbox => ["windows_sandbox"], + /// Select the NanVix micro-VM backend. + Microvm => ["microvm"], + /// Select the Hyperlight micro-VM backend. + Hyperlight => ["hyperlight"], + /// Select the WSL container backend. + Wslc => ["wslc"], + /// Select the Windows IsolationSession backend. + IsolationSession => ["isolation_session"], +} +} + +/// A complete one-shot `0.8.0-alpha` configuration request. +#[derive(Debug, serde::Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct Request { + /// Optional JSON Schema reference for editor validation. + #[serde(rename = "$schema", default)] + pub schema: OptionalField, + /// Optional human-readable annotation ignored by the runtime. + #[serde(rename = "_comment", default)] + pub comment: OptionalField, + /// The exact contract version marker. + pub version: Version, + /// Optional externally assigned container identifier. + #[serde(default)] + pub container_id: OptionalField, + /// Optional containment selection. + #[serde(default)] + pub containment: OptionalField, + /// Optional lifecycle settings. + #[serde(default)] + pub lifecycle: OptionalField, + /// The process to execute. + pub process: Process, + /// Optional filesystem policy. + #[serde(default)] + pub filesystem: OptionalField, + /// Optional fallback consent. + #[serde(default)] + pub fallback: OptionalField, + /// Optional network policy. + #[serde(default)] + pub network: OptionalField, + /// Optional cross-platform user-interface policy. + #[serde(default)] + pub ui: OptionalField, + /// Optional ProcessContainer settings. + /// The legacy `appContainer` spelling is accepted as an alias. + #[serde(alias = "appContainer", default)] + pub process_container: OptionalField, + /// Optional LXC distribution settings. + #[serde(default)] + pub lxc: OptionalField, + /// Optional macOS Seatbelt configuration. + #[serde(alias = "macos_sandbox", default)] + pub seatbelt: OptionalField, + /// Optional experimental settings. + #[serde(default)] + pub experimental: OptionalField, +} diff --git a/src/core/mxc_config_contract/src/dev/primitives.rs b/src/core/mxc_config_contract/src/dev/primitives.rs new file mode 100644 index 000000000..148e63d20 --- /dev/null +++ b/src/core/mxc_config_contract/src/dev/primitives.rs @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use serde::{de, Deserialize, Deserializer}; + +/// A marker that deserializes only from the JSON value `true`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct True; + +impl<'de> Deserialize<'de> for True { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + if bool::deserialize(deserializer)? { + Ok(Self) + } else { + Err(de::Error::custom("expected true")) + } + } +} + +/// An optional JSON field that distinguishes omission from explicit `null`. +/// +/// A missing field becomes an empty `OptionalField` through Serde's `default` +/// handling. A present field must deserialize as `T`; explicit `null` is +/// therefore rejected unless `T` itself accepts it. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct OptionalField(Option); + +impl Default for OptionalField { + fn default() -> Self { + Self(None) + } +} + +impl OptionalField { + /// Returns a shared reference to the value when the field was present. + pub fn as_ref(&self) -> Option<&T> { + self.0.as_ref() + } + + /// Converts the field into its optional value. + pub fn into_option(self) -> Option { + self.0 + } +} + +impl<'de, T> Deserialize<'de> for OptionalField +where + T: Deserialize<'de>, +{ + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + T::deserialize(deserializer).map(|value| Self(Some(value))) + } +} + +/// A non-empty string. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct NonEmptyString(String); + +impl NonEmptyString { + /// Creates a validated string. + /// + /// Returns an error when `value` is empty. + pub fn new(value: String) -> Result { + if value.is_empty() { + Err("string must not be empty".to_string()) + } else { + Ok(Self(value)) + } + } + + /// Returns the validated string as a borrowed string slice. + pub fn as_str(&self) -> &str { + &self.0 + } + + /// Consumes the wrapper and returns the validated string. + pub fn into_inner(self) -> String { + self.0 + } +} + +impl<'de> Deserialize<'de> for NonEmptyString { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let value = String::deserialize(deserializer)?; + Self::new(value).map_err(de::Error::custom) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn non_empty_string_accepts_valid() { + let value = NonEmptyString::new("abc".to_string()).unwrap(); + assert_eq!(value.as_str(), "abc"); + } + + #[test] + fn non_empty_string_rejects_empty() { + let error = NonEmptyString::new(String::new()).unwrap_err(); + assert_eq!(error, "string must not be empty"); + } +} diff --git a/src/core/mxc_config_contract/src/dev/stable.rs b/src/core/mxc_config_contract/src/dev/stable.rs new file mode 100644 index 000000000..3ab5b8bc8 --- /dev/null +++ b/src/core/mxc_config_contract/src/dev/stable.rs @@ -0,0 +1,218 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use super::primitives::{NonEmptyString, OptionalField}; + +/// Container lifecycle settings. +#[derive(Debug, serde::Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct Lifecycle { + /// Whether to destroy the container when execution ends. + #[serde(default)] + pub destroy_on_exit: OptionalField, + /// Whether to preserve applied policy after execution ends. + #[serde(default)] + pub preserve_policy: OptionalField, +} + +/// Process execution settings. +#[derive(Debug, serde::Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct Process { + /// The non-empty command line to execute. + pub command_line: NonEmptyString, + /// Optional working directory. + #[serde(default)] + pub cwd: OptionalField, + /// Optional environment entries encoded as `KEY=VALUE` strings. + #[serde(default)] + pub env: OptionalField>, + /// Optional execution timeout in milliseconds. + #[serde(default)] + pub timeout: OptionalField, +} + +/// Filesystem access policy. +#[derive(Debug, serde::Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct Filesystem { + /// Optional paths granted read-write access. + #[serde(default)] + pub readwrite_paths: OptionalField>, + /// Optional paths granted read-only access. + #[serde(default)] + pub readonly_paths: OptionalField>, + /// Optional paths denied access. + #[serde(default)] + pub denied_paths: OptionalField>, +} + +/// Operator consent for containment fallback behavior. +#[derive(Debug, serde::Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct Fallback { + /// Whether the runtime may mutate host filesystem DACLs as a fallback. + #[serde(default)] + pub allow_dacl_mutation: OptionalField, +} + +#[rustfmt::skip] +string_enum! { +/// Clipboard access granted to the contained process. +#[derive(Debug)] +pub enum UiClipboard { + /// Deny clipboard reads and writes. + None => ["none"], + /// Allow clipboard reads. + Read => ["read"], + /// Allow clipboard writes. + Write => ["write"], + /// Allow clipboard reads and writes. + All => ["all"], +} +} + +/// Cross-platform user-interface policy. +#[derive(Debug, serde::Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct Ui { + /// Whether visible user interface is disabled. + #[serde(default)] + pub disable: OptionalField, + /// Optional clipboard access level. + #[serde(default)] + pub clipboard: OptionalField, + /// Whether keyboard and mouse input injection is allowed. + #[serde(default)] + pub injection: OptionalField, +} + +#[rustfmt::skip] +string_enum! { +/// Isolation level for ProcessContainer desktop resources. +#[derive(Debug)] +pub enum ProcessContainerUiIsolation { + /// Isolate the complete container user-interface environment. + Container => ["container"], + /// Isolate desktop resources. + Desktop => ["desktop"], + /// Isolate user-interface handles. + Handles => ["handles"], + /// Isolate user-interface atoms. + Atoms => ["atoms"], +} +} + +/// ProcessContainer-specific user-interface policy. +#[derive(Debug, serde::Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ProcessContainerUi { + /// Optional desktop-resource isolation level. + #[serde(default)] + pub isolation: OptionalField, + /// Whether desktop system control is allowed. + #[serde(default)] + pub desktop_system_control: OptionalField, + /// Optional system-settings access level. + #[serde(default)] + pub system_settings: OptionalField, + /// Whether Input Method Editor access is allowed. + #[serde(default)] + pub ime: OptionalField, +} + +#[rustfmt::skip] +string_enum! { +/// Mode for capture denials +#[derive(Debug)] +pub enum CaptureDenialsMode { + /// Access stays **denied** and the denial is recorded. Deny-by-default containment is preserved; this is the safe default. + Block => ["block"], + /// Access is **allowed** and recorded (audit mode). This relaxes deny-by-default for the run, so it is a security-sensitive choice and the runner emits a security warning. + Allow => ["allow"], +} +} + +/// Windows denial-capture settings. +#[derive(Debug, serde::Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct CaptureDenials { + /// How each ungranted access is handled while it is recorded. + #[serde(default)] + pub mode: OptionalField, + /// Optional destination for the generated denial report. + #[serde(default)] + pub output_path: OptionalField, + /// Whether to retain the sealed ETL trace after analysis. Retained traces + /// can contain sensitive resource paths and identifiers; callers are + /// responsible for deleting them. + #[serde(default)] + pub retain_etl: OptionalField, +} + +/// ProcessContainer-specific settings. +#[derive(Debug, serde::Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ProcessContainer { + /// Whether least-privilege mode is enabled. + #[serde(default)] + pub least_privilege: OptionalField, + /// Optional learning-mode (deny-and-record) + #[serde(default)] + pub learning_mode: OptionalField, + /// Optional AppContainer capability names. + #[serde(default)] + pub capabilities: OptionalField>, + /// Optional capture-denials policy. + #[serde(default)] + pub capture_denials: OptionalField, + /// Optional ProcessContainer-specific user-interface policy. + #[serde(default)] + pub ui: OptionalField, +} + +/// Linux LXC distribution settings. +#[derive(Debug, serde::Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct Lxc { + /// The Linux distribution name. + pub distribution: String, + /// The distribution release. + pub release: String, +} + +#[rustfmt::skip] +string_enum! { +/// Launch method for macOS Seatbelt config. +#[derive(Debug)] +pub enum LaunchMethod { + /// Launch the contained process directly through `exec`. + Exec => ["exec"], + /// Launch the contained application through macOS LaunchServices. + Open => ["open"], +} +} + +/// macOS Seatbelt backend settings. +#[derive(Debug, serde::Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct Seatbelt { + /// Optional override of the generated sandbox profile. + #[serde(default)] + pub profile_override: OptionalField, + /// Whether GUI application access is allowed. + #[serde(default)] + pub gui_access: OptionalField, + /// Optional method used to launch the contained process. + #[serde(default)] + pub launch_method: OptionalField, + /// Whether the contained process may allocate nested pseudo-terminals. + #[serde(default)] + pub nested_pty: OptionalField, + /// Whether macOS Keychain access is allowed. + #[serde(default)] + pub keychain_access: OptionalField, + /// Additional Mach service global names the process may resolve. + #[serde(default)] + pub extra_mach_lookups: OptionalField>, +} diff --git a/src/core/mxc_config_contract/src/lib.rs b/src/core/mxc_config_contract/src/lib.rs index 264621f2a..b427342f9 100644 --- a/src/core/mxc_config_contract/src/lib.rs +++ b/src/core/mxc_config_contract/src/lib.rs @@ -19,6 +19,7 @@ mod registry; mod version; +pub mod dev; pub mod published; pub use registry::{descriptor, supported_versions, ContractDescriptor, ContractStatus, CONTRACTS}; diff --git a/src/core/mxc_config_contract/tests/v0_6_0_alpha/root.rs b/src/core/mxc_config_contract/tests/v0_6_0_alpha/root.rs index 1b8358e71..399d417fe 100644 --- a/src/core/mxc_config_contract/tests/v0_6_0_alpha/root.rs +++ b/src/core/mxc_config_contract/tests/v0_6_0_alpha/root.rs @@ -485,13 +485,11 @@ fn rejects_lxc_null_release() { // Experimental tests #[test] -fn rejects_experimental_field() { +fn rejects_experimental_section() { let json = r#"{ "version": "0.6.0-alpha", "process": {"commandLine": "echo"}, - "experimental": { - "someField": true - } + "experimental": {} }"#; assert_invalid(json); @@ -501,7 +499,7 @@ fn rejects_experimental_field() { #[test] fn rejects_state_aware_fields() { for field in [ - r#""phase": "somePhase""#, + r#""phase": "exec""#, r#""sandboxId": "someId""#, r#""correlationVector": "someVector""#, ] { diff --git a/src/core/mxc_config_contract/tests/v0_7_0_alpha/root.rs b/src/core/mxc_config_contract/tests/v0_7_0_alpha/root.rs index fafac1c4f..7f6f9a847 100644 --- a/src/core/mxc_config_contract/tests/v0_7_0_alpha/root.rs +++ b/src/core/mxc_config_contract/tests/v0_7_0_alpha/root.rs @@ -488,13 +488,11 @@ fn rejects_lxc_null_release() { // Experimental tests #[test] -fn rejects_experimental_field() { +fn rejects_experimental_section() { let json = r#"{ "version": "0.7.0-alpha", "process": {"commandLine": "echo"}, - "experimental": { - "someField": true - } + "experimental": {} }"#; assert_invalid(json); @@ -504,7 +502,7 @@ fn rejects_experimental_field() { #[test] fn rejects_state_aware_fields() { for field in [ - r#""phase": "somePhase""#, + r#""phase": "exec""#, r#""sandboxId": "someId""#, r#""correlationVector": "someVector""#, ] { diff --git a/src/core/mxc_config_contract/tests/v0_8_0_alpha.rs b/src/core/mxc_config_contract/tests/v0_8_0_alpha.rs new file mode 100644 index 000000000..82a7e8067 --- /dev/null +++ b/src/core/mxc_config_contract/tests/v0_8_0_alpha.rs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#[path = "v0_8_0_alpha/annotations.rs"] +mod annotations; +#[path = "v0_8_0_alpha/common.rs"] +mod common; +#[path = "v0_8_0_alpha/enums.rs"] +mod enums; +#[path = "v0_8_0_alpha/experimental.rs"] +mod experimental; +#[path = "v0_8_0_alpha/fixtures.rs"] +mod fixtures; +#[path = "v0_8_0_alpha/network.rs"] +mod network; +#[path = "v0_8_0_alpha/one_shot.rs"] +mod one_shot; +#[path = "v0_8_0_alpha/optional_fields.rs"] +mod optional_fields; +#[path = "v0_8_0_alpha/seatbelt.rs"] +mod seatbelt; diff --git a/src/core/mxc_config_contract/tests/v0_8_0_alpha/annotations.rs b/src/core/mxc_config_contract/tests/v0_8_0_alpha/annotations.rs new file mode 100644 index 000000000..2bc26672f --- /dev/null +++ b/src/core/mxc_config_contract/tests/v0_8_0_alpha/annotations.rs @@ -0,0 +1,128 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::common::{assert_invalid, assert_valid}; + +#[test] +fn accepts_schema_string() { + let json = r#"{ + "$schema": "https://github.com/microsoft/mxc/blob/main/schemas/dev/mxc-config.schema.0.8.0-dev.json", + "version": "0.8.0-alpha", + "process": { + "commandLine": "echo" + } + }"#; + + assert_valid(json); +} + +#[test] +fn accepts_empty_schema_string() { + let json = r#"{ + "$schema": "", + "version": "0.8.0-alpha", + "process": { + "commandLine": "echo" + } + }"#; + + assert_valid(json); +} + +#[test] +fn rejects_non_string_schema_values() { + for schema in ["null", "true", "false", "0", "1", "[]", "{}"] { + let json = format!( + r#"{{ + "$schema": {schema}, + "version": "0.8.0-alpha", + "process": {{"commandLine": "echo"}} + }}"# + ); + + assert_invalid(&json); + } +} + +#[test] +fn accepts_all_json_comment_value_types() { + for comment in ["null", "true", "false", "0", "1", "\"string\"", "[]", "{}"] { + let json = format!( + r#"{{ + "_comment": {comment}, + "version": "0.8.0-alpha", + "process": {{"commandLine": "echo"}} + }}"# + ); + + assert_valid(&json); + } +} + +#[test] +fn accepts_schema_and_comment_together() { + let json = r#"{ + "$schema": "https://github.com/microsoft/mxc/blob/main/schemas/dev/mxc-config.schema.0.8.0-dev.json", + "_comment": "This is a comment", + "version": "0.8.0-alpha", + "process": { + "commandLine": "echo" + } + }"#; + + assert_valid(json); +} + +#[test] +fn rejects_duplicate_schema_field() { + let json = r#"{ + "$schema": "https://github.com/microsoft/mxc/blob/main/schemas/dev/mxc-config.schema.0.8.0-dev.json", + "$schema": "https://github.com/microsoft/mxc/blob/main/schemas/dev/mxc-config.schema.0.8.0-dev.json", + "version": "0.8.0-alpha", + "process": { + "commandLine": "echo" + } + }"#; + + assert_invalid(json); +} + +#[test] +fn rejects_duplicate_comment_field() { + let json = r#"{ + "_comment": "This is a comment", + "_comment": "This is another comment", + "version": "0.8.0-alpha", + "process": { + "commandLine": "echo" + } + }"#; + + assert_invalid(json); +} + +#[test] +fn rejects_unprefixed_schema_field() { + let json = r#"{ + "schema": "https://github.com/microsoft/mxc/blob/main/schemas/dev/mxc-config.schema.0.8.0-dev.json", + "version": "0.8.0-alpha", + "process": { + "commandLine": "echo" + } + }"#; + + assert_invalid(json); +} + +#[test] +fn rejects_unprefixed_comment_field() { + let json = r#"{ + "comment": "This is a comment", + "version": "0.8.0-alpha", + "process": { + "commandLine": "echo" + } + }"#; + + assert_invalid(json); +} diff --git a/src/core/mxc_config_contract/tests/v0_8_0_alpha/common.rs b/src/core/mxc_config_contract/tests/v0_8_0_alpha/common.rs new file mode 100644 index 000000000..1b185f5f3 --- /dev/null +++ b/src/core/mxc_config_contract/tests/v0_8_0_alpha/common.rs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use mxc_config_contract::dev::OneShotRequest; + +pub(crate) fn assert_valid(json: &str) { + serde_json::from_str::(json).unwrap(); +} + +pub(crate) fn assert_invalid(json: &str) { + assert_invalid_with_context(json, "invalid configuration"); +} + +pub(crate) fn assert_invalid_cases<'a>( + cases: impl IntoIterator, + failure_kind: &str, +) { + for (name, required_fields, invalid_fields) in cases { + let json = format!( + r#"{{ + {required_fields}, + {invalid_fields} + }}"# + ); + + assert_invalid_with_context(&json, &format!("{failure_kind} '{name}'")); + } +} + +fn assert_invalid_with_context(json: &str, context: &str) { + if let Err(error) = serde_json::from_str::(json) { + panic!("{context} used malformed test JSON: {error}"); + } + + assert!( + serde_json::from_str::(json).is_err(), + "{context} was accepted" + ); +} diff --git a/src/core/mxc_config_contract/tests/v0_8_0_alpha/enums.rs b/src/core/mxc_config_contract/tests/v0_8_0_alpha/enums.rs new file mode 100644 index 000000000..8258a9946 --- /dev/null +++ b/src/core/mxc_config_contract/tests/v0_8_0_alpha/enums.rs @@ -0,0 +1,316 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::common::{assert_invalid, assert_valid}; + +// Enum value tests +#[test] +fn accepts_every_containment_value() { + for containment in [ + "process", + "processcontainer", + "lxc", + "bubblewrap", + "seatbelt", + "vm", + "windows_sandbox", + "microvm", + "hyperlight", + "isolation_session", + "wslc", + ] { + let json = format!( + r#"{{ + "version": "0.8.0-alpha", + "containment": "{containment}", + "process": {{"commandLine": "echo"}} + }}"# + ); + + assert_valid(&json); + } +} + +#[test] +fn rejects_invalid_containment_value() { + assert_invalid( + r#"{ + "version": "0.8.0-alpha", + "containment": "invalid", + "process": {"commandLine": "echo"} + }"#, + ); +} + +#[test] +fn accepts_every_default_network_policy_value() { + for default_network_policy in ["allow", "block"] { + let json = format!( + r#"{{ + "version": "0.8.0-alpha", + "network": {{ + "defaultPolicy": "{default_network_policy}" + }}, + "process": {{"commandLine": "echo"}} + }}"# + ); + + assert_valid(&json); + } +} + +#[test] +fn rejects_invalid_default_network_policy_value() { + assert_invalid( + r#"{ + "version": "0.8.0-alpha", + "network": { + "defaultPolicy": "invalid" + }, + "process": {"commandLine": "echo"} + }"#, + ); +} + +#[test] +fn accepts_every_network_enforcement_mode_value() { + for network_enforcement_mode in ["capabilities", "firewall", "both"] { + let json = format!( + r#"{{ + "version": "0.8.0-alpha", + "network": {{ + "enforcementMode": "{network_enforcement_mode}" + }}, + "process": {{"commandLine": "echo"}} + }}"# + ); + + assert_valid(&json); + } +} + +#[test] +fn rejects_invalid_network_enforcement_mode_value() { + assert_invalid( + r#"{ + "version": "0.8.0-alpha", + "network": { + "enforcementMode": "invalid" + }, + "process": {"commandLine": "echo"} + }"#, + ); +} + +#[test] +fn accepts_every_ui_clipboard_value() { + for ui_clipboard in ["none", "read", "write", "all"] { + let json = format!( + r#"{{ + "version": "0.8.0-alpha", + "ui": {{ + "clipboard": "{ui_clipboard}" + }}, + "process": {{"commandLine": "echo"}} + }}"# + ); + + assert_valid(&json); + } +} + +#[test] +fn rejects_invalid_ui_clipboard_value() { + assert_invalid( + r#"{ + "version": "0.8.0-alpha", + "ui": { + "clipboard": "invalid" + }, + "process": {"commandLine": "echo"} + }"#, + ); +} + +#[test] +fn accepts_every_process_container_ui_isolation_value() { + for process_container_ui_isolation in ["container", "desktop", "handles", "atoms"] { + let json = format!( + r#"{{ + "version": "0.8.0-alpha", + "processContainer": {{ + "ui": {{ + "isolation": "{process_container_ui_isolation}" + }} + }}, + "process": {{"commandLine": "echo"}} + }}"# + ); + + assert_valid(&json); + } +} + +#[test] +fn rejects_invalid_process_container_ui_isolation_value() { + assert_invalid( + r#"{ + "version": "0.8.0-alpha", + "processContainer": { + "ui": { + "isolation": "invalid" + } + }, + "process": {"commandLine": "echo"} + }"#, + ); +} + +#[test] +fn accepts_every_capture_denials_mode_value() { + for capture_denials_mode in ["allow", "block"] { + let json = format!( + r#"{{ + "version": "0.8.0-alpha", + "processContainer": {{ + "captureDenials": {{ + "mode": "{capture_denials_mode}", + "outputPath": "c:\\temp\\denials.log" + }} + }}, + "process": {{"commandLine": "echo"}} + }}"# + ); + + assert_valid(&json); + } +} + +#[test] +fn rejects_invalid_capture_denials_mode_value() { + assert_invalid( + r#"{ + "version": "0.8.0-alpha", + "processContainer": { + "captureDenials": { + "mode": "invalid", + "outputPath": "c:\\temp\\denials.log" + } + }, + "process": {"commandLine": "echo"} + }"#, + ); +} + +#[test] +fn rejects_non_string_capture_denials_mode_value() { + for capture_denials_mode in ["123", "true", "[]", "{}"] { + let json = format!( + r#"{{ + "version": "0.8.0-alpha", + "processContainer": {{ + "captureDenials": {{ + "mode": {capture_denials_mode}, + "outputPath": "c:\\temp\\denials.log" + }} + }}, + "process": {{"commandLine": "echo"}} + }}"# + ); + + assert_invalid(&json); + } +} + +#[test] +fn rejects_object_encoding_for_fieldless_enums() { + let cases = [ + r#"{ + "version": {"0.8.0-alpha": null}, + "process": {"commandLine": "echo"} + }"#, + r#"{ + "version": "0.8.0-alpha", + "containment": {"process": null}, + "process": {"commandLine": "echo"} + }"#, + r#"{ + "version": "0.8.0-alpha", + "network": {"defaultPolicy": {"allow": null}}, + "process": {"commandLine": "echo"} + }"#, + r#"{ + "version": "0.8.0-alpha", + "network": {"enforcementMode": {"both": null}}, + "process": {"commandLine": "echo"} + }"#, + r#"{ + "version": "0.8.0-alpha", + "ui": {"clipboard": {"all": null}}, + "process": {"commandLine": "echo"} + }"#, + r#"{ + "version": "0.8.0-alpha", + "processContainer": { + "ui": {"isolation": {"desktop": null}} + }, + "process": {"commandLine": "echo"} + }"#, + r#"{ + "version": "0.8.0-alpha", + "processContainer": { + "captureDenials": {"mode": {"block": null}} + }, + "process": {"commandLine": "echo"} + }"#, + r#"{ + "version": "0.8.0-alpha", + "seatbelt": { + "launchMethod": {"exec": null} + }, + "process": {"commandLine": "echo"} + }"#, + ]; + + for case in cases { + assert_invalid(case); + } +} + +use mxc_config_contract::dev::{OneShotContainment, OneShotRequest}; + +#[test] +fn appcontainer_containment_value_alias_maps_to_process_container() { + let json = r#"{ + "version": "0.8.0-alpha", + "containment": "appcontainer", + "process": { + "commandLine": "echo" + } + }"#; + + let request: OneShotRequest = serde_json::from_str(json).unwrap(); + + assert!(matches!( + request.containment.as_ref(), + Some(OneShotContainment::ProcessContainer) + )); +} + +#[test] +fn macos_sandbox_containment_value_alias_maps_to_seatbelt() { + let json = r#"{ + "version": "0.8.0-alpha", + "containment": "macos_sandbox", + "process": { + "commandLine": "echo" + } + }"#; + + let request: OneShotRequest = serde_json::from_str(json).unwrap(); + + assert!(matches!( + request.containment.as_ref(), + Some(OneShotContainment::Seatbelt) + )); +} diff --git a/src/core/mxc_config_contract/tests/v0_8_0_alpha/experimental.rs b/src/core/mxc_config_contract/tests/v0_8_0_alpha/experimental.rs new file mode 100644 index 000000000..1d8719bd3 --- /dev/null +++ b/src/core/mxc_config_contract/tests/v0_8_0_alpha/experimental.rs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#[path = "experimental/root.rs"] +mod root; +#[path = "experimental/test_and_telemetry.rs"] +mod test_and_telemetry; +#[path = "experimental/windows_sandbox.rs"] +mod windows_sandbox; +#[path = "experimental/wslc.rs"] +mod wslc; diff --git a/src/core/mxc_config_contract/tests/v0_8_0_alpha/experimental/root.rs b/src/core/mxc_config_contract/tests/v0_8_0_alpha/experimental/root.rs new file mode 100644 index 000000000..6046085da --- /dev/null +++ b/src/core/mxc_config_contract/tests/v0_8_0_alpha/experimental/root.rs @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::common::{assert_invalid, assert_valid}; + +#[test] +fn accepts_experimental_section() { + let json = r#"{ + "version": "0.8.0-alpha", + "process": {"commandLine": "echo"}, + "experimental": { + "test": {"message": "this is a message"} + } + }"#; + + assert_valid(json); +} + +#[test] +fn rejects_unknown_experimental_field() { + let json = r#"{ + "version": "0.8.0-alpha", + "process": {"commandLine": "echo"}, + "experimental": { + "unknown": "value" + } + }"#; + + assert_invalid(json); +} + +#[test] +fn rejects_null_experimental_section() { + let json = r#"{ + "version": "0.8.0-alpha", + "process": {"commandLine": "echo"}, + "experimental": null + }"#; + + assert_invalid(json); +} + +#[test] +fn rejects_duplicate_experimental_section() { + let json = r#"{ + "version": "0.8.0-alpha", + "process": {"commandLine": "echo"}, + "experimental": { + "test": {"message": "this is a message"} + }, + "experimental": { + "test": {"message": "this is another message"} + } + }"#; + + assert_invalid(json); +} + +#[test] +fn rejects_moved_experimental_seatbelt_sections() { + for field in [r#""seatbelt": {}"#, r#""macos_sandbox": {}"#] { + let json = format!( + r#"{{ + "version": "0.8.0-alpha", + "process": {{"commandLine": "echo"}}, + "experimental": {{{field}}} + }}"# + ); + + assert_invalid(&json); + } +} + +#[test] +fn rejects_state_aware_experimental_sections() { + for field in [ + r#""isolation_session": {"provision": {}}"#, + r#""wslc": {"provision": {}}"#, + ] { + let json = format!( + r#"{{ + "version": "0.8.0-alpha", + "process": {{"commandLine": "echo"}}, + "experimental": {{{field}}} + }}"# + ); + + assert_invalid(&json); + } +} diff --git a/src/core/mxc_config_contract/tests/v0_8_0_alpha/experimental/test_and_telemetry.rs b/src/core/mxc_config_contract/tests/v0_8_0_alpha/experimental/test_and_telemetry.rs new file mode 100644 index 000000000..5ae2ac250 --- /dev/null +++ b/src/core/mxc_config_contract/tests/v0_8_0_alpha/experimental/test_and_telemetry.rs @@ -0,0 +1,174 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::common::{assert_invalid, assert_invalid_cases, assert_valid}; + +#[test] +fn accepts_empty_test_object() { + let json = r#"{ + "version": "0.8.0-alpha", + "experimental": {"test": {}}, + "process": {"commandLine": "echo"} + }"#; + + assert_valid(json); +} + +#[test] +fn accepts_test_message_values() { + for message in ["", "Hello, world!", "This is a test message."] { + let json = format!( + r#"{{ + "version": "0.8.0-alpha", + "experimental": {{ + "test": {{ + "message": "{message}" + }} + }}, + "process": {{"commandLine": "echo"}} + }}"# + ); + + assert_valid(&json); + } +} + +#[test] +fn rejects_non_string_test_message_values() { + for message in ["null", "true", "false", "123", "[]", "{}"] { + let json = format!( + r#"{{ + "version": "0.8.0-alpha", + "experimental": {{ + "test": {{ + "message": {message} + }} + }}, + "process": {{"commandLine": "echo"}} + }}"# + ); + + assert_invalid(&json); + } +} + +#[test] +fn rejects_unknown_test_field() { + let json = r#"{ + "version": "0.8.0-alpha", + "experimental": { + "test": { + "unknownField": "value" + } + }, + "process": {"commandLine": "echo"} + }"#; + + assert_invalid(json); +} + +#[test] +fn rejects_duplicate_test_fields() { + let version_and_process = r#""version": "0.8.0-alpha", "process": {"commandLine": "echo"}"#; + + assert_invalid_cases( + [ + ( + "experimental.test", + version_and_process, + r#""experimental": {"test": {}, "test": {}}"#, + ), + ( + "experimental.test.message", + version_and_process, + r#""experimental": {"test": {"message": "First", "message": "Second"}}"#, + ), + ], + "duplicate experimental field", + ); +} + +#[test] +fn accepts_empty_telemetry_object() { + let json = r#"{ + "version": "0.8.0-alpha", + "experimental": {"telemetry": {}}, + "process": {"commandLine": "echo"} + }"#; + + assert_valid(json); +} + +#[test] +fn accepts_telemetry_enabled_values() { + for enabled in ["true", "false"] { + let json = format!( + r#"{{ + "version": "0.8.0-alpha", + "experimental": {{ + "telemetry": {{ + "enabled": {enabled} + }} + }}, + "process": {{"commandLine": "echo"}} + }}"# + ); + + assert_valid(&json); + } +} + +#[test] +fn rejects_non_boolean_telemetry_enabled_values() { + for enabled in ["null", "123", "\"true\"", "\"false\"", "[]", "{}"] { + let json = format!( + r#"{{ + "version": "0.8.0-alpha", + "experimental": {{ + "telemetry": {{ + "enabled": {enabled} + }} + }}, + "process": {{"commandLine": "echo"}} + }}"# + ); + + assert_invalid(&json); + } +} + +#[test] +fn rejects_unknown_telemetry_field() { + let json = r#"{ + "version": "0.8.0-alpha", + "experimental": { + "telemetry": { + "unknownField": "value" + } + }, + "process": {"commandLine": "echo"} + }"#; + + assert_invalid(json); +} + +#[test] +fn rejects_duplicate_telemetry_fields() { + let version_and_process = r#""version": "0.8.0-alpha", "process": {"commandLine": "echo"}"#; + + assert_invalid_cases( + [ + ( + "experimental.telemetry", + version_and_process, + r#""experimental": {"telemetry": {}, "telemetry": {}}"#, + ), + ( + "experimental.telemetry.enabled", + version_and_process, + r#""experimental": {"telemetry": {"enabled": true, "enabled": false}}"#, + ), + ], + "duplicate experimental field", + ); +} diff --git a/src/core/mxc_config_contract/tests/v0_8_0_alpha/experimental/windows_sandbox.rs b/src/core/mxc_config_contract/tests/v0_8_0_alpha/experimental/windows_sandbox.rs new file mode 100644 index 000000000..afd563861 --- /dev/null +++ b/src/core/mxc_config_contract/tests/v0_8_0_alpha/experimental/windows_sandbox.rs @@ -0,0 +1,118 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::common::{assert_invalid, assert_invalid_cases, assert_valid}; + +fn windows_sandbox_request(fields: &str) -> String { + format!( + r#"{{ + "version": "0.8.0-alpha", + "experimental": {{ + "windows_sandbox": {{{fields}}} + }}, + "process": {{"commandLine": "echo"}} + }}"# + ) +} + +#[test] +fn accepts_empty_windows_sandbox_object() { + assert_valid(&windows_sandbox_request("")); +} + +#[test] +fn accepts_windows_sandbox_fields() { + for fields in [ + r#""idleTimeoutMs": 1000"#, + r#""idleTimeout": 2000"#, + r#""daemonPipeName": "test_pipe""#, + r#""idleTimeoutMs": 1000, "idleTimeout": 2000"#, + ] { + assert_valid(&windows_sandbox_request(fields)); + } +} + +#[test] +fn rejects_incorrect_windows_sandbox_spellings() { + let wrong_outer_name = r#"{ + "version": "0.8.0-alpha", + "experimental": {"windowsSandbox": {}}, + "process": {"commandLine": "echo"} + }"#; + + assert_invalid(wrong_outer_name); + assert_invalid(&windows_sandbox_request(r#""idle_timeout": 1000"#)); +} + +#[test] +fn accepts_windows_sandbox_timeout_boundaries() { + for field in ["idleTimeoutMs", "idleTimeout"] { + for value in [0, u32::MAX] { + assert_valid(&windows_sandbox_request(&format!(r#""{field}": {value}"#))); + } + } +} + +#[test] +fn rejects_invalid_windows_sandbox_timeout_values() { + for field in ["idleTimeoutMs", "idleTimeout"] { + for value in ["-1", "1.5", "4294967296", r#""1000""#, "true", "[]", "{}"] { + assert_invalid(&windows_sandbox_request(&format!(r#""{field}": {value}"#))); + } + } +} + +#[test] +fn accepts_windows_sandbox_daemon_pipe_name_values() { + for pipe_name in ["", r"\\.\pipe\wxc-test"] { + let pipe_name_json = serde_json::to_string(pipe_name).unwrap(); + assert_valid(&windows_sandbox_request(&format!( + r#""daemonPipeName": {pipe_name_json}"# + ))); + } +} + +#[test] +fn rejects_non_string_windows_sandbox_daemon_pipe_name_values() { + for value in ["123", "true", "[]", "{}"] { + assert_invalid(&windows_sandbox_request(&format!( + r#""daemonPipeName": {value}"# + ))); + } +} + +#[test] +fn rejects_unknown_windows_sandbox_field() { + assert_invalid(&windows_sandbox_request(r#""unknownField": "value""#)); +} + +#[test] +fn rejects_duplicate_windows_sandbox_fields() { + let version_and_process = r#""version": "0.8.0-alpha", "process": {"commandLine": "echo"}"#; + + assert_invalid_cases( + [ + ( + "experimental.windows_sandbox", + version_and_process, + r#""experimental": {"windows_sandbox": {}, "windows_sandbox": {}}"#, + ), + ( + "experimental.windows_sandbox.idleTimeoutMs", + version_and_process, + r#""experimental": {"windows_sandbox": {"idleTimeoutMs": 1000, "idleTimeoutMs": 2000}}"#, + ), + ( + "experimental.windows_sandbox.idleTimeout", + version_and_process, + r#""experimental": {"windows_sandbox": {"idleTimeout": 1000, "idleTimeout": 2000}}"#, + ), + ( + "experimental.windows_sandbox.daemonPipeName", + version_and_process, + r#""experimental": {"windows_sandbox": {"daemonPipeName": "pipe1", "daemonPipeName": "pipe2"}}"#, + ), + ], + "duplicate experimental field", + ); +} diff --git a/src/core/mxc_config_contract/tests/v0_8_0_alpha/experimental/wslc.rs b/src/core/mxc_config_contract/tests/v0_8_0_alpha/experimental/wslc.rs new file mode 100644 index 000000000..a3de2255c --- /dev/null +++ b/src/core/mxc_config_contract/tests/v0_8_0_alpha/experimental/wslc.rs @@ -0,0 +1,264 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::common::{assert_invalid, assert_invalid_cases, assert_valid}; + +fn wslc_request(fields: &str) -> String { + format!( + r#"{{ + "version": "0.8.0-alpha", + "experimental": {{ + "wslc": {{{fields}}} + }}, + "process": {{"commandLine": "echo"}} + }}"# + ) +} + +#[test] +fn accepts_empty_wslc_object() { + assert_valid(&wslc_request("")); +} + +#[test] +fn accepts_wslc_string_field_values() { + for field in ["targetOs", "image", "imageTarPath", "storagePath"] { + for value in ["", "value"] { + let value_json = serde_json::to_string(value).unwrap(); + assert_valid(&wslc_request(&format!(r#""{field}": {value_json}"#))); + } + } +} + +#[test] +fn rejects_non_string_wslc_string_field_values() { + for field in ["targetOs", "image", "imageTarPath", "storagePath"] { + for value in ["123", "true", "[]", "{}"] { + assert_invalid(&wslc_request(&format!(r#""{field}": {value}"#))); + } + } +} + +#[test] +fn accepts_wslc_integer_boundaries() { + for fields in [ + r#""cpuCount": 0"#, + r#""cpuCount": 4294967295"#, + r#""memoryMb": 0"#, + r#""memoryMb": 4294967296"#, + r#""memoryMb": 18446744073709551615"#, + ] { + assert_valid(&wslc_request(fields)); + } +} + +#[test] +fn rejects_invalid_wslc_integer_values() { + for field in ["cpuCount", "memoryMb"] { + for value in ["-1", "1.5", r#""1024""#, "true", "[]", "{}"] { + assert_invalid(&wslc_request(&format!(r#""{field}": {value}"#))); + } + } + + assert_invalid(&wslc_request(r#""cpuCount": 4294967296"#)); + assert_invalid(&wslc_request(r#""memoryMb": 18446744073709551616"#)); +} + +#[test] +fn accepts_wslc_gpu_values() { + for value in ["true", "false"] { + assert_valid(&wslc_request(&format!(r#""gpu": {value}"#))); + } +} + +#[test] +fn rejects_non_boolean_wslc_gpu_values() { + for value in [r#""true""#, "123", "[]", "{}"] { + assert_invalid(&wslc_request(&format!(r#""gpu": {value}"#))); + } +} + +#[test] +fn rejects_unknown_wslc_field() { + assert_invalid(&wslc_request(r#""unknownField": "value""#)); +} + +#[test] +fn rejects_duplicate_wslc_fields() { + let version_and_process = r#""version": "0.8.0-alpha", "process": {"commandLine": "echo"}"#; + + assert_invalid_cases( + [ + ( + "experimental.wslc", + version_and_process, + r#""experimental": {"wslc": {}, "wslc": {}}"#, + ), + ( + "experimental.wslc.targetOs", + version_and_process, + r#""experimental": {"wslc": {"targetOs": "linux", "targetOs": "other"}}"#, + ), + ( + "experimental.wslc.image", + version_and_process, + r#""experimental": {"wslc": {"image": "first", "image": "second"}}"#, + ), + ( + "experimental.wslc.imageTarPath", + version_and_process, + r#""experimental": {"wslc": {"imageTarPath": "first", "imageTarPath": "second"}}"#, + ), + ( + "experimental.wslc.cpuCount", + version_and_process, + r#""experimental": {"wslc": {"cpuCount": 1, "cpuCount": 2}}"#, + ), + ( + "experimental.wslc.memoryMb", + version_and_process, + r#""experimental": {"wslc": {"memoryMb": 1024, "memoryMb": 2048}}"#, + ), + ( + "experimental.wslc.gpu", + version_and_process, + r#""experimental": {"wslc": {"gpu": true, "gpu": false}}"#, + ), + ( + "experimental.wslc.storagePath", + version_and_process, + r#""experimental": {"wslc": {"storagePath": "first", "storagePath": "second"}}"#, + ), + ], + "duplicate experimental field", + ); +} + +const VALID_PORT_MAPPING: &str = r#"{"windowsPort": 8080, "containerPort": 80}"#; + +fn wslc_port_mappings_request(mappings: &str) -> String { + wslc_request(&format!(r#""portMappings": {mappings}"#)) +} + +#[test] +fn accepts_empty_wslc_port_mappings() { + assert_valid(&wslc_port_mappings_request("[]")); +} + +#[test] +fn accepts_wslc_port_mappings() { + for mappings in [ + format!("[{VALID_PORT_MAPPING}]"), + r#"[{"windowsPort": 8080, "containerPort": 80, "protocol": "tcp"}]"#.to_string(), + r#"[{"windowsPort": 8080, "containerPort": 80}, {"windowsPort": 8443, "containerPort": 443}]"# + .to_string(), + ] { + assert_valid(&wslc_port_mappings_request(&mappings)); + } +} + +#[test] +fn accepts_wslc_port_boundaries() { + for mappings in [ + r#"[{"windowsPort": 1, "containerPort": 65535}]"#, + r#"[{"windowsPort": 65535, "containerPort": 1}]"#, + ] { + assert_valid(&wslc_port_mappings_request(mappings)); + } +} + +#[test] +fn rejects_invalid_wslc_port_values() { + for field in ["windowsPort", "containerPort"] { + for value in ["0", "-1", "1.5", "65536", r#""80""#, "true", "[]", "{}"] { + let other_field = if field == "windowsPort" { + r#""containerPort": 80"# + } else { + r#""windowsPort": 8080"# + }; + let mappings = format!(r#"[{{"{field}": {value}, {other_field}}}]"#); + assert_invalid(&wslc_port_mappings_request(&mappings)); + } + } +} + +#[test] +fn rejects_missing_wslc_port_mapping_fields() { + for mappings in [ + r#"[{"containerPort": 80}]"#, + r#"[{"windowsPort": 8080}]"#, + r#"[{"windowsPort": null, "containerPort": 80}]"#, + r#"[{"windowsPort": 8080, "containerPort": null}]"#, + ] { + assert_invalid(&wslc_port_mappings_request(mappings)); + } +} + +#[test] +fn rejects_invalid_wslc_port_mappings_shape() { + for mappings in [ + "{}", + r#""mapping""#, + "123", + "true", + "[123]", + r#"["mapping"]"#, + "[[]]", + ] { + assert_invalid(&wslc_port_mappings_request(mappings)); + } +} + +#[test] +fn rejects_invalid_wslc_port_mapping_protocol() { + for protocol in [ + r#""udp""#, + r#""invalid""#, + "123", + "true", + "[]", + "{}", + r#"{"tcp": null}"#, + ] { + let mappings = + format!(r#"[{{"windowsPort": 8080, "containerPort": 80, "protocol": {protocol}}}]"#); + assert_invalid(&wslc_port_mappings_request(&mappings)); + } +} + +#[test] +fn rejects_unknown_wslc_port_mapping_field() { + let mappings = r#"[{"windowsPort": 8080, "containerPort": 80, "unknownField": "value"}]"#; + assert_invalid(&wslc_port_mappings_request(mappings)); +} + +#[test] +fn rejects_duplicate_wslc_port_mapping_fields() { + let version_and_process = r#""version": "0.8.0-alpha", "process": {"commandLine": "echo"}"#; + + assert_invalid_cases( + [ + ( + "experimental.wslc.portMappings", + version_and_process, + r#""experimental": {"wslc": {"portMappings": [], "portMappings": []}}"#, + ), + ( + "experimental.wslc.portMappings[].windowsPort", + version_and_process, + r#""experimental": {"wslc": {"portMappings": [{"windowsPort": 8080, "windowsPort": 8081, "containerPort": 80}]}}"#, + ), + ( + "experimental.wslc.portMappings[].containerPort", + version_and_process, + r#""experimental": {"wslc": {"portMappings": [{"windowsPort": 8080, "containerPort": 80, "containerPort": 81}]}}"#, + ), + ( + "experimental.wslc.portMappings[].protocol", + version_and_process, + r#""experimental": {"wslc": {"portMappings": [{"windowsPort": 8080, "containerPort": 80, "protocol": "tcp", "protocol": "tcp"}]}}"#, + ), + ], + "duplicate experimental field", + ); +} diff --git a/src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures.rs b/src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures.rs new file mode 100644 index 000000000..e621f971a --- /dev/null +++ b/src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures.rs @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use mxc_config_contract::dev::OneShotRequest; + +const VALID_FIXTURES: &[(&str, &str)] = &[ + ("minimal", include_str!("fixtures/valid/minimal.json")), + ("complete", include_str!("fixtures/valid/complete.json")), + ( + "experimental field", + include_str!("fixtures/valid/experimental.json"), + ), + ( + "empty optional objects", + include_str!("fixtures/valid/empty_optional_objects.json"), + ), + ( + "appContainer alias", + include_str!("fixtures/valid/app_container_alias.json"), + ), + ( + "localhost proxy", + include_str!("fixtures/valid/proxy_localhost.json"), + ), + ( + "built-in proxy", + include_str!("fixtures/valid/proxy_builtin.json"), + ), + ("URL proxy", include_str!("fixtures/valid/proxy_url.json")), + ( + "schema and comments", + include_str!("fixtures/valid/annotations.json"), + ), + ( + "macos_sandbox alias", + include_str!("fixtures/valid/macos_sandbox_alias.json"), + ), + ( + "seatbelt minimal", + include_str!("fixtures/valid/seatbelt_minimal.json"), + ), + ( + "seatbelt complete", + include_str!("fixtures/valid/seatbelt_complete.json"), + ), +]; + +const INVALID_FIXTURES: &[(&str, &str)] = &[ + ( + "state-aware request", + include_str!("fixtures/invalid/state_aware.json"), + ), + ( + "unknown root field", + include_str!("fixtures/invalid/unknown_root_field.json"), + ), + ( + "unknown nested field", + include_str!("fixtures/invalid/unknown_nested_field.json"), + ), + ( + "incomplete LXC section", + include_str!("fixtures/invalid/incomplete_lxc.json"), + ), + ( + "seatbelt invalid launch method", + include_str!("fixtures/invalid/seatbelt_invalid_launch_method.json"), + ), + ( + "seatbelt unknown field", + include_str!("fixtures/invalid/seatbelt_unknown_field.json"), + ), +]; + +#[test] +fn accepts_valid_fixtures() { + for (name, json) in VALID_FIXTURES { + serde_json::from_str::(json) + .unwrap_or_else(|error| panic!("valid fixture '{name}' was rejected: {error}")); + } +} + +#[test] +fn rejects_invalid_fixtures() { + for (name, json) in INVALID_FIXTURES { + assert!( + serde_json::from_str::(json).is_err(), + "invalid fixture '{name}' was accepted" + ); + } +} diff --git a/src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/invalid/incomplete_lxc.json b/src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/invalid/incomplete_lxc.json new file mode 100644 index 000000000..4531dfb07 --- /dev/null +++ b/src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/invalid/incomplete_lxc.json @@ -0,0 +1,9 @@ +{ + "version": "0.8.0-alpha", + "process": { + "commandLine": "echo lxc" + }, + "lxc": { + "distribution": "ubuntu" + } +} diff --git a/src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/invalid/seatbelt_invalid_launch_method.json b/src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/invalid/seatbelt_invalid_launch_method.json new file mode 100644 index 000000000..b5a79cb9d --- /dev/null +++ b/src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/invalid/seatbelt_invalid_launch_method.json @@ -0,0 +1,10 @@ +{ + "version": "0.8.0-alpha", + "containment": "seatbelt", + "process": { + "commandLine": "echo invalid launch" + }, + "seatbelt": { + "launchMethod": "invalid" + } +} \ No newline at end of file diff --git a/src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/invalid/seatbelt_unknown_field.json b/src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/invalid/seatbelt_unknown_field.json new file mode 100644 index 000000000..963639108 --- /dev/null +++ b/src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/invalid/seatbelt_unknown_field.json @@ -0,0 +1,10 @@ +{ + "version": "0.8.0-alpha", + "containment": "seatbelt", + "process": { + "commandLine": "echo unknown field" + }, + "seatbelt": { + "unknownField": true + } +} \ No newline at end of file diff --git a/src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/invalid/state_aware.json b/src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/invalid/state_aware.json new file mode 100644 index 000000000..90ca797ef --- /dev/null +++ b/src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/invalid/state_aware.json @@ -0,0 +1,8 @@ +{ + "version": "0.8.0-alpha", + "phase": "exec", + "sandboxId": "example:12345678", + "process": { + "commandLine": "echo state-aware" + } +} diff --git a/src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/invalid/unknown_nested_field.json b/src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/invalid/unknown_nested_field.json new file mode 100644 index 000000000..c36a4df9b --- /dev/null +++ b/src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/invalid/unknown_nested_field.json @@ -0,0 +1,9 @@ +{ + "version": "0.8.0-alpha", + "process": { + "commandLine": "echo nested" + }, + "network": { + "unknownField": true + } +} diff --git a/src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/invalid/unknown_root_field.json b/src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/invalid/unknown_root_field.json new file mode 100644 index 000000000..c561ec7cb --- /dev/null +++ b/src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/invalid/unknown_root_field.json @@ -0,0 +1,7 @@ +{ + "version": "0.8.0-alpha", + "process": { + "commandLine": "echo root" + }, + "unknownField": true +} diff --git a/src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/valid/annotations.json b/src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/valid/annotations.json new file mode 100644 index 000000000..a3349d1f0 --- /dev/null +++ b/src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/valid/annotations.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://github.com/microsoft/mxc/blob/main/schemas/dev/mxc-config.schema.0.8.0-dev.json", + "_comment": { + "purpose": "Demonstrate 0.7 authoring annotations" + }, + "version": "0.8.0-alpha", + "process": { + "commandLine": "echo annotations" + } +} \ No newline at end of file diff --git a/src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/valid/app_container_alias.json b/src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/valid/app_container_alias.json new file mode 100644 index 000000000..faeecca90 --- /dev/null +++ b/src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/valid/app_container_alias.json @@ -0,0 +1,12 @@ +{ + "version": "0.8.0-alpha", + "process": { + "commandLine": "echo alias" + }, + "appContainer": { + "leastPrivilege": true, + "capabilities": [ + "internetClient" + ] + } +} diff --git a/src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/valid/complete.json b/src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/valid/complete.json new file mode 100644 index 000000000..abe46dfc5 --- /dev/null +++ b/src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/valid/complete.json @@ -0,0 +1,60 @@ +{ + "version": "0.8.0-alpha", + "containerId": "complete-example", + "containment": "processcontainer", + "lifecycle": { + "destroyOnExit": false, + "preservePolicy": true + }, + "process": { + "commandLine": "echo complete", + "cwd": "/work", + "env": [ + "EXAMPLE=value" + ], + "timeout": 1000 + }, + "filesystem": { + "readwritePaths": [ + "/work" + ], + "readonlyPaths": [ + "/input" + ], + "deniedPaths": [ + "/secret" + ] + }, + "fallback": { + "allowDaclMutation": false + }, + "network": { + "defaultPolicy": "allow", + "enforcementMode": "capabilities", + "blockedHosts": [ + "blocked.example" + ], + "allowLocalNetwork": true, + "proxy": { + "url": "http://proxy.example:8080" + } + }, + "ui": { + "disable": false, + "clipboard": "read", + "injection": true + }, + "processContainer": { + "leastPrivilege": true, + "learningMode": false, + "capabilities": [ + "internetClient" + ], + "ui": { + "isolation": "container", + "desktopSystemControl": false, + "systemSettings": "none", + "ime": false + } + } +} diff --git a/src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/valid/empty_optional_objects.json b/src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/valid/empty_optional_objects.json new file mode 100644 index 000000000..552b1a6d7 --- /dev/null +++ b/src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/valid/empty_optional_objects.json @@ -0,0 +1,14 @@ +{ + "version": "0.8.0-alpha", + "process": { + "commandLine": "echo empty" + }, + "lifecycle": {}, + "filesystem": {}, + "fallback": {}, + "network": {}, + "ui": {}, + "processContainer": { + "ui": {} + } +} diff --git a/src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/valid/experimental.json b/src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/valid/experimental.json new file mode 100644 index 000000000..c2c217812 --- /dev/null +++ b/src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/valid/experimental.json @@ -0,0 +1,9 @@ +{ + "version": "0.8.0-alpha", + "process": { + "commandLine": "echo experimental" + }, + "experimental": { + "test": {} + } +} diff --git a/src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/valid/macos_sandbox_alias.json b/src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/valid/macos_sandbox_alias.json new file mode 100644 index 000000000..df84cbbd8 --- /dev/null +++ b/src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/valid/macos_sandbox_alias.json @@ -0,0 +1,11 @@ +{ + "version": "0.8.0-alpha", + "containment": "macos_sandbox", + "process": { + "commandLine": "echo alias" + }, + "macos_sandbox": { + "guiAccess": true, + "launchMethod": "exec" + } +} \ No newline at end of file diff --git a/src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/valid/minimal.json b/src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/valid/minimal.json new file mode 100644 index 000000000..bda2004d2 --- /dev/null +++ b/src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/valid/minimal.json @@ -0,0 +1,6 @@ +{ + "version": "0.8.0-alpha", + "process": { + "commandLine": "echo hello" + } +} diff --git a/src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/valid/proxy_builtin.json b/src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/valid/proxy_builtin.json new file mode 100644 index 000000000..bcc5b6633 --- /dev/null +++ b/src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/valid/proxy_builtin.json @@ -0,0 +1,11 @@ +{ + "version": "0.8.0-alpha", + "process": { + "commandLine": "echo proxy" + }, + "network": { + "proxy": { + "builtinTestServer": true + } + } +} diff --git a/src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/valid/proxy_localhost.json b/src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/valid/proxy_localhost.json new file mode 100644 index 000000000..e72a98d16 --- /dev/null +++ b/src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/valid/proxy_localhost.json @@ -0,0 +1,11 @@ +{ + "version": "0.8.0-alpha", + "process": { + "commandLine": "echo proxy" + }, + "network": { + "proxy": { + "localhost": 8080 + } + } +} diff --git a/src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/valid/proxy_url.json b/src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/valid/proxy_url.json new file mode 100644 index 000000000..a92abadfc --- /dev/null +++ b/src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/valid/proxy_url.json @@ -0,0 +1,11 @@ +{ + "version": "0.8.0-alpha", + "process": { + "commandLine": "echo proxy" + }, + "network": { + "proxy": { + "url": "http://proxy.example:8080" + } + } +} diff --git a/src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/valid/seatbelt_complete.json b/src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/valid/seatbelt_complete.json new file mode 100644 index 000000000..8f9b34405 --- /dev/null +++ b/src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/valid/seatbelt_complete.json @@ -0,0 +1,18 @@ +{ + "version": "0.8.0-alpha", + "containment": "seatbelt", + "process": { + "commandLine": "echo seatbelt complete" + }, + "seatbelt": { + "profileOverride": "(version 1) (allow default)", + "guiAccess": true, + "launchMethod": "open", + "nestedPty": false, + "keychainAccess": true, + "extraMachLookups": [ + "com.apple.SecurityServer", + "com.apple.coreservices.launchservicesd" + ] + } +} \ No newline at end of file diff --git a/src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/valid/seatbelt_minimal.json b/src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/valid/seatbelt_minimal.json new file mode 100644 index 000000000..1f7842ac9 --- /dev/null +++ b/src/core/mxc_config_contract/tests/v0_8_0_alpha/fixtures/valid/seatbelt_minimal.json @@ -0,0 +1,8 @@ +{ + "version": "0.8.0-alpha", + "containment": "seatbelt", + "process": { + "commandLine": "echo seatbelt" + }, + "seatbelt": {} +} \ No newline at end of file diff --git a/src/core/mxc_config_contract/tests/v0_8_0_alpha/network.rs b/src/core/mxc_config_contract/tests/v0_8_0_alpha/network.rs new file mode 100644 index 000000000..e0e7f8f24 --- /dev/null +++ b/src/core/mxc_config_contract/tests/v0_8_0_alpha/network.rs @@ -0,0 +1,178 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::common::{assert_invalid, assert_valid}; + +// Network proxy tests +#[test] +fn accepts_localhost_proxy_port_boundaries() { + for proxy_port in [1, 65535] { + let json = format!( + r#"{{ + "version": "0.8.0-alpha", + "network": {{ + "proxy": {{ + "localhost": {proxy_port} + }} + }}, + "process": {{"commandLine": "echo"}} + }}"# + ); + + assert_valid(&json); + } +} + +#[test] +fn rejects_localhost_proxy_port_out_of_bounds() { + for proxy_port in [0, 65536] { + let json = format!( + r#"{{ + "version": "0.8.0-alpha", + "network": {{ + "proxy": {{ + "localhost": {proxy_port} + }} + }}, + "process": {{"commandLine": "echo"}} + }}"# + ); + + assert_invalid(&json); + } +} + +#[test] +fn accepts_builtin_test_server_true() { + let json = r#"{ + "version": "0.8.0-alpha", + "network": { + "proxy": { + "builtinTestServer": true + } + }, + "process": {"commandLine": "echo"} + }"#; + + assert_valid(json); +} + +#[test] +fn rejects_builtin_test_server_false() { + let json = r#"{ + "version": "0.8.0-alpha", + "network": { + "proxy": { + "builtinTestServer": false + } + }, + "process": {"commandLine": "echo"} + }"#; + + assert_invalid(json); +} + +#[test] +fn rejects_builtin_test_server_null() { + let json = r#"{ + "version": "0.8.0-alpha", + "network": { + "proxy": { + "builtinTestServer": null + } + }, + "process": {"commandLine": "echo"} + }"#; + + assert_invalid(json); +} + +#[test] +fn rejects_builtin_test_server_non_boolean_values() { + for builtin_test_server in ["0", "1", "\"true\"", "\"false\"", "[]", "{}"] { + let json = format!( + r#"{{ + "version": "0.8.0-alpha", + "network": {{ + "proxy": {{ + "builtinTestServer": {builtin_test_server} + }} + }}, + "process": {{"commandLine": "echo"}} + }}"# + ); + + assert_invalid(&json); + } +} + +#[test] +fn accepts_url_proxy() { + let json = r#"{ + "version": "0.8.0-alpha", + "network": { + "proxy": { + "url": "http://myproxy:8080" + } + }, + "process": {"commandLine": "echo"} + }"#; + + assert_valid(json); +} + +#[test] +fn rejects_all_combinations_of_proxy_with_multiple_variants() { + let builtin_test_server = r#""builtinTestServer": true"#; + let localhost = r#""localhost": 8080"#; + let url = r#""url": "http://myproxy:8080""#; + + for combo in [ + format!("{builtin_test_server}, {localhost}"), + format!("{builtin_test_server}, {url}"), + format!("{localhost}, {url}"), + format!("{builtin_test_server}, {localhost}, {url}"), + ] + .iter() + { + let json = format!( + r#"{{ + "version": "0.8.0-alpha", + "network": {{ + "proxy": {{ {combo} }} + }}, + "process": {{"commandLine": "echo"}} + }}"#, + ); + + assert_invalid(&json); + } +} + +#[test] +fn rejects_empty_proxy_object() { + let json = r#"{ + "version": "0.8.0-alpha", + "network": { + "proxy": {} + }, + "process": {"commandLine": "echo"} + }"#; + + assert_invalid(json); +} + +#[test] +fn rejects_unknown_proxy_field() { + let json = r#"{ + "version": "0.8.0-alpha", + "network": { + "proxy": { + "unknownField": true + } + }, + "process": {"commandLine": "echo"} + }"#; + + assert_invalid(json); +} diff --git a/src/core/mxc_config_contract/tests/v0_8_0_alpha/one_shot.rs b/src/core/mxc_config_contract/tests/v0_8_0_alpha/one_shot.rs new file mode 100644 index 000000000..6692b3b2d --- /dev/null +++ b/src/core/mxc_config_contract/tests/v0_8_0_alpha/one_shot.rs @@ -0,0 +1,669 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::common::{assert_invalid, assert_invalid_cases, assert_valid}; + +// Root object tests +#[test] +fn rejects_unknown_root_field() { + assert_invalid( + r#"{ + "version": "0.8.0-alpha", + "process": {"commandLine": "echo"}, + "unknownField": true + }"#, + ); +} + +#[test] +fn rejects_duplicate_root_fields() { + let version = r#""version": "0.8.0-alpha""#; + let process = r#""process": {"commandLine": "echo"}"#; + let version_and_process = format!("{version}, {process}"); + + assert_invalid_cases( + [ + ( + "version", + process, + r#""version": "0.8.0-alpha", "version": "0.8.0-alpha""#, + ), + ( + "containerId", + version_and_process.as_str(), + r#""containerId": "first", "containerId": "second""#, + ), + ( + "containment", + version_and_process.as_str(), + r#""containment": "process", "containment": "processcontainer""#, + ), + ( + "lifecycle", + version_and_process.as_str(), + r#""lifecycle": {"destroyOnExit": true}, "lifecycle": {"preservePolicy": false}"#, + ), + ( + "process", + version, + r#""process": {"commandLine": "echo"}, "process": {"commandLine": "echo again"}"#, + ), + ( + "filesystem", + version_and_process.as_str(), + r#""filesystem": {"readonlyPaths": ["/first"]}, "filesystem": {"readonlyPaths": ["/second"]}"#, + ), + ( + "fallback", + version_and_process.as_str(), + r#""fallback": {"allowDaclMutation": true}, "fallback": {"allowDaclMutation": false}"#, + ), + ( + "network", + version_and_process.as_str(), + r#""network": {"defaultPolicy": "allow"}, "network": {"defaultPolicy": "block"}"#, + ), + ( + "ui", + version_and_process.as_str(), + r#""ui": {"disable": true}, "ui": {"disable": false}"#, + ), + ( + "processContainer", + version_and_process.as_str(), + r#""processContainer": {"leastPrivilege": true}, "processContainer": {"leastPrivilege": false}"#, + ), + ( + "lxc", + version_and_process.as_str(), + r#""lxc": {"distribution": "ubuntu", "release": "20.04"}, "lxc": {"distribution": "alpine", "release": "3.20"}"#, + ), + ], + "duplicate root field", + ); +} + +#[test] +fn rejects_unknown_nested_fields() { + let version = r#""version": "0.8.0-alpha""#; + let process = r#""process": {"commandLine": "echo"}"#; + let version_and_process = format!("{version}, {process}"); + + assert_invalid_cases( + [ + ( + "lifecycle", + version_and_process.as_str(), + r#""lifecycle": {"unknownField": true}"#, + ), + ( + "process", + version, + r#""process": {"commandLine": "echo", "unknownField": true}"#, + ), + ( + "filesystem", + version_and_process.as_str(), + r#""filesystem": {"unknownField": true}"#, + ), + ( + "fallback", + version_and_process.as_str(), + r#""fallback": {"unknownField": true}"#, + ), + ( + "network", + version_and_process.as_str(), + r#""network": {"unknownField": true}"#, + ), + ( + "ui", + version_and_process.as_str(), + r#""ui": {"unknownField": true}"#, + ), + ( + "processContainer", + version_and_process.as_str(), + r#""processContainer": {"unknownField": true}"#, + ), + ( + "processContainer.ui", + version_and_process.as_str(), + r#""processContainer": {"ui": {"unknownField": true}}"#, + ), + ( + "processContainer.captureDenials", + version_and_process.as_str(), + r#""processContainer": {"captureDenials": {"unknownField": true}}"#, + ), + ( + "lxc", + version_and_process.as_str(), + r#""lxc": {"distribution": "ubuntu", "release": "20.04", "unknownField": true}"#, + ), + ], + "unknown field in nested object", + ); +} + +#[test] +fn rejects_duplicate_nested_field() { + assert_invalid( + r#"{ + "version": "0.8.0-alpha", + "process": { + "commandLine": "echo", + "commandLine": "echo again" + } + }"#, + ); +} + +#[test] +fn rejects_duplicate_process_container_fields() { + let version_and_process = r#""version": "0.8.0-alpha", "process": {"commandLine": "echo"}"#; + + assert_invalid_cases( + [ + ( + "processContainer.learningMode", + version_and_process, + r#""processContainer": {"learningMode": true, "learningMode": false}"#, + ), + ( + "processContainer.captureDenials", + version_and_process, + r#""processContainer": {"captureDenials": {}, "captureDenials": {}}"#, + ), + ( + "processContainer.captureDenials.mode", + version_and_process, + r#""processContainer": {"captureDenials": {"mode": "block", "mode": "allow"}}"#, + ), + ( + "processContainer.captureDenials.outputPath", + version_and_process, + r#""processContainer": {"captureDenials": {"outputPath": "/tmp/output", "outputPath": "/tmp/output2"}}"#, + ), + ( + "processContainer.captureDenials.retainEtl", + version_and_process, + r#""processContainer": {"captureDenials": {"retainEtl": true, "retainEtl": false}}"#, + ), + ], + "duplicate nested field", + ); +} + +// Version field tests +#[test] +fn rejects_missing_version() { + assert_invalid( + r#"{ + "process": {"commandLine": "echo"} + }"#, + ); +} + +#[test] +fn rejects_duplicate_version() { + assert_invalid( + r#"{ + "version": "0.8.0-alpha", + "version": "0.8.0-alpha", + "process": {"commandLine": "echo"} + }"#, + ); +} + +#[test] +fn rejects_invalid_version() { + assert_invalid( + r#"{ + "version": "invalid", + "process": {"commandLine": "echo"} + }"#, + ); +} + +#[test] +fn rejects_non_exact_version() { + assert_invalid( + r#"{ + "version": "0.8.0", + "process": {"commandLine": "echo"} + }"#, + ); +} + +#[test] +fn rejects_non_string_version() { + assert_invalid( + r#"{ + "version": 0.6, + "process": {"commandLine": "echo"} + }"#, + ); +} + +#[test] +fn rejects_null_version() { + assert_invalid( + r#"{ + "version": null, + "process": {"commandLine": "echo"} + }"#, + ); +} + +// Required field tests +#[test] +fn rejects_missing_process() { + assert_invalid( + r#"{ + "version": "0.8.0-alpha" + }"#, + ); +} + +#[test] +fn rejects_null_process() { + assert_invalid( + r#"{ + "version": "0.8.0-alpha", + "process": null + }"#, + ); +} + +#[test] +fn rejects_missing_process_command_line() { + assert_invalid( + r#"{ + "version": "0.8.0-alpha", + "process": {} + }"#, + ); +} + +#[test] +fn rejects_null_process_command_line() { + assert_invalid( + r#"{ + "version": "0.8.0-alpha", + "process": {"commandLine": null} + }"#, + ); +} + +#[test] +fn rejects_empty_process_command_line() { + assert_invalid( + r#"{ + "version": "0.8.0-alpha", + "process": {"commandLine": ""} + }"#, + ); +} + +// processcontainer and appcontainer tests +#[test] +fn accepts_process_container() { + let json = r#"{ + "version": "0.8.0-alpha", + "processContainer": { + "ui": { + "isolation": "container" + } + }, + "process": {"commandLine": "echo"} + }"#; + + assert_valid(json); +} + +#[test] +fn accepts_app_container_section_alias() { + let json = r#"{ + "version": "0.8.0-alpha", + "appContainer": { + "ui": { + "isolation": "container" + } + }, + "process": {"commandLine": "echo"} + }"#; + + assert_valid(json); +} + +#[test] +fn rejects_process_container_and_app_container_section_alias_together() { + let json = r#"{ + "version": "0.8.0-alpha", + "processContainer": { + "ui": { + "isolation": "container" + } + }, + "appContainer": { + "ui": { + "isolation": "container" + } + }, + "process": {"commandLine": "echo"} + }"#; + + assert_invalid(json); +} + +#[test] +fn accepts_process_container_learning_mode_values() { + for process_container_learning_mode in ["true", "false"] { + let json = format!( + r#"{{ + "version": "0.8.0-alpha", + "processContainer": {{ + "learningMode": {process_container_learning_mode} + }}, + "process": {{"commandLine": "echo"}} + }}"# + ); + + assert_valid(&json); + } +} + +#[test] +fn rejects_non_boolean_process_container_learning_mode_values() { + for process_container_learning_mode in ["\"string\"", "123", "[]", "{}"] { + let json = format!( + r#"{{ + "version": "0.8.0-alpha", + "processContainer": {{ + "learningMode": {process_container_learning_mode} + }}, + "process": {{"commandLine": "echo"}} + }}"# + ); + + assert_invalid(&json); + } +} + +#[test] +fn accepts_empty_capture_denials() { + let json = r#"{ + "version": "0.8.0-alpha", + "processContainer": { + "captureDenials": {} + }, + "process": {"commandLine": "echo"} + }"#; + + assert_valid(json); +} + +#[test] +fn accepts_capture_denials_output_path() { + for output_path in [r"c:\temp\denials.log", ""] { + let output_path_json = serde_json::to_string(output_path).unwrap(); + let json = format!( + r#"{{ + "version": "0.8.0-alpha", + "processContainer": {{ + "captureDenials": {{ + "outputPath": {output_path_json} + }} + }}, + "process": {{"commandLine": "echo"}} + }}"# + ); + + assert_valid(&json); + } +} + +#[test] +fn rejects_non_string_capture_denials_output_path() { + for capture_denials_output_path in ["123", "true", "[]", "{}"] { + let json = format!( + r#"{{ + "version": "0.8.0-alpha", + "processContainer": {{ + "captureDenials": {{ + "outputPath": {capture_denials_output_path} + }} + }}, + "process": {{"commandLine": "echo"}} + }}"# + ); + + assert_invalid(&json); + } +} + +#[test] +fn accepts_capture_denials_retain_etl_values() { + for capture_denials_retain_etl in ["true", "false"] { + let json = format!( + r#"{{ + "version": "0.8.0-alpha", + "processContainer": {{ + "captureDenials": {{ + "retainEtl": {capture_denials_retain_etl} + }} + }}, + "process": {{"commandLine": "echo"}} + }}"# + ); + + assert_valid(&json); + } +} + +#[test] +fn rejects_non_boolean_capture_denials_retain_etl_values() { + for capture_denials_retain_etl in ["\"string\"", "123", "[]", "{}"] { + let json = format!( + r#"{{ + "version": "0.8.0-alpha", + "processContainer": {{ + "captureDenials": {{ + "retainEtl": {capture_denials_retain_etl} + }} + }}, + "process": {{"commandLine": "echo"}} + }}"# + ); + + assert_invalid(&json); + } +} + +// Numerics and collections +#[test] +fn rejects_negative_process_timeout() { + let json = r#"{ + "version": "0.8.0-alpha", + "process": { + "commandLine": "echo", + "timeout": -1 + } + }"#; + + assert_invalid(json); +} + +#[test] +fn rejects_process_timeout_above_u32() { + let json = r#"{ + "version": "0.8.0-alpha", + "process": { + "commandLine": "echo", + "timeout": 4294967296 + } + }"#; + + assert_invalid(json); +} + +#[test] +fn rejects_non_string_process_env_items() { + let json = r#"{ + "version": "0.8.0-alpha", + "process": { + "commandLine": "echo", + "env": ["A=1", 123] + } + }"#; + + assert_invalid(json); +} + +#[test] +fn rejects_non_string_filesystem_readonly_path_items() { + let json = r#"{ + "version": "0.8.0-alpha", + "process": { + "commandLine": "echo" + }, + "filesystem": { + "readonlyPaths": [123] + } + }"#; + + assert_invalid(json); +} + +#[test] +fn rejects_non_string_filesystem_readwrite_path_items() { + let json = r#"{ + "version": "0.8.0-alpha", + "process": { + "commandLine": "echo" + }, + "filesystem": { + "readwritePaths": [123] + } + }"#; + + assert_invalid(json); +} + +#[test] +fn rejects_non_string_filesystem_denied_path_items() { + let json = r#"{ + "version": "0.8.0-alpha", + "process": { + "commandLine": "echo" + }, + "filesystem": { + "deniedPaths": [123] + } + }"#; + + assert_invalid(json); +} + +#[test] +fn rejects_non_string_process_container_capabilities() { + let json = r#"{ + "version": "0.8.0-alpha", + "processContainer": { + "capabilities": [123] + }, + "process": {"commandLine": "echo"} + }"#; + + assert_invalid(json); +} + +// Lxc tests +#[test] +fn accepts_lxc() { + let json = r#"{ + "version": "0.8.0-alpha", + "lxc": { + "distribution": "ubuntu", + "release": "20.04" + }, + "process": {"commandLine": "echo"} + }"#; + + assert_valid(json); +} + +#[test] +fn rejects_lxc_missing_distribution() { + let json = r#"{ + "version": "0.8.0-alpha", + "lxc": { + "release": "20.04" + }, + "process": {"commandLine": "echo"} + }"#; + + assert_invalid(json); +} + +#[test] +fn rejects_lxc_missing_release() { + let json = r#"{ + "version": "0.8.0-alpha", + "lxc": { + "distribution": "ubuntu" + }, + "process": {"commandLine": "echo"} + }"#; + + assert_invalid(json); +} + +#[test] +fn rejects_lxc_null_distribution() { + let json = r#"{ + "version": "0.8.0-alpha", + "lxc": { + "distribution": null, + "release": "20.04" + }, + "process": {"commandLine": "echo"} + }"#; + + assert_invalid(json); +} + +#[test] +fn rejects_lxc_null_release() { + let json = r#"{ + "version": "0.8.0-alpha", + "lxc": { + "distribution": "ubuntu", + "release": null + }, + "process": {"commandLine": "echo"} + }"#; + + assert_invalid(json); +} + +// State-aware tests +#[test] +fn rejects_state_aware_fields() { + for field in [ + r#""phase": "exec""#, + r#""sandboxId": "someId""#, + r#""correlationVector": "someVector""#, + ] { + let json = format!( + r#"{{ + "version": "0.8.0-alpha", + "process": {{"commandLine": "echo"}}, + {field} + }}"# + ); + + assert_invalid(&json); + } +} diff --git a/src/core/mxc_config_contract/tests/v0_8_0_alpha/optional_fields.rs b/src/core/mxc_config_contract/tests/v0_8_0_alpha/optional_fields.rs new file mode 100644 index 000000000..be10ae129 --- /dev/null +++ b/src/core/mxc_config_contract/tests/v0_8_0_alpha/optional_fields.rs @@ -0,0 +1,408 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::common::{assert_invalid_cases, assert_valid}; +use mxc_config_contract::dev::OneShotRequest; + +#[test] +fn accepts_empty_optional_objects() { + for field in [ + r#""lifecycle": {}"#, + r#""filesystem": {}"#, + r#""fallback": {}"#, + r#""network": {}"#, + r#""ui": {}"#, + r#""processContainer": {}"#, + r#""processContainer": {"ui": {}}"#, + r#""seatbelt": {}"#, + r#""experimental": {}"#, + ] { + let json = format!( + r#"{{ + "version": "0.8.0-alpha", + "process": {{"commandLine": "echo"}}, + {field} + }}"# + ); + + assert_valid(&json); + } +} + +#[test] +fn accepts_empty_optional_array_for_process_env() { + let json = r#"{ + "version": "0.8.0-alpha", + "process": {"commandLine": "echo", "env": []} + }"#; + + assert_valid(json); +} + +#[test] +fn accepts_empty_optional_arrays() { + for field in [ + r#""filesystem": {"readwritePaths": []}"#, + r#""filesystem": {"readonlyPaths": []}"#, + r#""filesystem": {"deniedPaths": []}"#, + r#""network": {"allowedHosts": []}"#, + r#""network": {"blockedHosts": []}"#, + r#""processContainer": {"capabilities": []}"#, + r#""seatbelt": {"extraMachLookups": []}"#, + ] { + let json = format!( + r#"{{ + "version": "0.8.0-alpha", + "process": {{"commandLine": "echo"}}, + {field} + }}"# + ); + + assert_valid(&json); + } +} + +#[test] +fn accepts_absent_optional_fields() { + let json = r#"{ + "version": "0.8.0-alpha", + "process": {"commandLine": "echo"} + }"#; + + serde_json::from_str::(json).unwrap(); +} + +#[test] +fn rejects_null_optional_fields() { + let version = r#""version": "0.8.0-alpha""#; + let process = r#""process": {"commandLine": "echo"}"#; + let version_and_process = format!("{version}, {process}"); + + assert_invalid_cases( + [ + ( + "containerId", + version_and_process.as_str(), + r#""containerId": null"#, + ), + ( + "containment", + version_and_process.as_str(), + r#""containment": null"#, + ), + ( + "lifecycle", + version_and_process.as_str(), + r#""lifecycle": null"#, + ), + ( + "filesystem", + version_and_process.as_str(), + r#""filesystem": null"#, + ), + ( + "fallback", + version_and_process.as_str(), + r#""fallback": null"#, + ), + ( + "network", + version_and_process.as_str(), + r#""network": null"#, + ), + ("ui", version_and_process.as_str(), r#""ui": null"#), + ( + "processContainer", + version_and_process.as_str(), + r#""processContainer": null"#, + ), + ( + "appContainer", + version_and_process.as_str(), + r#""appContainer": null"#, + ), + ("lxc", version_and_process.as_str(), r#""lxc": null"#), + ( + "macos_sandbox", + version_and_process.as_str(), + r#""macos_sandbox": null"#, + ), + ( + "seatbelt", + version_and_process.as_str(), + r#""seatbelt": null"#, + ), + ( + "seatbelt.profileOverride", + version_and_process.as_str(), + r#""seatbelt": {"profileOverride": null}"#, + ), + ( + "seatbelt.guiAccess", + version_and_process.as_str(), + r#""seatbelt": {"guiAccess": null}"#, + ), + ( + "seatbelt.launchMethod", + version_and_process.as_str(), + r#""seatbelt": {"launchMethod": null}"#, + ), + ( + "seatbelt.nestedPty", + version_and_process.as_str(), + r#""seatbelt": {"nestedPty": null}"#, + ), + ( + "seatbelt.keychainAccess", + version_and_process.as_str(), + r#""seatbelt": {"keychainAccess": null}"#, + ), + ( + "seatbelt.extraMachLookups", + version_and_process.as_str(), + r#""seatbelt": {"extraMachLookups": null}"#, + ), + ( + "lifecycle.destroyOnExit", + version_and_process.as_str(), + r#""lifecycle": {"destroyOnExit": null}"#, + ), + ( + "lifecycle.preservePolicy", + version_and_process.as_str(), + r#""lifecycle": {"preservePolicy": null}"#, + ), + ( + "process.cwd", + version, + r#""process": {"commandLine": "echo", "cwd": null}"#, + ), + ( + "process.env", + version, + r#""process": {"commandLine": "echo", "env": null}"#, + ), + ( + "process.timeout", + version, + r#""process": {"commandLine": "echo", "timeout": null}"#, + ), + ( + "filesystem.readwritePaths", + version_and_process.as_str(), + r#""filesystem": {"readwritePaths": null}"#, + ), + ( + "filesystem.readonlyPaths", + version_and_process.as_str(), + r#""filesystem": {"readonlyPaths": null}"#, + ), + ( + "filesystem.deniedPaths", + version_and_process.as_str(), + r#""filesystem": {"deniedPaths": null}"#, + ), + ( + "fallback.allowDaclMutation", + version_and_process.as_str(), + r#""fallback": {"allowDaclMutation": null}"#, + ), + ( + "network.defaultPolicy", + version_and_process.as_str(), + r#""network": {"defaultPolicy": null}"#, + ), + ( + "network.enforcementMode", + version_and_process.as_str(), + r#""network": {"enforcementMode": null}"#, + ), + ( + "network.allowedHosts", + version_and_process.as_str(), + r#""network": {"allowedHosts": null}"#, + ), + ( + "network.blockedHosts", + version_and_process.as_str(), + r#""network": {"blockedHosts": null}"#, + ), + ( + "network.allowLocalNetwork", + version_and_process.as_str(), + r#""network": {"allowLocalNetwork": null}"#, + ), + ( + "network.proxy", + version_and_process.as_str(), + r#""network": {"proxy": null}"#, + ), + ( + "ui.disable", + version_and_process.as_str(), + r#""ui": {"disable": null}"#, + ), + ( + "ui.clipboard", + version_and_process.as_str(), + r#""ui": {"clipboard": null}"#, + ), + ( + "ui.injection", + version_and_process.as_str(), + r#""ui": {"injection": null}"#, + ), + ( + "processContainer.leastPrivilege", + version_and_process.as_str(), + r#""processContainer": {"leastPrivilege": null}"#, + ), + ( + "processContainer.learningMode", + version_and_process.as_str(), + r#""processContainer": {"learningMode": null}"#, + ), + ( + "processContainer.capabilities", + version_and_process.as_str(), + r#""processContainer": {"capabilities": null}"#, + ), + ( + "processContainer.captureDenials", + version_and_process.as_str(), + r#""processContainer": {"captureDenials": null}"#, + ), + ( + "processContainer.captureDenials.mode", + version_and_process.as_str(), + r#""processContainer": {"captureDenials": { "mode": null}}"#, + ), + ( + "processContainer.captureDenials.outputPath", + version_and_process.as_str(), + r#""processContainer": {"captureDenials": { "outputPath": null}}"#, + ), + ( + "processContainer.captureDenials.retainEtl", + version_and_process.as_str(), + r#""processContainer": {"captureDenials": { "retainEtl": null}}"#, + ), + ( + "processContainer.ui", + version_and_process.as_str(), + r#""processContainer": {"ui": null}"#, + ), + ( + "processContainer.ui.isolation", + version_and_process.as_str(), + r#""processContainer": {"ui": {"isolation": null}}"#, + ), + ( + "processContainer.ui.desktopSystemControl", + version_and_process.as_str(), + r#""processContainer": {"ui": {"desktopSystemControl": null}}"#, + ), + ( + "processContainer.ui.systemSettings", + version_and_process.as_str(), + r#""processContainer": {"ui": {"systemSettings": null}}"#, + ), + ( + "processContainer.ui.ime", + version_and_process.as_str(), + r#""processContainer": {"ui": {"ime": null}}"#, + ), + ( + "experimental.test", + version_and_process.as_str(), + r#""experimental": {"test": null}"#, + ), + ( + "experimental.test.message", + version_and_process.as_str(), + r#""experimental": {"test": {"message": null}}"#, + ), + ( + "experimental.telemetry", + version_and_process.as_str(), + r#""experimental": {"telemetry": null}"#, + ), + ( + "experimental.telemetry.enabled", + version_and_process.as_str(), + r#""experimental": {"telemetry": {"enabled": null}}"#, + ), + ( + "experimental.windows_sandbox", + version_and_process.as_str(), + r#""experimental": {"windows_sandbox": null}"#, + ), + ( + "experimental.windows_sandbox.idleTimeoutMs", + version_and_process.as_str(), + r#""experimental": {"windows_sandbox": {"idleTimeoutMs": null}}"#, + ), + ( + "experimental.windows_sandbox.idleTimeout", + version_and_process.as_str(), + r#""experimental": {"windows_sandbox": {"idleTimeout": null}}"#, + ), + ( + "experimental.windows_sandbox.daemonPipeName", + version_and_process.as_str(), + r#""experimental": {"windows_sandbox": {"daemonPipeName": null}}"#, + ), + ( + "experimental.wslc", + version_and_process.as_str(), + r#""experimental": {"wslc": null}"#, + ), + ( + "experimental.wslc.targetOs", + version_and_process.as_str(), + r#""experimental": {"wslc": {"targetOs": null}}"#, + ), + ( + "experimental.wslc.image", + version_and_process.as_str(), + r#""experimental": {"wslc": {"image": null}}"#, + ), + ( + "experimental.wslc.imageTarPath", + version_and_process.as_str(), + r#""experimental": {"wslc": {"imageTarPath": null}}"#, + ), + ( + "experimental.wslc.cpuCount", + version_and_process.as_str(), + r#""experimental": {"wslc": {"cpuCount": null}}"#, + ), + ( + "experimental.wslc.memoryMb", + version_and_process.as_str(), + r#""experimental": {"wslc": {"memoryMb": null}}"#, + ), + ( + "experimental.wslc.gpu", + version_and_process.as_str(), + r#""experimental": {"wslc": {"gpu": null}}"#, + ), + ( + "experimental.wslc.storagePath", + version_and_process.as_str(), + r#""experimental": {"wslc": {"storagePath": null}}"#, + ), + ( + "experimental.wslc.portMappings", + version_and_process.as_str(), + r#""experimental": {"wslc": {"portMappings": null}}"#, + ), + ( + "experimental.wslc.portMappings[].protocol", + version_and_process.as_str(), + r#""experimental": {"wslc": {"portMappings": [{"windowsPort": 8080, "containerPort": 80, "protocol": null}]}}"#, + ), + ], + "null optional field", + ); +} diff --git a/src/core/mxc_config_contract/tests/v0_8_0_alpha/seatbelt.rs b/src/core/mxc_config_contract/tests/v0_8_0_alpha/seatbelt.rs new file mode 100644 index 000000000..198adeb7d --- /dev/null +++ b/src/core/mxc_config_contract/tests/v0_8_0_alpha/seatbelt.rs @@ -0,0 +1,225 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +use crate::common::{assert_invalid, assert_valid}; + +#[test] +fn accepts_complete_seatbelt_object() { + let json = r#"{ + "version": "0.8.0-alpha", + "seatbelt": { + "profileOverride": "none", + "guiAccess": false, + "launchMethod": "exec", + "nestedPty": false, + "keychainAccess": false, + "extraMachLookups": ["com.apple.securityd", "com.apple.coreservices.launchservicesd"] + }, + "process": {"commandLine": "echo"} + }"#; + + assert_valid(json); +} + +#[test] +fn accepts_every_seatbelt_launch_method() { + for launch_method in ["exec", "open"] { + let json = format!( + r#"{{ + "version": "0.8.0-alpha", + "seatbelt": {{ + "launchMethod": "{launch_method}" + }}, + "process": {{"commandLine": "echo"}} + }}"# + ); + + assert_valid(&json); + } +} + +#[test] +fn rejects_invalid_seatbelt_launch_method() { + let json = r#"{ + "version": "0.8.0-alpha", + "seatbelt": { + "launchMethod": "invalid" + }, + "process": {"commandLine": "echo"} + }"#; + + assert_invalid(json); +} + +#[test] +fn rejects_non_string_seatbelt_launch_method() { + for launch_method in ["true", "false", "0", "1", "[]", "{}"] { + let json = format!( + r#"{{ + "version": "0.8.0-alpha", + "seatbelt": {{ + "launchMethod": {launch_method} + }}, + "process": {{"commandLine": "echo"}} + }}"# + ); + + assert_invalid(&json); + } +} + +#[test] +fn accepts_empty_profile_override() { + let json = r#"{ + "version": "0.8.0-alpha", + "seatbelt": { + "profileOverride": "" + }, + "process": {"commandLine": "echo"} + }"#; + + assert_valid(json); +} + +#[test] +fn rejects_non_string_extra_mach_lookup_items() { + for extra_mach_lookup in ["true", "false", "0", "1", "[]", "{}"] { + let json = format!( + r#"{{ + "version": "0.8.0-alpha", + "seatbelt": {{ + "extraMachLookups": [{extra_mach_lookup}] + }}, + "process": {{"commandLine": "echo"}} + }}"# + ); + + assert_invalid(&json); + } +} + +#[test] +fn rejects_non_array_extra_mach_lookups() { + for extra_mach_lookup in ["true", "false", "0", "1", "\"string\"", "{}"] { + let json = format!( + r#"{{ + "version": "0.8.0-alpha", + "seatbelt": {{ + "extraMachLookups": {extra_mach_lookup} + }}, + "process": {{"commandLine": "echo"}} + }}"# + ); + + assert_invalid(&json); + } +} + +#[test] +fn rejects_non_string_profile_override_values() { + for profile_override in ["true", "false", "0", "1", "[]", "{}"] { + let json = format!( + r#"{{ + "version": "0.8.0-alpha", + "seatbelt": {{ + "profileOverride": {profile_override} + }}, + "process": {{"commandLine": "echo"}} + }}"# + ); + + assert_invalid(&json); + } +} + +#[test] +fn rejects_non_boolean_gui_access_values() { + for gui_access in ["0", "1", "\"string\"", "[]", "{}"] { + let json = format!( + r#"{{ + "version": "0.8.0-alpha", + "seatbelt": {{ + "guiAccess": {gui_access} + }}, + "process": {{"commandLine": "echo"}} + }}"# + ); + + assert_invalid(&json); + } +} + +#[test] +fn rejects_non_boolean_nested_pty_values() { + for nested_pty in ["0", "1", "\"string\"", "[]", "{}"] { + let json = format!( + r#"{{ + "version": "0.8.0-alpha", + "seatbelt": {{ + "nestedPty": {nested_pty} + }}, + "process": {{"commandLine": "echo"}} + }}"# + ); + + assert_invalid(&json); + } +} + +#[test] +fn rejects_non_boolean_keychain_access_values() { + for keychain_access in ["0", "1", "\"string\"", "[]", "{}"] { + let json = format!( + r#"{{ + "version": "0.8.0-alpha", + "seatbelt": {{ + "keychainAccess": {keychain_access} + }}, + "process": {{"commandLine": "echo"}} + }}"# + ); + + assert_invalid(&json); + } +} + +#[test] +fn rejects_unknown_seatbelt_field() { + let json = r#"{ + "version": "0.8.0-alpha", + "seatbelt": { + "unknownField": "value" + }, + "process": {"commandLine": "echo"} + }"#; + + assert_invalid(json); +} + +#[test] +fn accepts_macos_sandbox_section_alias() { + let json = r#"{ + "version": "0.8.0-alpha", + "macos_sandbox": { + "profileOverride": "none" + }, + "process": {"commandLine": "echo"} + }"#; + + assert_valid(json); +} + +#[test] +fn rejects_seatbelt_and_macos_sandbox_section_alias_together() { + let json = r#"{ + "version": "0.8.0-alpha", + "seatbelt": { + "profileOverride": "none" + }, + "macos_sandbox": { + "profileOverride": "none" + }, + "process": {"commandLine": "echo"} + }"#; + + assert_invalid(json); +} diff --git a/src/core/mxc_config_contract/tests/version_boundaries.rs b/src/core/mxc_config_contract/tests/version_boundaries.rs index c2c7be148..567188aa5 100644 --- a/src/core/mxc_config_contract/tests/version_boundaries.rs +++ b/src/core/mxc_config_contract/tests/version_boundaries.rs @@ -7,5 +7,11 @@ mod annotations; mod common; #[path = "version_boundaries/compatibility.rs"] mod compatibility; +#[path = "version_boundaries/containment.rs"] +mod containment; +#[path = "version_boundaries/experimental.rs"] +mod experimental; +#[path = "version_boundaries/process_container.rs"] +mod process_container; #[path = "version_boundaries/seatbelt.rs"] mod seatbelt; diff --git a/src/core/mxc_config_contract/tests/version_boundaries/annotations.rs b/src/core/mxc_config_contract/tests/version_boundaries/annotations.rs index 1e9a73ba0..d7654e251 100644 --- a/src/core/mxc_config_contract/tests/version_boundaries/annotations.rs +++ b/src/core/mxc_config_contract/tests/version_boundaries/annotations.rs @@ -1,46 +1,16 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use crate::common::assert_v06_rejects_v07_accepts; +use crate::common::assert_v07_introduces; #[test] fn schema_is_available_starting_in_v07() { - let v06_json = r#"{ - "$schema": "https://github.com/microsoft/mxc/blob/main/schemas/stable/mxc-config.schema.0.7.0-alpha.json", - "version": "0.6.0-alpha", - "process": { - "commandLine": "echo" - } - }"#; - - let v07_json = r#"{ - "$schema": "https://github.com/microsoft/mxc/blob/main/schemas/stable/mxc-config.schema.0.7.0-alpha.json", - "version": "0.7.0-alpha", - "process": { - "commandLine": "echo" - } - }"#; - - assert_v06_rejects_v07_accepts(v06_json, v07_json); + assert_v07_introduces( + r#""$schema": "https://github.com/microsoft/mxc/blob/main/schemas/stable/mxc-config.schema.0.7.0-alpha.json""#, + ); } #[test] fn comment_is_available_starting_in_v07() { - let v06_json = r#"{ - "_comment": "This is a comment", - "version": "0.6.0-alpha", - "process": { - "commandLine": "echo" - } - }"#; - - let v07_json = r#"{ - "_comment": "This is a comment", - "version": "0.7.0-alpha", - "process": { - "commandLine": "echo" - } - }"#; - - assert_v06_rejects_v07_accepts(v06_json, v07_json); + assert_v07_introduces(r#""_comment": "This is a comment""#); } diff --git a/src/core/mxc_config_contract/tests/version_boundaries/common.rs b/src/core/mxc_config_contract/tests/version_boundaries/common.rs index 5b1e9f563..cea587672 100644 --- a/src/core/mxc_config_contract/tests/version_boundaries/common.rs +++ b/src/core/mxc_config_contract/tests/version_boundaries/common.rs @@ -1,14 +1,55 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +use mxc_config_contract::dev::OneShotRequest as V08Request; use mxc_config_contract::published::v0_6_0_alpha::Request as V06Request; use mxc_config_contract::published::v0_7_0_alpha::Request as V07Request; pub(crate) fn assert_v06_rejects_v07_accepts(v06_json: &str, v07_json: &str) { + assert_well_formed(v06_json, "0.6 boundary input"); + assert_well_formed(v07_json, "0.7 boundary input"); assert!(serde_json::from_str::(v06_json).is_err()); - assert_valid(v07_json); + assert_v07_valid(v07_json); } -pub(crate) fn assert_valid(json: &str) { +fn assert_v07_rejects_v08_accepts(v07_json: &str, v08_json: &str) { + assert_well_formed(v07_json, "0.7 boundary input"); + assert_well_formed(v08_json, "0.8 boundary input"); + assert!(serde_json::from_str::(v07_json).is_err()); + assert_v08_valid(v08_json); +} + +pub(crate) fn assert_v07_introduces(additional_fields: &str) { + let v06_json = one_shot_request("0.6.0-alpha", additional_fields); + let v07_json = one_shot_request("0.7.0-alpha", additional_fields); + assert_v06_rejects_v07_accepts(&v06_json, &v07_json); +} + +pub(crate) fn assert_v08_introduces(additional_fields: &str) { + let v07_json = one_shot_request("0.7.0-alpha", additional_fields); + let v08_json = one_shot_request("0.8.0-alpha", additional_fields); + assert_v07_rejects_v08_accepts(&v07_json, &v08_json); +} + +fn assert_v07_valid(json: &str) { serde_json::from_str::(json).unwrap(); } + +fn assert_v08_valid(json: &str) { + serde_json::from_str::(json).unwrap(); +} + +fn one_shot_request(version: &str, additional_fields: &str) -> String { + format!( + r#"{{ + "version": "{version}", + "process": {{"commandLine": "echo"}}, + {additional_fields} + }}"# + ) +} + +fn assert_well_formed(json: &str, context: &str) { + serde_json::from_str::(json) + .unwrap_or_else(|error| panic!("{context} used malformed JSON: {error}")); +} diff --git a/src/core/mxc_config_contract/tests/version_boundaries/compatibility.rs b/src/core/mxc_config_contract/tests/version_boundaries/compatibility.rs index 0e670b7df..c8f012611 100644 --- a/src/core/mxc_config_contract/tests/version_boundaries/compatibility.rs +++ b/src/core/mxc_config_contract/tests/version_boundaries/compatibility.rs @@ -1,12 +1,16 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +use mxc_config_contract::dev::{ + OneShotContainment as V08Containment, OneShotRequest as V08Request, +}; use mxc_config_contract::published::{ v0_6_0_alpha::{Containment as V06Containment, Request as V06Request}, v0_7_0_alpha::{Containment as V07Containment, Request as V07Request}, }; + #[test] -fn appcontainer_containment_value_alias_remains_accepted_in_v07() { +fn appcontainer_containment_value_alias_remains_accepted_across_registered_contracts() { let v06_json = r#"{ "version": "0.6.0-alpha", "containment": "appcontainer", @@ -23,8 +27,17 @@ fn appcontainer_containment_value_alias_remains_accepted_in_v07() { } }"#; + let v08_json = r#"{ + "version": "0.8.0-alpha", + "containment": "appcontainer", + "process": { + "commandLine": "echo" + } + }"#; + let v06_request: V06Request = serde_json::from_str(v06_json).unwrap(); let v07_request: V07Request = serde_json::from_str(v07_json).unwrap(); + let v08_request: V08Request = serde_json::from_str(v08_json).unwrap(); assert!(matches!( v06_request.containment.as_ref(), @@ -35,10 +48,15 @@ fn appcontainer_containment_value_alias_remains_accepted_in_v07() { v07_request.containment.as_ref(), Some(V07Containment::ProcessContainer) )); + + assert!(matches!( + v08_request.containment.as_ref(), + Some(V08Containment::ProcessContainer) + )); } #[test] -fn app_container_section_alias_remains_accepted_in_v07() { +fn app_container_section_alias_remains_accepted_across_registered_contracts() { let v06_json = r#"{ "version": "0.6.0-alpha", "containment": "process", @@ -67,8 +85,23 @@ fn app_container_section_alias_remains_accepted_in_v07() { } }"#; + let v08_json = r#"{ + "version": "0.8.0-alpha", + "containment": "process", + "appContainer": { + "leastPrivilege": true, + "capabilities": [ + "internetClient" + ] + }, + "process": { + "commandLine": "echo" + } + }"#; + let v06_request: V06Request = serde_json::from_str(v06_json).unwrap(); let v07_request: V07Request = serde_json::from_str(v07_json).unwrap(); + let v08_request: V08Request = serde_json::from_str(v08_json).unwrap(); let v06_process_container = v06_request .process_container @@ -79,9 +112,14 @@ fn app_container_section_alias_remains_accepted_in_v07() { .process_container .as_ref() .expect("0.7 appContainer alias should populate process_container"); + let v08_process_container = v08_request + .process_container + .as_ref() + .expect("0.8 appContainer alias should populate process_container"); assert_eq!(v06_process_container.least_privilege.as_ref(), Some(&true)); assert_eq!(v07_process_container.least_privilege.as_ref(), Some(&true)); + assert_eq!(v08_process_container.least_privilege.as_ref(), Some(&true)); assert_eq!( v06_process_container @@ -97,4 +135,57 @@ fn app_container_section_alias_remains_accepted_in_v07() { .expect("0.7 capabilities"), &vec!["internetClient".to_string()] ); + assert_eq!( + v08_process_container + .capabilities + .as_ref() + .expect("0.8 capabilities"), + &vec!["internetClient".to_string()] + ); +} + +#[test] +fn macos_sandbox_containment_value_alias_remains_accepted_from_v07() { + let v07_json = r#"{ + "version": "0.7.0-alpha", + "containment": "macos_sandbox", + "process": {"commandLine": "echo"} + }"#; + let v08_json = r#"{ + "version": "0.8.0-alpha", + "containment": "macos_sandbox", + "process": {"commandLine": "echo"} + }"#; + + let v07_request: V07Request = serde_json::from_str(v07_json).unwrap(); + let v08_request: V08Request = serde_json::from_str(v08_json).unwrap(); + + assert!(matches!( + v07_request.containment.as_ref(), + Some(V07Containment::Seatbelt) + )); + assert!(matches!( + v08_request.containment.as_ref(), + Some(V08Containment::Seatbelt) + )); +} + +#[test] +fn macos_sandbox_section_alias_remains_accepted_from_v07() { + let v07_json = r#"{ + "version": "0.7.0-alpha", + "macos_sandbox": {}, + "process": {"commandLine": "echo"} + }"#; + let v08_json = r#"{ + "version": "0.8.0-alpha", + "macos_sandbox": {}, + "process": {"commandLine": "echo"} + }"#; + + let v07_request: V07Request = serde_json::from_str(v07_json).unwrap(); + let v08_request: V08Request = serde_json::from_str(v08_json).unwrap(); + + assert!(v07_request.seatbelt.as_ref().is_some()); + assert!(v08_request.seatbelt.as_ref().is_some()); } diff --git a/src/core/mxc_config_contract/tests/version_boundaries/containment.rs b/src/core/mxc_config_contract/tests/version_boundaries/containment.rs new file mode 100644 index 000000000..3d45b8952 --- /dev/null +++ b/src/core/mxc_config_contract/tests/version_boundaries/containment.rs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::common::assert_v08_introduces; + +#[test] +fn development_containment_values_are_introduced_in_v08() { + for containment in [ + "vm", + "windows_sandbox", + "microvm", + "hyperlight", + "isolation_session", + "wslc", + ] { + assert_v08_introduces(&format!(r#""containment": "{containment}""#)); + } +} diff --git a/src/core/mxc_config_contract/tests/version_boundaries/experimental.rs b/src/core/mxc_config_contract/tests/version_boundaries/experimental.rs new file mode 100644 index 000000000..5da0014a5 --- /dev/null +++ b/src/core/mxc_config_contract/tests/version_boundaries/experimental.rs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::common::assert_v08_introduces; + +#[test] +fn experimental_test_is_introduced_in_v08() { + assert_v08_introduces(r#""experimental": {"test": {}}"#); +} + +#[test] +fn experimental_telemetry_is_introduced_in_v08() { + assert_v08_introduces(r#""experimental": {"telemetry": {}}"#); +} + +#[test] +fn experimental_windows_sandbox_is_introduced_in_v08() { + assert_v08_introduces(r#""experimental": {"windows_sandbox": {}}"#); +} + +#[test] +fn experimental_wslc_is_introduced_in_v08() { + assert_v08_introduces(r#""experimental": {"wslc": {}}"#); +} diff --git a/src/core/mxc_config_contract/tests/version_boundaries/process_container.rs b/src/core/mxc_config_contract/tests/version_boundaries/process_container.rs new file mode 100644 index 000000000..9af48fa91 --- /dev/null +++ b/src/core/mxc_config_contract/tests/version_boundaries/process_container.rs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::common::assert_v08_introduces; + +#[test] +fn learning_mode_is_introduced_in_v08() { + assert_v08_introduces(r#""processContainer": {"learningMode": true}"#); +} + +#[test] +fn capture_denials_is_introduced_in_v08() { + assert_v08_introduces(r#""processContainer": {"captureDenials": {}}"#); +} diff --git a/src/core/mxc_config_contract/tests/version_boundaries/seatbelt.rs b/src/core/mxc_config_contract/tests/version_boundaries/seatbelt.rs index fa1e15ec8..0c2e892cf 100644 --- a/src/core/mxc_config_contract/tests/version_boundaries/seatbelt.rs +++ b/src/core/mxc_config_contract/tests/version_boundaries/seatbelt.rs @@ -1,80 +1,24 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use crate::common::assert_v06_rejects_v07_accepts; +use crate::common::assert_v07_introduces; #[test] fn seatbelt_section_is_introduced_in_v07() { - let v06_json = r#"{ - "version": "0.6.0-alpha", - "containment": "process", - "process": {"commandLine": "echo"}, - "seatbelt": {} - }"#; - - let v07_json = r#"{ - "version": "0.7.0-alpha", - "containment": "process", - "process": {"commandLine": "echo"}, - "seatbelt": {} - }"#; - - assert_v06_rejects_v07_accepts(v06_json, v07_json); + assert_v07_introduces(r#""seatbelt": {}"#); } #[test] fn seatbelt_containment_value_is_introduced_in_v07() { - let v06_json = r#"{ - "version": "0.6.0-alpha", - "containment": "seatbelt", - "process": {"commandLine": "echo"}, - "seatbelt": {} - }"#; - - let v07_json = r#"{ - "version": "0.7.0-alpha", - "containment": "seatbelt", - "process": {"commandLine": "echo"}, - "seatbelt": {} - }"#; - - assert_v06_rejects_v07_accepts(v06_json, v07_json); + assert_v07_introduces(r#""containment": "seatbelt", "seatbelt": {}"#); } #[test] fn macos_sandbox_section_alias_is_introduced_in_v07() { - let v06_json = r#"{ - "version": "0.6.0-alpha", - "containment": "process", - "process": {"commandLine": "echo"}, - "macos_sandbox": {} - }"#; - - let v07_json = r#"{ - "version": "0.7.0-alpha", - "containment": "process", - "process": {"commandLine": "echo"}, - "macos_sandbox": {} - }"#; - - assert_v06_rejects_v07_accepts(v06_json, v07_json); + assert_v07_introduces(r#""macos_sandbox": {}"#); } #[test] fn macos_sandbox_containment_value_alias_is_introduced_in_v07() { - let v06_json = r#"{ - "version": "0.6.0-alpha", - "containment": "macos_sandbox", - "process": {"commandLine": "echo"}, - "macos_sandbox": {} - }"#; - - let v07_json = r#"{ - "version": "0.7.0-alpha", - "containment": "macos_sandbox", - "process": {"commandLine": "echo"}, - "macos_sandbox": {} - }"#; - - assert_v06_rejects_v07_accepts(v06_json, v07_json); + assert_v07_introduces(r#""containment": "macos_sandbox", "macos_sandbox": {}"#); }