diff --git a/src/core/wxc_common/src/config_contract_adapters/dev/mod.rs b/src/core/wxc_common/src/config_contract_adapters/dev/mod.rs new file mode 100644 index 000000000..83e878ea5 --- /dev/null +++ b/src/core/wxc_common/src/config_contract_adapters/dev/mod.rs @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +pub(crate) mod one_shot; diff --git a/src/core/wxc_common/src/config_contract_adapters/dev/one_shot.rs b/src/core/wxc_common/src/config_contract_adapters/dev/one_shot.rs new file mode 100644 index 000000000..5291720a3 --- /dev/null +++ b/src/core/wxc_common/src/config_contract_adapters/dev/one_shot.rs @@ -0,0 +1,398 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::wire; +use mxc_config_contract::dev as contract; +use mxc_config_contract::ContractVersion; + +fn convert_version(value: contract::Version) -> &'static str { + match value { + contract::Version::V0_8_0Alpha => ContractVersion::V0_8_0Alpha.as_str(), + } +} + +fn convert_containment(value: contract::OneShotContainment) -> wire::Containment { + match value { + contract::OneShotContainment::Process => wire::Containment::Process, + contract::OneShotContainment::ProcessContainer => wire::Containment::ProcessContainer, + contract::OneShotContainment::Lxc => wire::Containment::Lxc, + contract::OneShotContainment::Bubblewrap => wire::Containment::Bubblewrap, + contract::OneShotContainment::Seatbelt => wire::Containment::Seatbelt, + contract::OneShotContainment::Vm => wire::Containment::Vm, + contract::OneShotContainment::WindowsSandbox => wire::Containment::WindowsSandbox, + contract::OneShotContainment::Microvm => wire::Containment::Microvm, + contract::OneShotContainment::Hyperlight => wire::Containment::Hyperlight, + contract::OneShotContainment::Wslc => wire::Containment::Wslc, + contract::OneShotContainment::IsolationSession => wire::Containment::IsolationSession, + } +} + +fn convert_process(value: contract::Process) -> wire::Process { + let contract::Process { + command_line, + cwd, + env, + timeout, + } = value; + wire::Process { + command_line: Some(command_line.into_inner()), + cwd: cwd.into_option(), + env: env.into_option(), + timeout: timeout.into_option(), + } +} + +fn convert_lifecycle(value: contract::Lifecycle) -> wire::Lifecycle { + let contract::Lifecycle { + destroy_on_exit, + preserve_policy, + } = value; + wire::Lifecycle { + destroy_on_exit: destroy_on_exit.into_option(), + preserve_policy: preserve_policy.into_option(), + } +} + +fn convert_filesystem(value: contract::Filesystem) -> wire::Filesystem { + let contract::Filesystem { + readwrite_paths, + readonly_paths, + denied_paths, + } = value; + wire::Filesystem { + readwrite_paths: readwrite_paths.into_option(), + readonly_paths: readonly_paths.into_option(), + denied_paths: denied_paths.into_option(), + } +} + +fn convert_fallback(value: contract::Fallback) -> wire::Fallback { + let contract::Fallback { + allow_dacl_mutation, + } = value; + wire::Fallback { + allow_dacl_mutation: allow_dacl_mutation.into_option(), + } +} + +fn convert_default_network_policy(value: contract::DefaultNetworkPolicy) -> wire::NetworkPolicy { + match value { + contract::DefaultNetworkPolicy::Allow => wire::NetworkPolicy::Allow, + contract::DefaultNetworkPolicy::Block => wire::NetworkPolicy::Block, + } +} + +fn convert_network_enforcement_mode( + value: contract::NetworkEnforcementMode, +) -> wire::NetworkEnforcement { + match value { + contract::NetworkEnforcementMode::Capabilities => wire::NetworkEnforcement::Capabilities, + contract::NetworkEnforcementMode::Firewall => wire::NetworkEnforcement::Firewall, + contract::NetworkEnforcementMode::Both => wire::NetworkEnforcement::Both, + } +} + +fn convert_network(value: contract::Network) -> wire::Network { + let contract::Network { + default_policy, + enforcement_mode, + allow_local_network, + allowed_hosts, + blocked_hosts, + proxy, + } = value; + wire::Network { + default_policy: default_policy + .into_option() + .map(convert_default_network_policy), + enforcement_mode: enforcement_mode + .into_option() + .map(convert_network_enforcement_mode), + allow_local_network: allow_local_network.into_option(), + allowed_hosts: allowed_hosts.into_option(), + blocked_hosts: blocked_hosts.into_option(), + proxy: proxy.into_option().map(convert_proxy), + } +} + +fn convert_proxy(value: contract::NetworkProxy) -> wire::Proxy { + match value { + contract::NetworkProxy::Localhost(port) => wire::Proxy { + localhost: Some(port.get()), + builtin_test_server: None, + url: None, + }, + contract::NetworkProxy::BuiltinTestServer(contract::True) => wire::Proxy { + localhost: None, + builtin_test_server: Some(true), + url: None, + }, + contract::NetworkProxy::Url(url) => wire::Proxy { + localhost: None, + builtin_test_server: None, + url: Some(url), + }, + } +} + +fn convert_clipboard(value: contract::UiClipboard) -> wire::ClipboardPolicy { + match value { + contract::UiClipboard::None => wire::ClipboardPolicy::None, + contract::UiClipboard::Read => wire::ClipboardPolicy::Read, + contract::UiClipboard::Write => wire::ClipboardPolicy::Write, + contract::UiClipboard::All => wire::ClipboardPolicy::All, + } +} + +fn convert_ui(value: contract::Ui) -> wire::Ui { + let contract::Ui { + disable, + clipboard, + injection, + } = value; + wire::Ui { + disable: disable.into_option(), + clipboard: clipboard.into_option().map(convert_clipboard), + injection: injection.into_option(), + } +} + +fn convert_capture_denials_mode(value: contract::CaptureDenialsMode) -> wire::CaptureDenialsMode { + match value { + contract::CaptureDenialsMode::Block => wire::CaptureDenialsMode::Block, + contract::CaptureDenialsMode::Allow => wire::CaptureDenialsMode::Allow, + } +} + +fn convert_capture_denials(value: contract::CaptureDenials) -> wire::CaptureDenials { + let contract::CaptureDenials { + mode, + output_path, + retain_etl, + } = value; + wire::CaptureDenials { + mode: mode.into_option().map(convert_capture_denials_mode), + output_path: output_path.into_option(), + retain_etl: retain_etl.into_option(), + } +} + +fn convert_process_container(value: contract::ProcessContainer) -> wire::ProcessContainer { + let contract::ProcessContainer { + least_privilege, + learning_mode, + capabilities, + capture_denials, + ui, + } = value; + wire::ProcessContainer { + least_privilege: least_privilege.into_option(), + learning_mode: learning_mode.into_option(), + capabilities: capabilities.into_option(), + capture_denials: capture_denials.into_option().map(convert_capture_denials), + ui: ui.into_option().map(convert_process_container_ui), + } +} + +fn convert_process_container_ui_isolation( + value: contract::ProcessContainerUiIsolation, +) -> wire::UiIsolation { + match value { + contract::ProcessContainerUiIsolation::Container => wire::UiIsolation::Container, + contract::ProcessContainerUiIsolation::Desktop => wire::UiIsolation::Desktop, + contract::ProcessContainerUiIsolation::Handles => wire::UiIsolation::Handles, + contract::ProcessContainerUiIsolation::Atoms => wire::UiIsolation::Atoms, + } +} + +fn convert_process_container_ui(value: contract::ProcessContainerUi) -> wire::BaseProcessUi { + let contract::ProcessContainerUi { + isolation, + desktop_system_control, + system_settings, + ime, + } = value; + wire::BaseProcessUi { + isolation: isolation + .into_option() + .map(convert_process_container_ui_isolation), + desktop_system_control: desktop_system_control.into_option(), + system_settings: system_settings.into_option(), + ime: ime.into_option(), + } +} + +fn convert_lxc(value: contract::Lxc) -> wire::Lxc { + let contract::Lxc { + distribution, + release, + } = value; + wire::Lxc { + distribution: Some(distribution), + release: Some(release), + } +} + +fn convert_launch_method(value: contract::LaunchMethod) -> wire::LaunchMethod { + match value { + contract::LaunchMethod::Exec => wire::LaunchMethod::Exec, + contract::LaunchMethod::Open => wire::LaunchMethod::Open, + } +} + +fn convert_seatbelt(value: contract::Seatbelt) -> wire::Seatbelt { + let contract::Seatbelt { + profile_override, + gui_access, + launch_method, + nested_pty, + keychain_access, + extra_mach_lookups, + } = value; + wire::Seatbelt { + profile_override: profile_override.into_option(), + gui_access: gui_access.into_option(), + launch_method: launch_method.into_option().map(convert_launch_method), + nested_pty: nested_pty.into_option(), + keychain_access: keychain_access.into_option(), + extra_mach_lookups: extra_mach_lookups.into_option(), + } +} + +fn convert_test(value: contract::TestFeature) -> wire::TestFeature { + let contract::TestFeature { message } = value; + wire::TestFeature { + message: message.into_option(), + } +} + +fn convert_windows_sandbox(value: contract::OneShotWindowsSandbox) -> wire::WindowsSandbox { + let contract::OneShotWindowsSandbox { + idle_timeout, + idle_timeout_ms, + daemon_pipe_name, + } = value; + wire::WindowsSandbox { + idle_timeout: idle_timeout.into_option(), + idle_timeout_ms: idle_timeout_ms.into_option(), + daemon_pipe_name: daemon_pipe_name.into_option(), + } +} + +fn convert_protocol(value: contract::TransportProtocol) -> wire::TransportProtocol { + match value { + contract::TransportProtocol::Tcp => wire::TransportProtocol::Tcp, + } +} + +fn convert_wslc_port_mapping(value: contract::PortMapping) -> wire::PortMapping { + let contract::PortMapping { + windows_port, + container_port, + protocol, + } = value; + wire::PortMapping { + windows_port: windows_port.get(), + container_port: container_port.get(), + protocol: protocol.into_option().map(convert_protocol), + } +} + +fn convert_wslc(value: contract::OneShotWslc) -> wire::Wslc { + let contract::OneShotWslc { + target_os, + image, + image_tar_path, + cpu_count, + memory_mb, + gpu, + storage_path, + port_mappings, + } = value; + wire::Wslc { + provision: None, + target_os: target_os.into_option(), + image: image.into_option(), + image_tar_path: image_tar_path.into_option(), + cpu_count: cpu_count.into_option(), + memory_mb: memory_mb.into_option(), + gpu: gpu.into_option(), + storage_path: storage_path.into_option(), + port_mappings: port_mappings.into_option().map(|mappings| { + mappings + .into_iter() + .map(convert_wslc_port_mapping) + .collect() + }), + } +} + +fn convert_telemetry(value: contract::Telemetry) -> wire::Telemetry { + let contract::Telemetry { enabled } = value; + wire::Telemetry { + enabled: enabled.into_option(), + } +} + +fn convert_experimental(value: contract::OneShotExperimental) -> wire::Experimental { + let contract::OneShotExperimental { + test, + windows_sandbox, + wslc, + telemetry, + } = value; + wire::Experimental { + test: test.into_option().map(convert_test), + windows_sandbox: windows_sandbox.into_option().map(convert_windows_sandbox), + wslc: wslc.into_option().map(convert_wslc), + isolation_session: None, + seatbelt: None, + telemetry: telemetry.into_option().map(convert_telemetry), + } +} + +pub(crate) fn into_wire(request: contract::OneShotRequest) -> wire::MxcConfig { + let contract::OneShotRequest { + schema, + comment, + version, + container_id, + containment, + lifecycle, + process, + filesystem, + fallback, + network, + lxc, + process_container, + ui, + seatbelt, + experimental, + } = request; + wire::MxcConfig { + schema: schema.into_option(), + comment: comment.into_option(), + version: Some(convert_version(version).to_owned()), + phase: None, + sandbox_id: None, + correlation_vector: None, + container_id: container_id.into_option(), + containment: containment.into_option().map(convert_containment), + process: Some(convert_process(process)), + lifecycle: lifecycle.into_option().map(convert_lifecycle), + process_container: process_container + .into_option() + .map(convert_process_container), + lxc: lxc.into_option().map(convert_lxc), + filesystem: filesystem.into_option().map(convert_filesystem), + fallback: fallback.into_option().map(convert_fallback), + network: network.into_option().map(convert_network), + ui: ui.into_option().map(convert_ui), + seatbelt: seatbelt.into_option().map(convert_seatbelt), + experimental: experimental.into_option().map(convert_experimental), + } +} + +#[cfg(test)] +#[path = "one_shot_tests/mod.rs"] +mod tests; diff --git a/src/core/wxc_common/src/config_contract_adapters/dev/one_shot_tests/common.rs b/src/core/wxc_common/src/config_contract_adapters/dev/one_shot_tests/common.rs new file mode 100644 index 000000000..3f1b943c2 --- /dev/null +++ b/src/core/wxc_common/src/config_contract_adapters/dev/one_shot_tests/common.rs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use super::*; + +pub(super) struct ContainmentCase { + pub(super) input: &'static str, + pub(super) expected: &'static str, +} + +pub(super) fn request_with_containment(containment: &str) -> String { + format!( + r#"{{ + "version": "0.8.0-alpha", + "containment": "{containment}", + "process": {{"commandLine": "echo hello"}} + }}"# + ) +} + +pub(super) fn assert_matches_current_wire_deserialization(json: &str) { + let current: super::wire::MxcConfig = crate::config_deserialize::from_str(json).unwrap(); + let contract: super::contract::OneShotRequest = serde_json::from_str(json).unwrap(); + let adapted = super::into_wire(contract); + + assert_eq!( + serde_json::to_value(adapted).unwrap(), + serde_json::to_value(current).unwrap() + ); +} + +pub(super) fn adapt(json: &str) -> wire::MxcConfig { + let request: contract::OneShotRequest = serde_json::from_str(json).unwrap(); + into_wire(request) +} diff --git a/src/core/wxc_common/src/config_contract_adapters/dev/one_shot_tests/experimental.rs b/src/core/wxc_common/src/config_contract_adapters/dev/one_shot_tests/experimental.rs new file mode 100644 index 000000000..cc5d42df8 --- /dev/null +++ b/src/core/wxc_common/src/config_contract_adapters/dev/one_shot_tests/experimental.rs @@ -0,0 +1,227 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use super::common::{adapt, assert_matches_current_wire_deserialization, request_with_containment}; + +const TEST_FEATURE_AND_TELEMETRY_REQUEST_JSON: &str = r#"{ + "version": "0.8.0-alpha", + "containment": "process", + "process": { + "commandLine": "echo hello" + }, + "experimental": { + "test": { + "message": "test message" + }, + "telemetry": { + "enabled": false + } + } +}"#; + +const WINDOWS_SANDBOX_REQUEST_JSON: &str = r#"{ + "version": "0.8.0-alpha", + "containment": "windows_sandbox", + "process": { + "commandLine": "echo hello" + }, + "experimental": { + "windows_sandbox": { + "idleTimeoutMs": 60000, + "idleTimeout": 30, + "daemonPipeName": "custom-sandbox-pipe" + } + } +}"#; + +const WSLC_REQUEST_JSON: &str = r#"{ + "version": "0.8.0-alpha", + "containment": "wslc", + "process": { + "commandLine": "echo hello" + }, + "experimental": { + "wslc": { + "targetOs": "linux", + "image": "alpine:latest", + "imageTarPath": "C:\\images\\alpine.tar", + "cpuCount": 4, + "memoryMb": 4294967296, + "gpu": true, + "storagePath": "C:\\wslc", + "portMappings": [ + { + "windowsPort": 8080, + "containerPort": 80 + }, + { + "windowsPort": 8443, + "containerPort": 443, + "protocol": "tcp" + } + ] + } + } +}"#; + +#[test] +fn windows_sandbox_maps_expected_wire_fields() { + let wire = adapt(WINDOWS_SANDBOX_REQUEST_JSON); + + assert!(matches!( + wire.containment, + Some(super::wire::Containment::WindowsSandbox) + )); + + let experimental = wire.experimental.expect("experimental should be populated"); + + let windows_sandbox = experimental + .windows_sandbox + .expect("windows_sandbox should be populated"); + + assert_eq!(windows_sandbox.idle_timeout_ms, Some(60000)); + assert_eq!(windows_sandbox.idle_timeout, Some(30)); + assert_eq!( + windows_sandbox.daemon_pipe_name.as_deref(), + Some("custom-sandbox-pipe") + ); + + assert!(experimental.test.is_none()); + assert!(experimental.wslc.is_none()); + assert!(experimental.isolation_session.is_none()); + assert!(experimental.seatbelt.is_none()); + assert!(experimental.telemetry.is_none()); +} + +#[test] +fn test_feature_and_telemetry_map_expected_wire_fields() { + let wire = adapt(TEST_FEATURE_AND_TELEMETRY_REQUEST_JSON); + + let experimental = wire.experimental.expect("experimental should be populated"); + + let test = experimental.test.expect("test should be populated"); + assert_eq!(test.message.as_deref(), Some("test message")); + + let telemetry = experimental + .telemetry + .expect("telemetry should be populated"); + assert_eq!(telemetry.enabled, Some(false)); + + assert!(experimental.windows_sandbox.is_none()); + assert!(experimental.wslc.is_none()); + assert!(experimental.isolation_session.is_none()); + assert!(experimental.seatbelt.is_none()); +} + +#[test] +fn wslc_maps_expected_wire_fields() { + let wire = adapt(WSLC_REQUEST_JSON); + + assert!(matches!( + wire.containment, + Some(super::wire::Containment::Wslc) + )); + + let experimental = wire.experimental.expect("experimental should be populated"); + let wslc = experimental.wslc.expect("wslc should be populated"); + + assert_eq!(wslc.target_os.as_deref(), Some("linux")); + assert_eq!(wslc.image.as_deref(), Some("alpine:latest")); + assert_eq!( + wslc.image_tar_path.as_deref(), + Some(r"C:\images\alpine.tar") + ); + assert_eq!(wslc.cpu_count, Some(4)); + assert_eq!(wslc.memory_mb, Some(4294967296)); + assert_eq!(wslc.gpu, Some(true)); + assert_eq!(wslc.storage_path.as_deref(), Some(r"C:\wslc")); + assert!(wslc.provision.is_none()); + + let mappings = wslc + .port_mappings + .expect("portMappings should be populated"); + assert_eq!(mappings.len(), 2); + + assert_eq!(mappings[0].windows_port, 8080); + assert_eq!(mappings[0].container_port, 80); + assert!(mappings[0].protocol.is_none()); + + assert_eq!(mappings[1].windows_port, 8443); + assert_eq!(mappings[1].container_port, 443); + assert!(matches!( + &mappings[1].protocol, + Some(super::wire::TransportProtocol::Tcp) + )); + + assert!(experimental.test.is_none()); + assert!(experimental.windows_sandbox.is_none()); + assert!(experimental.isolation_session.is_none()); + assert!(experimental.seatbelt.is_none()); + assert!(experimental.telemetry.is_none()); +} + +#[test] +fn windows_sandbox_matches_current_wire_deserialization() { + assert_matches_current_wire_deserialization(WINDOWS_SANDBOX_REQUEST_JSON); +} + +#[test] +fn wslc_matches_current_wire_deserialization() { + assert_matches_current_wire_deserialization(WSLC_REQUEST_JSON); +} + +#[test] +fn test_feature_and_telemetry_match_current_wire_deserialization() { + assert_matches_current_wire_deserialization(TEST_FEATURE_AND_TELEMETRY_REQUEST_JSON); +} + +struct DevelopmentContainmentCase { + input: &'static str, + expected: &'static str, +} + +const DEVELOPMENT_CONTAINMENT_CASES: &[DevelopmentContainmentCase] = &[ + DevelopmentContainmentCase { + input: "vm", + expected: "vm", + }, + DevelopmentContainmentCase { + input: "windows_sandbox", + expected: "windows_sandbox", + }, + DevelopmentContainmentCase { + input: "microvm", + expected: "microvm", + }, + DevelopmentContainmentCase { + input: "hyperlight", + expected: "hyperlight", + }, + DevelopmentContainmentCase { + input: "wslc", + expected: "wslc", + }, + DevelopmentContainmentCase { + input: "isolation_session", + expected: "isolation_session", + }, +]; + +#[test] +fn development_containment_variants_map_expected_wire_values() { + for case in DEVELOPMENT_CONTAINMENT_CASES { + let wire = adapt(&request_with_containment(case.input)); + assert_eq!( + serde_json::to_value(wire.containment.unwrap()).unwrap(), + serde_json::json!(case.expected) + ); + } +} + +#[test] +fn development_containment_variants_match_current_wire_deserialization() { + for case in DEVELOPMENT_CONTAINMENT_CASES { + let json = request_with_containment(case.input); + assert_matches_current_wire_deserialization(&json); + } +} diff --git a/src/core/wxc_common/src/config_contract_adapters/dev/one_shot_tests/mod.rs b/src/core/wxc_common/src/config_contract_adapters/dev/one_shot_tests/mod.rs new file mode 100644 index 000000000..639ad5dad --- /dev/null +++ b/src/core/wxc_common/src/config_contract_adapters/dev/one_shot_tests/mod.rs @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use super::{contract, into_wire, wire}; + +mod common; +mod experimental; +mod stable_candidate; diff --git a/src/core/wxc_common/src/config_contract_adapters/dev/one_shot_tests/stable_candidate.rs b/src/core/wxc_common/src/config_contract_adapters/dev/one_shot_tests/stable_candidate.rs new file mode 100644 index 000000000..f91d4a254 --- /dev/null +++ b/src/core/wxc_common/src/config_contract_adapters/dev/one_shot_tests/stable_candidate.rs @@ -0,0 +1,1132 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use super::common::{ + adapt, assert_matches_current_wire_deserialization, request_with_containment, ContainmentCase, +}; + +const MINIMAL_REQUEST_JSON: &str = r#"{ + "version": "0.8.0-alpha", + "process": { + "commandLine": "echo hello" + } +}"#; + +const PROCESS_CONTAINER_REQUEST_JSON: &str = r#"{ + "version": "0.8.0-alpha", + "containerId": "container-id", + "containment": "processcontainer", + "lifecycle": { + "destroyOnExit": true, + "preservePolicy": true + }, + "process": { + "commandLine": "echo hello", + "cwd": "/home/user", + "env": [ + "VAR1=value1", "VAR2=value2" + ], + "timeout": 60 + }, + "filesystem": { + "readwritePaths": ["/path/to/readwrite"], + "readonlyPaths": ["/path/to/readonly"], + "deniedPaths": ["/path/to/denied"] + }, + "fallback": { + "allowDaclMutation": true + }, + "network": { + "defaultPolicy": "allow", + "enforcementMode": "capabilities", + "allowLocalNetwork": true, + "allowedHosts": ["example.com"], + "blockedHosts": ["blocked.com"], + "proxy": { + "localhost": 8080 + } + }, + "ui": { + "disable": true, + "clipboard": "all", + "injection": true + }, + "processContainer": { + "leastPrivilege": true, + "capabilities": ["cap1", "cap2"], + "ui": { + "isolation": "container", + "desktopSystemControl": true, + "systemSettings": "none", + "ime": true + } + } +}"#; + +const PROCESS_CONTAINER_ADDITIONS_REQUEST_JSON: &str = r#"{ + "version": "0.8.0-alpha", + "containment": "processcontainer", + "process": { + "commandLine": "echo hello" + }, + "processContainer": { + "learningMode": true, + "captureDenials": { + "mode": "block", + "outputPath": "C:\\denials.json", + "retainEtl": true + } + } +}"#; + +const LXC_REQUEST_JSON: &str = r#"{ + "version": "0.8.0-alpha", + "containerId": "container-id", + "containment": "lxc", + "lifecycle": { + "destroyOnExit": true, + "preservePolicy": true + }, + "process": { + "commandLine": "echo hello", + "cwd": "/home/user", + "env": [ + "VAR1=value1", "VAR2=value2" + ], + "timeout": 60 + }, + "filesystem": { + "readwritePaths": ["/path/to/readwrite"], + "readonlyPaths": ["/path/to/readonly"], + "deniedPaths": ["/path/to/denied"] + }, + "network": { + "defaultPolicy": "allow", + "enforcementMode": "firewall", + "allowLocalNetwork": true, + "allowedHosts": ["example.com"], + "blockedHosts": ["blocked.com"] + }, + "ui": { + "disable": true, + "clipboard": "all", + "injection": true + }, + "lxc": { + "distribution": "ubuntu", + "release": "20.04" + } +}"#; + +const SEATBELT_REQUEST_JSON: &str = r#"{ + "version": "0.8.0-alpha", + "containment": "seatbelt", + "process": { + "commandLine": "echo hello", + "cwd": "/home/user", + "env": [ + "VAR1=value1", "VAR2=value2" + ], + "timeout": 60 + }, + "filesystem": { + "readwritePaths": ["/path/to/readwrite"], + "readonlyPaths": ["/path/to/readonly"], + "deniedPaths": ["/path/to/denied"] + }, + "network": { + "defaultPolicy": "allow", + "enforcementMode": "firewall", + "allowLocalNetwork": true, + "allowedHosts": ["example.com"], + "blockedHosts": ["blocked.com"] + }, + "seatbelt": { + "profileOverride": "custom-profile.sb", + "guiAccess": true, + "launchMethod": "open", + "nestedPty": true, + "keychainAccess": true, + "extraMachLookups": ["com.example.service"] + } +}"#; + +const EMPTY_OPTIONAL_SECTIONS_REQUEST_JSON: &str = r#"{ + "version": "0.8.0-alpha", + "process": { + "commandLine": "echo hello" + }, + "lifecycle": {}, + "filesystem": {}, + "fallback": {}, + "network": {}, + "ui": {} +}"#; + +const EMPTY_PROCESS_CONTAINER_SECTION_REQUEST_JSON: &str = r#"{ + "version": "0.8.0-alpha", + "containment": "processcontainer", + "process": { + "commandLine": "echo hello" + }, + "processContainer": {} +}"#; + +const EMPTY_PROCESS_CONTAINER_UI_SECTION_REQUEST_JSON: &str = r#"{ + "version": "0.8.0-alpha", + "containment": "processcontainer", + "process": { + "commandLine": "echo hello" + }, + "processContainer": { + "ui": {} + } +}"#; + +const EMPTY_SEATBELT_SECTION_REQUEST_JSON: &str = r#"{ + "version": "0.8.0-alpha", + "containment": "seatbelt", + "process": { + "commandLine": "echo hello" + }, + "seatbelt": {} +}"#; + +const APP_CONTAINER_SECTION_ALIAS_REQUEST_JSON: &str = r#"{ + "version": "0.8.0-alpha", + "containment": "processcontainer", + "process": { + "commandLine": "echo hello" + }, + "appContainer": { + "leastPrivilege": true, + "capabilities": ["internetClient"] + } +}"#; + +const MACOS_SANDBOX_SECTION_ALIAS_REQUEST_JSON: &str = r#"{ + "version": "0.8.0-alpha", + "containment": "seatbelt", + "process": { + "commandLine": "echo hello" + }, + "macos_sandbox": { + "profileOverride": "custom-profile.sb", + "guiAccess": true, + "launchMethod": "open", + "nestedPty": true, + "keychainAccess": true, + "extraMachLookups": ["com.example.service"] + } +}"#; + +struct ProxyCase { + json: &'static str, + localhost: Option, + builtin_test_server: Option, + url: Option<&'static str>, +} + +const PROXY_CASES: &[ProxyCase] = &[ + ProxyCase { + json: r#"{"localhost": 8080}"#, + localhost: Some(8080), + builtin_test_server: None, + url: None, + }, + ProxyCase { + json: r#"{"builtinTestServer": true}"#, + localhost: None, + builtin_test_server: Some(true), + url: None, + }, + ProxyCase { + json: r#"{"url": "http://proxy.example:8080"}"#, + localhost: None, + builtin_test_server: None, + url: Some("http://proxy.example:8080"), + }, +]; + +const STABLE_CONTAINMENT_CASES: &[ContainmentCase] = &[ + ContainmentCase { + input: "process", + expected: "process", + }, + ContainmentCase { + input: "processcontainer", + expected: "processcontainer", + }, + ContainmentCase { + input: "appcontainer", + expected: "processcontainer", + }, + ContainmentCase { + input: "lxc", + expected: "lxc", + }, + ContainmentCase { + input: "bubblewrap", + expected: "bubblewrap", + }, + ContainmentCase { + input: "seatbelt", + expected: "seatbelt", + }, + ContainmentCase { + input: "macos_sandbox", + expected: "seatbelt", + }, +]; + +const DEFAULT_NETWORK_POLICY_CASES: &[&str] = &["allow", "block"]; + +const NETWORK_ENFORCEMENT_MODE_CASES: &[&str] = &["capabilities", "firewall", "both"]; + +const UI_CLIPBOARD_CASES: &[&str] = &["none", "read", "write", "all"]; + +const PROCESS_CONTAINER_UI_ISOLATION_CASES: &[&str] = &["container", "desktop", "handles", "atoms"]; + +const SEATBELT_LAUNCH_METHOD_CASES: &[&str] = &["exec", "open"]; + +const CAPTURE_DENIALS_MODE_CASES: &[&str] = &["block", "allow"]; + +fn request_with_comment(comment: &str) -> String { + format!( + r#"{{ + "version": "0.8.0-alpha", + "_comment": {comment}, + "process": {{"commandLine": "echo hello"}} + }}"# + ) +} + +fn request_with_proxy(proxy_json: &str) -> String { + format!( + r#"{{ + "version": "0.8.0-alpha", + "process": {{"commandLine": "echo hello"}}, + "network": {{"proxy": {proxy_json}}} + }}"# + ) +} + +fn request_with_default_network_policy(default_policy: &str) -> String { + format!( + r#"{{ + "version": "0.8.0-alpha", + "process": {{"commandLine": "echo hello"}}, + "network": {{"defaultPolicy": "{default_policy}"}} + }}"# + ) +} + +fn request_with_network_enforcement_mode(enforcement_mode: &str) -> String { + format!( + r#"{{ + "version": "0.8.0-alpha", + "process": {{"commandLine": "echo hello"}}, + "network": {{"enforcementMode": "{enforcement_mode}"}} + }}"# + ) +} + +fn request_with_ui_clipboard(clipboard: &str) -> String { + format!( + r#"{{ + "version": "0.8.0-alpha", + "process": {{"commandLine": "echo hello"}}, + "ui": {{"clipboard": "{clipboard}"}} + }}"# + ) +} + +fn request_with_process_container_ui_isolation(isolation: &str) -> String { + format!( + r#"{{ + "version": "0.8.0-alpha", + "process": {{"commandLine": "echo hello"}}, + "processContainer": {{"ui": {{"isolation": "{isolation}"}}}} + }}"# + ) +} + +fn request_with_capture_denials_mode(mode: &str) -> String { + format!( + r#"{{ + "version": "0.8.0-alpha", + "containment": "processcontainer", + "process": {{"commandLine": "echo"}}, + "processContainer": {{ + "captureDenials": {{"mode": "{mode}"}} + }} + }}"# + ) +} + +fn request_with_seatbelt_launch_method(launch_method: &str) -> String { + format!( + r#"{{ + "version": "0.8.0-alpha", + "process": {{"commandLine": "echo hello"}}, + "seatbelt": {{"launchMethod": "{launch_method}"}} + }}"# + ) +} + +#[test] +fn minimal_request_maps_expected_wire_fields() { + let json = MINIMAL_REQUEST_JSON; + + let wire = adapt(json); + + assert!(wire.schema.is_none()); + assert!(wire.comment.is_none()); + assert_eq!(wire.version, Some("0.8.0-alpha".to_string())); + assert!(wire.phase.is_none()); + assert!(wire.sandbox_id.is_none()); + assert!(wire.correlation_vector.is_none()); + assert!(wire.container_id.is_none()); + assert!(wire.containment.is_none()); + + let process = wire.process.expect("process should be populated"); + assert_eq!(process.command_line.as_deref(), Some("echo hello")); + assert!(process.cwd.is_none()); + assert!(process.env.is_none()); + assert!(process.timeout.is_none()); + + assert!(wire.lifecycle.is_none()); + assert!(wire.process_container.is_none()); + assert!(wire.lxc.is_none()); + assert!(wire.filesystem.is_none()); + assert!(wire.fallback.is_none()); + assert!(wire.network.is_none()); + assert!(wire.ui.is_none()); + assert!(wire.seatbelt.is_none()); + assert!(wire.experimental.is_none()); +} + +#[test] +fn process_container_request_maps_expected_wire_fields() { + let json = PROCESS_CONTAINER_REQUEST_JSON; + + let wire = adapt(json); + + assert!(wire.schema.is_none()); + assert!(wire.comment.is_none()); + assert_eq!(wire.version, Some("0.8.0-alpha".to_string())); + assert!(wire.phase.is_none()); + assert!(wire.sandbox_id.is_none()); + assert!(wire.correlation_vector.is_none()); + assert_eq!(wire.container_id.as_deref(), Some("container-id")); + assert!(matches!( + wire.containment, + Some(super::wire::Containment::ProcessContainer) + )); + + let process = wire.process.expect("process should be populated"); + assert_eq!(process.command_line.as_deref(), Some("echo hello")); + assert_eq!(process.cwd.as_deref(), Some("/home/user")); + assert_eq!( + process.env.unwrap().as_slice(), + &["VAR1=value1", "VAR2=value2"] + ); + assert_eq!(process.timeout, Some(60)); + + let lifecycle = wire.lifecycle.expect("lifecycle should be populated"); + assert_eq!(lifecycle.destroy_on_exit, Some(true)); + assert_eq!(lifecycle.preserve_policy, Some(true)); + + let filesystem = wire.filesystem.expect("filesystem should be populated"); + assert_eq!( + filesystem.readwrite_paths.unwrap().as_slice(), + &["/path/to/readwrite"] + ); + assert_eq!( + filesystem.readonly_paths.unwrap().as_slice(), + &["/path/to/readonly"] + ); + assert_eq!( + filesystem.denied_paths.unwrap().as_slice(), + &["/path/to/denied"] + ); + + let fallback = wire.fallback.expect("fallback should be populated"); + assert_eq!(fallback.allow_dacl_mutation, Some(true)); + + let network = wire.network.expect("network should be populated"); + assert!(matches!( + network.default_policy, + Some(super::wire::NetworkPolicy::Allow) + )); + assert!(matches!( + network.enforcement_mode, + Some(super::wire::NetworkEnforcement::Capabilities) + )); + assert_eq!(network.allow_local_network, Some(true)); + assert_eq!(network.allowed_hosts.unwrap().as_slice(), &["example.com"]); + assert_eq!(network.blocked_hosts.unwrap().as_slice(), &["blocked.com"]); + + let proxy = network.proxy.expect("proxy should be populated"); + assert_eq!(proxy.localhost, Some(8080)); + assert!(proxy.builtin_test_server.is_none()); + assert!(proxy.url.is_none()); + + let ui = wire.ui.expect("ui should be populated"); + assert_eq!(ui.disable, Some(true)); + assert!(matches!( + ui.clipboard, + Some(super::wire::ClipboardPolicy::All) + )); + assert_eq!(ui.injection, Some(true)); + + let process_container = wire + .process_container + .expect("process_container should be populated"); + assert_eq!(process_container.least_privilege, Some(true)); + assert!(process_container.learning_mode.is_none()); + assert_eq!( + process_container.capabilities.unwrap().as_slice(), + &["cap1", "cap2"] + ); + assert!(process_container.capture_denials.is_none()); + + let process_container_ui = process_container + .ui + .expect("process_container.ui should be populated"); + assert!(matches!( + process_container_ui.isolation, + Some(super::wire::UiIsolation::Container) + )); + assert_eq!(process_container_ui.desktop_system_control, Some(true)); + assert_eq!( + process_container_ui.system_settings, + Some("none".to_string()) + ); + assert_eq!(process_container_ui.ime, Some(true)); + + assert!(wire.lxc.is_none()); +} + +#[test] +fn lxc_request_maps_expected_wire_fields() { + let json = LXC_REQUEST_JSON; + + let wire = adapt(json); + + assert!(wire.schema.is_none()); + assert!(wire.comment.is_none()); + assert_eq!(wire.version, Some("0.8.0-alpha".to_string())); + assert!(wire.phase.is_none()); + assert!(wire.sandbox_id.is_none()); + assert!(wire.correlation_vector.is_none()); + assert_eq!(wire.container_id.as_deref(), Some("container-id")); + assert!(matches!( + wire.containment, + Some(super::wire::Containment::Lxc) + )); + + let process = wire.process.expect("process should be populated"); + assert_eq!(process.command_line.as_deref(), Some("echo hello")); + assert_eq!(process.cwd.as_deref(), Some("/home/user")); + assert_eq!( + process.env.unwrap().as_slice(), + &["VAR1=value1", "VAR2=value2"] + ); + assert_eq!(process.timeout, Some(60)); + + let lifecycle = wire.lifecycle.expect("lifecycle should be populated"); + assert_eq!(lifecycle.destroy_on_exit, Some(true)); + assert_eq!(lifecycle.preserve_policy, Some(true)); + + let filesystem = wire.filesystem.expect("filesystem should be populated"); + assert_eq!( + filesystem.readwrite_paths.unwrap().as_slice(), + &["/path/to/readwrite"] + ); + assert_eq!( + filesystem.readonly_paths.unwrap().as_slice(), + &["/path/to/readonly"] + ); + assert_eq!( + filesystem.denied_paths.unwrap().as_slice(), + &["/path/to/denied"] + ); + + let network = wire.network.expect("network should be populated"); + assert!(matches!( + network.default_policy, + Some(super::wire::NetworkPolicy::Allow) + )); + assert!(matches!( + network.enforcement_mode, + Some(super::wire::NetworkEnforcement::Firewall) + )); + assert_eq!(network.allow_local_network, Some(true)); + assert_eq!(network.allowed_hosts.unwrap().as_slice(), &["example.com"]); + assert_eq!(network.blocked_hosts.unwrap().as_slice(), &["blocked.com"]); + assert!(network.proxy.is_none()); + + let ui = wire.ui.expect("ui should be populated"); + assert_eq!(ui.disable, Some(true)); + assert!(matches!( + ui.clipboard, + Some(super::wire::ClipboardPolicy::All) + )); + assert_eq!(ui.injection, Some(true)); + + let lxc = wire.lxc.expect("lxc should be populated"); + assert_eq!(lxc.distribution.as_deref(), Some("ubuntu")); + assert_eq!(lxc.release.as_deref(), Some("20.04")); +} + +#[test] +fn seatbelt_request_maps_expected_wire_fields() { + let json = SEATBELT_REQUEST_JSON; + + let wire = adapt(json); + + assert!(wire.schema.is_none()); + assert!(wire.comment.is_none()); + assert_eq!(wire.version, Some("0.8.0-alpha".to_string())); + assert!(wire.phase.is_none()); + assert!(wire.sandbox_id.is_none()); + assert!(wire.correlation_vector.is_none()); + assert!(wire.container_id.is_none()); + assert!(matches!( + wire.containment, + Some(super::wire::Containment::Seatbelt) + )); + + let process = wire.process.expect("process should be populated"); + assert_eq!(process.command_line.as_deref(), Some("echo hello")); + assert_eq!(process.cwd.as_deref(), Some("/home/user")); + assert_eq!( + process.env.unwrap().as_slice(), + &["VAR1=value1", "VAR2=value2"] + ); + assert_eq!(process.timeout, Some(60)); + + let filesystem = wire.filesystem.expect("filesystem should be populated"); + assert_eq!( + filesystem.readwrite_paths.unwrap().as_slice(), + &["/path/to/readwrite"] + ); + assert_eq!( + filesystem.readonly_paths.unwrap().as_slice(), + &["/path/to/readonly"] + ); + assert_eq!( + filesystem.denied_paths.unwrap().as_slice(), + &["/path/to/denied"] + ); + + let network = wire.network.expect("network should be populated"); + assert!(matches!( + network.default_policy, + Some(super::wire::NetworkPolicy::Allow) + )); + assert_eq!(network.allow_local_network, Some(true)); + assert!(matches!( + network.enforcement_mode, + Some(super::wire::NetworkEnforcement::Firewall) + )); + assert_eq!(network.allowed_hosts.unwrap().as_slice(), &["example.com"]); + assert_eq!(network.blocked_hosts.unwrap().as_slice(), &["blocked.com"]); + assert!(network.proxy.is_none()); + + let seatbelt = wire.seatbelt.expect("seatbelt should be populated"); + assert_eq!( + seatbelt.profile_override.as_deref(), + Some("custom-profile.sb") + ); + assert_eq!(seatbelt.gui_access, Some(true)); + assert!(matches!( + seatbelt.launch_method, + Some(super::wire::LaunchMethod::Open) + )); + assert_eq!(seatbelt.nested_pty, Some(true)); + assert_eq!(seatbelt.keychain_access, Some(true)); + assert_eq!( + seatbelt.extra_mach_lookups.unwrap().as_slice(), + &["com.example.service"] + ); +} + +#[test] +fn empty_optional_sections_map_to_present_empty_wire_sections() { + let wire = adapt(EMPTY_OPTIONAL_SECTIONS_REQUEST_JSON); + + let lifecycle = wire.lifecycle.expect("lifecycle should be populated"); + assert!(lifecycle.destroy_on_exit.is_none()); + assert!(lifecycle.preserve_policy.is_none()); + + let filesystem = wire.filesystem.expect("filesystem should be populated"); + assert!(filesystem.readwrite_paths.is_none()); + assert!(filesystem.readonly_paths.is_none()); + assert!(filesystem.denied_paths.is_none()); + + let fallback = wire.fallback.expect("fallback should be populated"); + assert!(fallback.allow_dacl_mutation.is_none()); + + let network = wire.network.expect("network should be populated"); + assert!(network.default_policy.is_none()); + assert!(network.enforcement_mode.is_none()); + assert!(network.allow_local_network.is_none()); + assert!(network.allowed_hosts.is_none()); + assert!(network.blocked_hosts.is_none()); + assert!(network.proxy.is_none()); + + let ui = wire.ui.expect("ui should be populated"); + assert!(ui.disable.is_none()); + assert!(ui.clipboard.is_none()); + assert!(ui.injection.is_none()); +} + +#[test] +fn empty_process_container_section_maps_to_present_empty_wire_sections() { + let wire = adapt(EMPTY_PROCESS_CONTAINER_SECTION_REQUEST_JSON); + + let process_container = wire + .process_container + .expect("processContainer should be populated"); + assert!(process_container.least_privilege.is_none()); + assert!(process_container.learning_mode.is_none()); + assert!(process_container.capabilities.is_none()); + assert!(process_container.capture_denials.is_none()); + assert!(process_container.ui.is_none()); +} + +#[test] +fn empty_process_container_ui_section_maps_to_present_empty_wire_section() { + let wire = adapt(EMPTY_PROCESS_CONTAINER_UI_SECTION_REQUEST_JSON); + + let process_container = wire + .process_container + .expect("processContainer should be populated"); + + assert!(process_container.least_privilege.is_none()); + assert!(process_container.learning_mode.is_none()); + assert!(process_container.capabilities.is_none()); + assert!(process_container.capture_denials.is_none()); + + let ui = process_container + .ui + .expect("processContainer.ui should be populated"); + + assert!(ui.isolation.is_none()); + assert!(ui.desktop_system_control.is_none()); + assert!(ui.system_settings.is_none()); + assert!(ui.ime.is_none()); +} + +#[test] +fn empty_seatbelt_section_maps_to_present_empty_wire_section() { + let wire = adapt(EMPTY_SEATBELT_SECTION_REQUEST_JSON); + + let seatbelt = wire.seatbelt.expect("seatbelt should be populated"); + assert!(seatbelt.profile_override.is_none()); + assert!(seatbelt.gui_access.is_none()); + assert!(seatbelt.launch_method.is_none()); + assert!(seatbelt.nested_pty.is_none()); + assert!(seatbelt.keychain_access.is_none()); + assert!(seatbelt.extra_mach_lookups.is_none()); +} + +#[test] +fn annotations_map_expected_wire_fields() { + let json = r#"{ + "$schema": "https://example.com/schema.json", + "_comment": "This is a comment", + "version": "0.8.0-alpha", + "process": {"commandLine": "echo hello"} + }"#; + + let wire = adapt(json); + + assert_eq!( + wire.schema.as_deref(), + Some("https://example.com/schema.json") + ); + + assert_eq!( + wire.comment.as_ref(), + Some(&serde_json::json!("This is a comment")) + ); +} + +#[test] +fn comment_values_map_expected_wire_fields() { + struct CommentCase { + json: &'static str, + expected: serde_json::Value, + } + + let cases: &[CommentCase] = &[ + CommentCase { + json: r#""plain comment""#, + expected: serde_json::json!("plain comment"), + }, + CommentCase { + json: r#"{"purpose":"test","enabled":true}"#, + expected: serde_json::json!({ + "purpose": "test", + "enabled": true + }), + }, + CommentCase { + json: r#"["first", 2, false]"#, + expected: serde_json::json!(["first", 2, false]), + }, + CommentCase { + json: "42", + expected: serde_json::json!(42), + }, + CommentCase { + json: "true", + expected: serde_json::json!(true), + }, + ]; + + for case in cases { + let json = request_with_comment(case.json); + + let wire = adapt(&json); + + assert_eq!( + wire.comment.as_ref(), + Some(&case.expected), + "comment value did not match expected for input: {}", + case.json + ); + } +} + +#[test] +fn null_comment_maps_expected_wire_field() { + let json = request_with_comment("null"); + + let wire = adapt(&json); + + assert_eq!(wire.comment.as_ref(), Some(&serde_json::Value::Null)); +} + +#[test] +fn proxy_variants_map_expected_wire_fields() { + for case in PROXY_CASES { + let json = request_with_proxy(case.json); + + let wire = adapt(&json); + let proxy = wire + .network + .expect("network should be populated") + .proxy + .expect("proxy should be populated"); + + assert_eq!(proxy.localhost, case.localhost); + assert_eq!(proxy.builtin_test_server, case.builtin_test_server); + assert_eq!(proxy.url.as_deref(), case.url); + } +} + +#[test] +fn enum_variants_map_expected_wire_values() { + for case in STABLE_CONTAINMENT_CASES { + let json = request_with_containment(case.input); + let wire = adapt(&json); + + assert_eq!( + serde_json::to_value(wire.containment.unwrap()).unwrap(), + serde_json::json!(case.expected) + ); + } + + for default_network_policy in DEFAULT_NETWORK_POLICY_CASES { + let json = request_with_default_network_policy(default_network_policy); + let wire = adapt(&json); + + assert_eq!( + serde_json::to_value( + wire.network + .unwrap() + .default_policy + .expect("defaultPolicy should be populated") + ) + .unwrap(), + serde_json::json!(default_network_policy) + ); + } + + for network_enforcement_mode in NETWORK_ENFORCEMENT_MODE_CASES { + let json = request_with_network_enforcement_mode(network_enforcement_mode); + let wire = adapt(&json); + + assert_eq!( + serde_json::to_value( + wire.network + .unwrap() + .enforcement_mode + .expect("enforcementMode should be populated") + ) + .unwrap(), + serde_json::json!(network_enforcement_mode) + ); + } + + for ui_clipboard in UI_CLIPBOARD_CASES { + let json = request_with_ui_clipboard(ui_clipboard); + let wire = adapt(&json); + + assert_eq!( + serde_json::to_value( + wire.ui + .unwrap() + .clipboard + .expect("clipboard should be populated") + ) + .unwrap(), + serde_json::json!(ui_clipboard) + ); + } + + for process_container_ui_isolation in PROCESS_CONTAINER_UI_ISOLATION_CASES { + let json = request_with_process_container_ui_isolation(process_container_ui_isolation); + let wire = adapt(&json); + + assert_eq!( + serde_json::to_value( + wire.process_container + .unwrap() + .ui + .unwrap() + .isolation + .expect("isolation should be populated") + ) + .unwrap(), + serde_json::json!(process_container_ui_isolation) + ); + } + + for capture_denials_mode in CAPTURE_DENIALS_MODE_CASES { + let json = request_with_capture_denials_mode(capture_denials_mode); + let wire = adapt(&json); + + assert_eq!( + serde_json::to_value( + wire.process_container + .unwrap() + .capture_denials + .unwrap() + .mode + .expect("mode should be populated") + ) + .unwrap(), + serde_json::json!(capture_denials_mode) + ); + } + + for launch_method in SEATBELT_LAUNCH_METHOD_CASES { + let json = request_with_seatbelt_launch_method(launch_method); + let wire = adapt(&json); + + assert_eq!( + serde_json::to_value( + wire.seatbelt + .unwrap() + .launch_method + .expect("launchMethod should be populated") + ) + .unwrap(), + serde_json::json!(launch_method) + ); + } +} + +#[test] +fn app_container_section_alias_maps_expected_wire_fields() { + let wire = adapt(APP_CONTAINER_SECTION_ALIAS_REQUEST_JSON); + let process_container = wire + .process_container + .expect("appContainer should map to process_container"); + + assert_eq!(process_container.least_privilege, Some(true)); + assert_eq!( + process_container.capabilities.unwrap().as_slice(), + &["internetClient"] + ); + assert!(process_container.learning_mode.is_none()); + assert!(process_container.capture_denials.is_none()); + assert!(process_container.ui.is_none()); +} + +#[test] +fn macos_sandbox_section_alias_maps_expected_wire_fields() { + let wire = adapt(MACOS_SANDBOX_SECTION_ALIAS_REQUEST_JSON); + let seatbelt = wire.seatbelt.expect("macos_sandbox should map to seatbelt"); + + assert_eq!( + seatbelt.profile_override.as_deref(), + Some("custom-profile.sb") + ); + assert_eq!(seatbelt.gui_access, Some(true)); + assert!(matches!( + seatbelt.launch_method, + Some(super::wire::LaunchMethod::Open) + )); + assert_eq!(seatbelt.nested_pty, Some(true)); + assert_eq!(seatbelt.keychain_access, Some(true)); + assert_eq!( + seatbelt.extra_mach_lookups.unwrap().as_slice(), + &["com.example.service"] + ); +} + +#[test] +fn process_container_additions_map_expected_wire_fields() { + let wire = adapt(PROCESS_CONTAINER_ADDITIONS_REQUEST_JSON); + + assert!(matches!( + wire.containment, + Some(super::wire::Containment::ProcessContainer) + )); + + let process_container = wire + .process_container + .expect("processContainer should be populated"); + + assert_eq!(process_container.learning_mode, Some(true)); + assert!(process_container.least_privilege.is_none()); + assert!(process_container.capabilities.is_none()); + assert!(process_container.ui.is_none()); + + let capture = process_container + .capture_denials + .expect("captureDenials should be populated"); + + assert!(matches!( + capture.mode, + Some(super::wire::CaptureDenialsMode::Block) + )); + assert_eq!(capture.output_path.as_deref(), Some(r"C:\denials.json")); + assert_eq!(capture.retain_etl, Some(true)); +} + +#[test] +fn minimal_request_matches_current_wire_deserialization() { + let json = MINIMAL_REQUEST_JSON; + assert_matches_current_wire_deserialization(json); +} + +#[test] +fn process_container_request_matches_current_wire_deserialization() { + let json = PROCESS_CONTAINER_REQUEST_JSON; + assert_matches_current_wire_deserialization(json); +} + +#[test] +fn lxc_request_matches_current_wire_deserialization() { + let json = LXC_REQUEST_JSON; + assert_matches_current_wire_deserialization(json); +} + +#[test] +fn capture_denials_mode_variants_match_current_wire_deserialization() { + for case in CAPTURE_DENIALS_MODE_CASES { + let json = request_with_capture_denials_mode(case); + assert_matches_current_wire_deserialization(&json); + } +} + +#[test] +fn process_container_additions_match_current_wire_deserialization() { + assert_matches_current_wire_deserialization(PROCESS_CONTAINER_ADDITIONS_REQUEST_JSON); +} + +#[test] +fn seatbelt_request_matches_current_wire_deserialization() { + let json = SEATBELT_REQUEST_JSON; + assert_matches_current_wire_deserialization(json); +} + +#[test] +fn empty_optional_sections_match_current_wire_deserialization() { + assert_matches_current_wire_deserialization(EMPTY_OPTIONAL_SECTIONS_REQUEST_JSON); +} + +#[test] +fn empty_process_container_section_matches_current_wire_deserialization() { + assert_matches_current_wire_deserialization(EMPTY_PROCESS_CONTAINER_SECTION_REQUEST_JSON); +} + +#[test] +fn empty_process_container_ui_section_matches_current_wire_deserialization() { + assert_matches_current_wire_deserialization(EMPTY_PROCESS_CONTAINER_UI_SECTION_REQUEST_JSON); +} + +#[test] +fn empty_seatbelt_section_matches_current_wire_deserialization() { + assert_matches_current_wire_deserialization(EMPTY_SEATBELT_SECTION_REQUEST_JSON); +} + +#[test] +fn proxy_variants_match_current_wire_deserialization() { + for case in PROXY_CASES { + let json = request_with_proxy(case.json); + assert_matches_current_wire_deserialization(&json); + } +} + +#[test] +fn enum_variants_match_current_wire_deserialization() { + for case in STABLE_CONTAINMENT_CASES { + let json = request_with_containment(case.input); + assert_matches_current_wire_deserialization(&json); + } + + for default_policy in DEFAULT_NETWORK_POLICY_CASES { + let json = request_with_default_network_policy(default_policy); + assert_matches_current_wire_deserialization(&json); + } + + for enforcement_mode in NETWORK_ENFORCEMENT_MODE_CASES { + let json = request_with_network_enforcement_mode(enforcement_mode); + assert_matches_current_wire_deserialization(&json); + } + + for clipboard in UI_CLIPBOARD_CASES { + let json = request_with_ui_clipboard(clipboard); + assert_matches_current_wire_deserialization(&json); + } + + for isolation in PROCESS_CONTAINER_UI_ISOLATION_CASES { + let json = request_with_process_container_ui_isolation(isolation); + assert_matches_current_wire_deserialization(&json); + } + + for launch_method in SEATBELT_LAUNCH_METHOD_CASES { + let json = request_with_seatbelt_launch_method(launch_method); + assert_matches_current_wire_deserialization(&json); + } +} + +#[test] +fn app_container_section_alias_matches_current_wire_deserialization() { + assert_matches_current_wire_deserialization(APP_CONTAINER_SECTION_ALIAS_REQUEST_JSON); +} + +#[test] +fn macos_sandbox_section_alias_matches_current_wire_deserialization() { + assert_matches_current_wire_deserialization(MACOS_SANDBOX_SECTION_ALIAS_REQUEST_JSON); +} + +#[test] +fn annotations_match_current_wire_deserialization() { + let json = r#"{ + "$schema": "https://example.com/schema.json", + "_comment": "This is a comment", + "version": "0.8.0-alpha", + "process": {"commandLine": "echo hello"} + }"#; + + assert_matches_current_wire_deserialization(json); +} diff --git a/src/core/wxc_common/src/config_contract_adapters/mod.rs b/src/core/wxc_common/src/config_contract_adapters/mod.rs index 947fde8d8..6399b0ac2 100644 --- a/src/core/wxc_common/src/config_contract_adapters/mod.rs +++ b/src/core/wxc_common/src/config_contract_adapters/mod.rs @@ -1,12 +1,17 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -// Phase 3 introduces and tests the adapter before Phase 6 wires it into +// Phase 3 introduces and tests the adapter before Phase 8 wires it into // shadow exact-contract dispatch. #[cfg_attr(not(test), allow(dead_code))] pub(crate) mod v0_6; -// Phase 4 introduces and tests the adapter before Phase 6 wires it into +// Phase 4 introduces and tests the adapter before Phase 8 wires it into // shadow exact-contract dispatch. #[cfg_attr(not(test), allow(dead_code))] pub(crate) mod v0_7; + +// Phase 5 introduces and tests the adapter before Phase 8 wires it into +// shadow exact-contract dispatch. +#[cfg_attr(not(test), allow(dead_code))] +pub(crate) mod dev;