diff --git a/build.ps1 b/build.ps1 index 08df26a9a..6f076e082 100755 --- a/build.ps1 +++ b/build.ps1 @@ -60,7 +60,8 @@ $filesForWindowsPackage = @( 'RunCommandOnSet.dsc.resource.json', 'RunCommandOnSet.exe', 'sshdconfig.exe', - 'sshdconfig.dsc.resource.json', + 'sshd-windows.dsc.resource.json', + 'sshd_config.dsc.resource.json', 'windowspowershell.dsc.resource.json', 'wmi.dsc.resource.json', 'wmi.resource.ps1', @@ -87,7 +88,7 @@ $filesForLinuxPackage = @( 'RunCommandOnSet.dsc.resource.json', 'runcommandonset', 'sshdconfig', - 'sshdconfig.dsc.resource.json' + 'sshd_config.dsc.resource.json' ) $filesForMacPackage = @( @@ -109,7 +110,7 @@ $filesForMacPackage = @( 'RunCommandOnSet.dsc.resource.json', 'runcommandonset', 'sshdconfig', - 'sshdconfig.dsc.resource.json' + 'sshd_config.dsc.resource.json' ) # the list of files other than the binaries which need to be executable @@ -173,7 +174,7 @@ if ($null -ne $packageType) { & $rustup default stable - ## Test if Node is installed + ## Test if Node is installed ## Skipping upgrade as users may have a specific version they want to use if (!(Get-Command 'node' -ErrorAction Ignore)) { Write-Verbose -Verbose "Node.js not found, installing..." diff --git a/sshdconfig/Cargo.lock b/sshdconfig/Cargo.lock index 73c9d0912..d63fad980 100644 --- a/sshdconfig/Cargo.lock +++ b/sshdconfig/Cargo.lock @@ -26,6 +26,62 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.59.0", +] + +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + [[package]] name = "atty" version = "0.2.14" @@ -43,6 +99,37 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "base62" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10e52a7bcb1d6beebee21fb5053af9e3cbb7a7ed1a4909e534040e676437ab1f" +dependencies = [ + "rustversion", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" + +[[package]] +name = "bstr" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "bumpalo" version = "3.19.0" @@ -78,30 +165,219 @@ dependencies = [ "windows-link", ] +[[package]] +name = "clap" +version = "4.5.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be92d32e80243a54711e5d7ce823c35c41c9d929dc4ab58e1276f625841aadf9" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707eab41e9622f9139419d573eca0900137718000c517d47da73045f54331c3d" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "convert_case" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crossterm" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" +dependencies = [ + "bitflags 2.9.1", + "crossterm_winapi", + "derive_more", + "document-features", + "mio", + "parking_lot", + "rustix", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "derive_more" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "document-features" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" +dependencies = [ + "litrs", +] + [[package]] name = "dyn-clone" version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005" +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "errno" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "glob" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" + +[[package]] +name = "globset" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54a1028dfc5f5df5da8a56a73e6c153c9a9708ec57232470703592a3f18e49f5" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", +] + +[[package]] +name = "globwalk" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc" +dependencies = [ + "bitflags 1.3.2", + "ignore", + "walkdir", +] + [[package]] name = "hashbrown" version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.1.19" @@ -123,7 +399,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core", + "windows-core 0.61.2", ] [[package]] @@ -135,6 +411,22 @@ dependencies = [ "cc", ] +[[package]] +name = "ignore" +version = "0.4.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d89fd380afde86567dfba715db065673989d6253f42b88179abd3eae47bda4b" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata 0.4.9", + "same-file", + "walkdir", + "winapi-util", +] + [[package]] name = "indexmap" version = "2.10.0" @@ -145,6 +437,21 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.15" @@ -173,18 +480,70 @@ version = "0.2.174" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "litrs" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" + +[[package]] +name = "lock_api" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + [[package]] name = "memchr" version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +[[package]] +name = "mio" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.59.0", +] + +[[package]] +name = "normpath" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8911957c4b1549ac0dc74e30db9c8b0e66ddcd6d7acc33098f4c63a64a6d7ed" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -210,12 +569,41 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + [[package]] name = "overload" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "parking_lot" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -240,6 +628,15 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "redox_syscall" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" +dependencies = [ + "bitflags 2.9.1", +] + [[package]] name = "regex" version = "1.11.1" @@ -248,8 +645,17 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata", - "regex-syntax", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", ] [[package]] @@ -260,15 +666,119 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-syntax 0.8.5", ] +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + [[package]] name = "regex-syntax" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "registry" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "515143bd3c240fd5a47002a552fd7eba71acf8cd3cf7472e5ec392cda2ed3d90" +dependencies = [ + "bitflags 1.3.2", + "log", + "thiserror 1.0.69", + "utfx", + "windows", +] + +[[package]] +name = "registry_lib" +version = "0.1.0" +dependencies = [ + "cc", + "crossterm", + "registry", + "rust-i18n", + "schemars", + "serde", + "serde_json", + "static_vcruntime", + "thiserror 2.0.12", + "tracing", + "tracing-subscriber", + "utfx", +] + +[[package]] +name = "rust-i18n" +version = "3.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda2551fdfaf6cc5ee283adc15e157047b92ae6535cf80f6d4962d05717dc332" +dependencies = [ + "globwalk", + "once_cell", + "regex", + "rust-i18n-macro", + "rust-i18n-support", + "smallvec", +] + +[[package]] +name = "rust-i18n-macro" +version = "3.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22baf7d7f56656d23ebe24f6bb57a5d40d2bce2a5f1c503e692b5b2fa450f965" +dependencies = [ + "glob", + "once_cell", + "proc-macro2", + "quote", + "rust-i18n-support", + "serde", + "serde_json", + "serde_yaml", + "syn", +] + +[[package]] +name = "rust-i18n-support" +version = "3.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940ed4f52bba4c0152056d771e563b7133ad9607d4384af016a134b58d758f19" +dependencies = [ + "arc-swap", + "base62", + "globwalk", + "itertools", + "lazy_static", + "normpath", + "once_cell", + "proc-macro2", + "regex", + "serde", + "serde_json", + "serde_yaml", + "siphasher", + "toml", + "triomphe", +] + +[[package]] +name = "rustix" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +dependencies = [ + "bitflags 2.9.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + [[package]] name = "rustversion" version = "1.0.21" @@ -281,6 +791,15 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "schemars" version = "0.8.22" @@ -305,6 +824,12 @@ dependencies = [ "syn", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "serde" version = "1.0.219" @@ -349,6 +874,28 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -364,6 +911,42 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" +dependencies = [ + "libc", +] + +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + [[package]] name = "smallvec" version = "1.15.1" @@ -377,10 +960,13 @@ dependencies = [ "atty", "cc", "chrono", + "clap", + "registry_lib", + "rust-i18n", "schemars", "serde", "serde_json", - "thiserror", + "thiserror 2.0.12", "tracing", "tracing-subscriber", "tree-sitter", @@ -388,12 +974,30 @@ dependencies = [ "tree-sitter-ssh-server-config", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "static_vcruntime" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "954e3e877803def9dc46075bf4060147c55cd70db97873077232eae0269dc89b" + [[package]] name = "streaming-iterator" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b2231b7c3057d5e4ad0156fb3dc807d900806020c5ffa3ee6ff2c8c76fb8520" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "syn" version = "2.0.104" @@ -405,13 +1009,33 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.12", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -434,6 +1058,47 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + [[package]] name = "tracing" version = "0.1.41" @@ -477,18 +1142,35 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", +] + [[package]] name = "tracing-subscriber" version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" dependencies = [ + "matchers", "nu-ansi-term", + "once_cell", + "regex", + "serde", + "serde_json", "sharded-slab", "smallvec", "thread_local", + "tracing", "tracing-core", "tracing-log", + "tracing-serde", ] [[package]] @@ -499,7 +1181,7 @@ checksum = "a7cf18d43cbf0bfca51f657132cc616a5097edc4424d538bae6fa60142eaf9f0" dependencies = [ "cc", "regex", - "regex-syntax", + "regex-syntax 0.8.5", "serde_json", "streaming-iterator", "tree-sitter-language", @@ -531,18 +1213,69 @@ dependencies = [ "tree-sitter-rust", ] +[[package]] +name = "triomphe" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef8f7726da4807b58ea5c96fdc122f80702030edc33b35aff9190a51148ccc85" +dependencies = [ + "arc-swap", + "serde", + "stable_deref_trait", +] + [[package]] name = "unicode-ident" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "utfx" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "133bf74f01486773317ddfcde8e2e20d2933cc3b68ab797e5d718bef996a81de" + [[package]] name = "valuable" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + [[package]] name = "wasm-bindgen" version = "0.2.100" @@ -617,23 +1350,66 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +dependencies = [ + "windows-core 0.58.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +dependencies = [ + "windows-implement 0.58.0", + "windows-interface 0.58.0", + "windows-result 0.2.0", + "windows-strings 0.1.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows-core" version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ - "windows-implement", - "windows-interface", + "windows-implement 0.60.0", + "windows-interface 0.59.1", "windows-link", - "windows-result", - "windows-strings", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-implement" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -647,6 +1423,17 @@ dependencies = [ "syn", ] +[[package]] +name = "windows-interface" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-interface" version = "0.59.1" @@ -664,6 +1451,15 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-result" version = "0.3.4" @@ -673,6 +1469,16 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result 0.2.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows-strings" version = "0.4.2" @@ -681,3 +1487,158 @@ checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ "windows-link", ] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" +dependencies = [ + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + +[[package]] +name = "winnow" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd" +dependencies = [ + "memchr", +] diff --git a/sshdconfig/Cargo.toml b/sshdconfig/Cargo.toml index b23bc67ca..a46ee2889 100644 --- a/sshdconfig/Cargo.toml +++ b/sshdconfig/Cargo.toml @@ -16,6 +16,8 @@ lto = true [dependencies] atty = { version = "0.2" } chrono = { version = "0.4" } +clap = { version = "4.5", features = ["derive"] } +rust-i18n = { version = "3.1" } schemars = "0.8" serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0", features = ["preserve_order"] } @@ -26,5 +28,8 @@ tree-sitter = "0.25" tree-sitter-rust = "0.24" tree-sitter-ssh-server-config = { path = "../tree-sitter-ssh-server-config" } +[target.'cfg(windows)'.dependencies] +registry_lib = { path = "../registry_lib" } + [build-dependencies] cc="*" diff --git a/sshdconfig/locales/en-us.toml b/sshdconfig/locales/en-us.toml new file mode 100644 index 000000000..3ad1ee312 --- /dev/null +++ b/sshdconfig/locales/en-us.toml @@ -0,0 +1,45 @@ +_version = 1 + +[args] +setInput = "input to set in sshd_config" + +[error] +command = "Command" +invalidInput = "Invalid Input" +json = "JSON" +language = "Language" +notImplemented = "Not Implemented" +parser = "Parser" +parseInt = "Parse Integer" +registry = "Registry" + +[get] +defaultShellCmdOptionMustBeString = "cmdOption must be a string" +defaultShellEscapeArgsMustBe0Or1 = "'%{input}' must be a 0 or 1" +defaultShellEscapeArgsMustBeDWord = "escapeArguments must be a DWord" +defaultShellMustBeString = "shell must be a string" +notImplemented = "get not yet implemented for Microsoft.OpenSSH.SSHD/sshd_config" +windowsOnly = "Microsoft.OpenSSH.SSHD/Windows is only applicable to Windows" + +[set] +failedToParseInput = "failed to parse input as DefaultShell with error: '%{error}'" +shellPathDoesNotExist = "shell path does not exist: '%{shell}'" +shellPathMustNotBeRelative = "shell path must not be relative" + +[parser] +failedToParse = "failed to parse: '%{input}'" +failedToParseAsArray = "value is not an array" +failedToParseChildNode = "failed to parse child node: '%{input}'" +failedToParseNode = "failed to parse '%{input}'" +failedToParseRoot = "failed to parse root: '%{input}'" +invalidConfig = "invalid config: '%{input}'" +invalidValue = "operator is an invalid value for node" +keyNotFound = "key '%{key}' not found" +keyNotRepeatable = "key '%{key}' is not repeatable" +missingKeyInChildNode = "missing key in child node: '%{input}'" +missingValueInChildNode = "missing value in child node: '%{input}'" +unknownNode = "unknown node: '%{kind}'" +unknownNodeType = "unknown node type: '%{node}'" + +[util] +sshdElevation = "elevated security context required" diff --git a/sshdconfig/src/args.rs b/sshdconfig/src/args.rs new file mode 100644 index 000000000..b8c595c05 --- /dev/null +++ b/sshdconfig/src/args.rs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use clap::{Parser, Subcommand, ValueEnum}; +use rust_i18n::t; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive(Parser)] +pub struct Args { + #[clap(subcommand)] + pub command: Command, +} + +#[derive(Subcommand)] +pub enum Command { + /// Get default shell, eventually to be used for `sshd_config` and repeatable keywords + Get { + #[clap(short = 's', long, hide = true)] + setting: Setting, + }, + /// Set default shell, eventually to be used for `sshd_config` and repeatable keywords + Set { + #[clap(short = 'i', long, help = t!("args.setInput").to_string())] + input: String + }, + /// Export `sshd_config` + Export, + Schema { + // Used to inform which schema to generate + #[clap(short = 's', long, hide = true)] + setting: Setting, + }, +} + +#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] +pub struct DefaultShell { + #[serde(skip_serializing_if = "Option::is_none")] + pub shell: Option<String>, + #[serde(rename = "cmdOption", skip_serializing_if = "Option::is_none")] + pub cmd_option: Option<String>, + #[serde(rename = "escapeArguments", skip_serializing_if = "Option::is_none")] + pub escape_arguments: Option<bool>, +} + +#[derive(Clone, Debug, Eq, PartialEq, ValueEnum)] +pub enum Setting { + SshdConfig, + WindowsGlobal +} \ No newline at end of file diff --git a/sshdconfig/src/error.rs b/sshdconfig/src/error.rs index bfd931d16..53206aced 100644 --- a/sshdconfig/src/error.rs +++ b/sshdconfig/src/error.rs @@ -1,18 +1,26 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +use rust_i18n::t; use thiserror::Error; #[derive(Debug, Error)] pub enum SshdConfigError { - #[error("Command: {0}")] + #[error("{t}: {0}", t = t!("error.command"))] CommandError(String), - #[error("JSON: {0}")] + #[error("{t}: {0}", t = t!("error.invalidInput"))] + InvalidInput(String), + #[error("{t}: {0}", t = t!("error.json"))] Json(#[from] serde_json::Error), - #[error("Language: {0}")] + #[error("{t}: {0}", t = t!("error.language"))] LanguageError(#[from] tree_sitter::LanguageError), - #[error("Parser: {0}")] + #[error("{t}: {0}", t = t!("error.notImplemented"))] + NotImplemented(String), + #[error("{t}: {0}", t = t!("error.parser"))] ParserError(String), - #[error("Parser Int: {0}")] + #[error("{t}: {0}", t = t!("error.parseInt"))] ParseIntError(#[from] std::num::ParseIntError), + #[cfg(windows)] + #[error("{t}: {0}", t = t!("error.registry"))] + RegistryError(#[from] registry_lib::error::RegistryError), } diff --git a/sshdconfig/src/export.rs b/sshdconfig/src/export.rs index f9e129ff6..bc720c3a7 100644 --- a/sshdconfig/src/export.rs +++ b/sshdconfig/src/export.rs @@ -10,9 +10,10 @@ use crate::util::invoke_sshd_config_validation; /// # Errors /// /// This function will return an error if the command cannot invoke sshd -T, parse the return, or convert it to json. -pub fn invoke_export() -> Result<String, SshdConfigError> { +pub fn invoke_export() -> Result<(), SshdConfigError> { let sshd_config_text = invoke_sshd_config_validation()?; let sshd_config: serde_json::Map<String, serde_json::Value> = parse_text_to_map(&sshd_config_text)?; - let json = serde_json::to_string_pretty(&sshd_config)?; - Ok(json) + let json = serde_json::to_string(&sshd_config)?; + println!("{json}"); + Ok(()) } diff --git a/sshdconfig/src/get.rs b/sshdconfig/src/get.rs new file mode 100644 index 000000000..837e8d011 --- /dev/null +++ b/sshdconfig/src/get.rs @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#[cfg(windows)] +use { + registry_lib::{config::{Registry, RegistryValueData}, RegistryHelper}, + crate::args::DefaultShell, + crate::metadata::windows::{DEFAULT_SHELL, DEFAULT_SHELL_CMD_OPTION, DEFAULT_SHELL_ESCAPE_ARGS, REGISTRY_PATH}, +}; + +use crate::args::Setting; +use crate::error::SshdConfigError; +use rust_i18n::t; + +/// Invoke the get command. +/// +/// # Errors +/// +/// This function will return an error if the desired settings cannot be retrieved. +pub fn invoke_get(setting: &Setting) -> Result<(), SshdConfigError> { + match *setting { + Setting::SshdConfig => Err(SshdConfigError::NotImplemented(t!("get.notImplemented").to_string())), + Setting::WindowsGlobal => get_default_shell() + } +} + +#[cfg(windows)] +fn get_default_shell() -> Result<(), SshdConfigError> { + let registry_helper = RegistryHelper::new(REGISTRY_PATH, Some(DEFAULT_SHELL.to_string()), None)?; + let default_shell: Registry = registry_helper.get()?; + let mut shell = None; + // default_shell is a single string consisting of the shell exe path + if let Some(value) = default_shell.value_data { + match value { + RegistryValueData::String(s) => { + shell = Some(s); + } + _ => return Err(SshdConfigError::InvalidInput(t!("get.defaultShellMustBeString").to_string())), + } + } + + let registry_helper = RegistryHelper::new(REGISTRY_PATH, Some(DEFAULT_SHELL_CMD_OPTION.to_string()), None)?; + let option: Registry = registry_helper.get()?; + let mut cmd_option = None; + if let Some(value) = option.value_data { + match value { + RegistryValueData::String(s) => cmd_option = Some(s), + _ => return Err(SshdConfigError::InvalidInput(t!("get.defaultShellCmdOptionMustBeString").to_string())), + } + } + + let registry_helper = RegistryHelper::new(REGISTRY_PATH, Some(DEFAULT_SHELL_ESCAPE_ARGS.to_string()), None)?; + let escape_args: Registry = registry_helper.get()?; + let mut escape_arguments = None; + if let Some(value) = escape_args.value_data { + if let RegistryValueData::DWord(b) = value { + if b == 0 || b == 1 { + escape_arguments = if b == 1 { Some(true) } else { Some(false) }; + } else { + return Err(SshdConfigError::InvalidInput(t!("get.defaultShellEscapeArgsMustBe0Or1", input = b).to_string())); + } + } else { + return Err(SshdConfigError::InvalidInput(t!("get.defaultShellEscapeArgsMustBeDWord").to_string())); + } + } + + let result = DefaultShell { + shell, + cmd_option, + escape_arguments + }; + + let output = serde_json::to_string(&result)?; + println!("{output}"); + Ok(()) +} + +#[cfg(not(windows))] +fn get_default_shell() -> Result<(), SshdConfigError> { + Err(SshdConfigError::InvalidInput(t!("get.windowsOnly").to_string())) +} diff --git a/sshdconfig/src/main.rs b/sshdconfig/src/main.rs index acb5d2c5d..ef2ce3bc3 100644 --- a/sshdconfig/src/main.rs +++ b/sshdconfig/src/main.rs @@ -1,43 +1,50 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +use clap::{Parser}; +use rust_i18n::i18n; use schemars::schema_for; -use serde_json::to_string_pretty; -use std::{env::args, process::exit}; -use crate::export::invoke_export; -use crate::parser::SshdConfigParser; +use args::{Args, Command, DefaultShell, Setting}; +use export::invoke_export; +use get::invoke_get; +use parser::SshdConfigParser; +use set::invoke_set; +mod args; mod error; mod export; +mod get; mod metadata; mod parser; +mod set; mod util; -fn main() { - - // TODO: add support for other commands and use clap for argument parsing - let args: Vec<String> = args().collect(); +i18n!("locales", fallback = "en-us"); - if args.len() != 2 || (args[1] != "export" && args[1] != "schema") { - eprintln!("Usage: {} <export|schema>", args[0]); - exit(1); - } - - if args[1] == "schema" { - // for dsc tests on linux/mac - let schema = schema_for!(SshdConfigParser); - println!("{}", to_string_pretty(&schema).unwrap()); - return; - } - - // only supports export for sshdconfig for now - match invoke_export() { - Ok(result) => { - println!("{result}"); - } - Err(e) => { - eprintln!("Error exporting SSHD config: {e:?}"); +fn main() { + let args = Args::parse(); + + let result = match &args.command { + Command::Export => invoke_export(), + Command::Get { setting } => invoke_get(setting), + Command::Set { input } => invoke_set(input), + Command::Schema { setting } => { + let schema = match setting { + Setting::SshdConfig => { + schema_for!(SshdConfigParser) + }, + Setting::WindowsGlobal => { + schema_for!(DefaultShell) + } + }; + println!("{}", serde_json::to_string(&schema).unwrap()); + Ok(()) } + }; + + if let Err(e) = result { + eprintln!("{e}"); + std::process::exit(1); } } diff --git a/sshdconfig/src/metadata.rs b/sshdconfig/src/metadata.rs index d1f56311d..e3d7a37f6 100644 --- a/sshdconfig/src/metadata.rs +++ b/sshdconfig/src/metadata.rs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +use clap::ValueEnum; + // TODO: ensure lists are complete // keywords that can be repeated over multiple lines and should be represented as arrays @@ -13,6 +15,16 @@ pub const REPEATABLE_KEYWORDS: [&str; 6] = [ "subsystem" ]; +#[derive(Clone, Debug, Eq, PartialEq, ValueEnum)] +pub enum RepeatableKeyword { + HostKey, + Include, + ListenAddress, + Port, + SetEnv, + Subsystem, +} + // keywords that can have multiple argments per line and should be represented as arrays // but cannot be repeated over multiple lines, as subsequent entries are ignored pub const MULTI_ARG_KEYWORDS: [&str; 7] = [ @@ -24,3 +36,11 @@ pub const MULTI_ARG_KEYWORDS: [&str; 7] = [ "macs", "pubkeyacceptedalgorithms" ]; + +#[cfg(windows)] +pub mod windows { + pub const REGISTRY_PATH: &str = "HKLM\\SOFTWARE\\OpenSSH"; + pub const DEFAULT_SHELL: &str = "DefaultShell"; + pub const DEFAULT_SHELL_CMD_OPTION: &str = "DefaultShellCommandOption"; + pub const DEFAULT_SHELL_ESCAPE_ARGS: &str = "DefaultShellEscapeArguments"; +} diff --git a/sshdconfig/src/parser.rs b/sshdconfig/src/parser.rs index f6e3cad0f..42aead483 100644 --- a/sshdconfig/src/parser.rs +++ b/sshdconfig/src/parser.rs @@ -7,6 +7,7 @@ use tree_sitter::Parser; use crate::error::SshdConfigError; use crate::metadata::{MULTI_ARG_KEYWORDS, REPEATABLE_KEYWORDS}; +use rust_i18n::t; #[derive(Debug, JsonSchema)] pub struct SshdConfigParser { @@ -35,14 +36,14 @@ impl SshdConfigParser { parser.set_language(&tree_sitter_ssh_server_config::LANGUAGE.into())?; let Some(tree) = &mut parser.parse(input, None) else { - return Err(SshdConfigError::ParserError(format!("failed to parse: {input}"))); + return Err(SshdConfigError::ParserError(t!("parser.failedToParse", input = input).to_string())); }; let root_node = tree.root_node(); if root_node.is_error() { - return Err(SshdConfigError::ParserError(format!("failed to parse root: {input}"))); + return Err(SshdConfigError::ParserError(t!("parser.failedToParseRoot", input = input).to_string())); } if root_node.kind() != "server_config" { - return Err(SshdConfigError::ParserError(format!("invalid config: {input}"))); + return Err(SshdConfigError::ParserError(t!("parser.invalidConfig", input = input).to_string())); } let input_bytes = input.as_bytes(); let mut cursor = root_node.walk(); @@ -54,12 +55,12 @@ impl SshdConfigParser { fn parse_child_node(&mut self, node: tree_sitter::Node, input: &str, input_bytes: &[u8]) -> Result<(), SshdConfigError> { if node.is_error() { - return Err(SshdConfigError::ParserError(format!("failed to parse: {input}"))); + return Err(SshdConfigError::ParserError(t!("parser.failedToParse", input = input).to_string())); } match node.kind() { "keyword" => self.parse_keyword_node(node, input, input_bytes), "comment" | "empty_line" => Ok(()), - _ => Err(SshdConfigError::ParserError(format!("unknown node type: {}", node.kind()))), + _ => Err(SshdConfigError::ParserError(t!("parser.unknownNodeType", node = node.kind()).to_string())), } } @@ -72,9 +73,9 @@ impl SshdConfigParser { if let Some(keyword) = keyword_node.child_by_field_name("keyword") { let Ok(text) = keyword.utf8_text(input_bytes) else { - return Err(SshdConfigError::ParserError(format!( - "failed to parse keyword node: {input}" - ))); + return Err(SshdConfigError::ParserError( + t!("parser.failedToParseChildNode", input = input).to_string() + )); }; if REPEATABLE_KEYWORDS.contains(&text) { is_repeatable = true; @@ -87,7 +88,7 @@ impl SshdConfigParser { for node in keyword_node.named_children(&mut cursor) { if node.is_error() { - return Err(SshdConfigError::ParserError(format!("failed to parse child node: {input}"))); + return Err(SshdConfigError::ParserError(t!("parser.failedToParseChildNode", input = input).to_string())); } if node.kind() == "arguments" { value = parse_arguments_node(node, input, input_bytes, is_vec)?; @@ -95,11 +96,11 @@ impl SshdConfigParser { } if let Some(key) = key { if value.is_null() { - return Err(SshdConfigError::ParserError(format!("missing value in child node: {input}"))); + return Err(SshdConfigError::ParserError(t!("parser.missingValueInChildNode", input = input).to_string())); } return self.update_map(&key, value, is_repeatable); } - Err(SshdConfigError::ParserError(format!("missing key in child node: {input}"))) + Err(SshdConfigError::ParserError(t!("parser.missingKeyInChildNode", input = input).to_string())) } fn update_map(&mut self, key: &str, value: Value, is_repeatable: bool) -> Result<(), SshdConfigError> { @@ -114,19 +115,19 @@ impl SshdConfigParser { } } else { return Err(SshdConfigError::ParserError( - "value is not an array".to_string(), + t!("parser.failedToParseAsArray").to_string() )); } } else { return Err(SshdConfigError::ParserError( - "value is not an array".to_string(), + t!("parser.failedToParseAsArray").to_string() )); } } else { - return Err(SshdConfigError::ParserError(format!("key {key} not found"))); + return Err(SshdConfigError::ParserError(t!("parser.keyNotFound", key = key).to_string())); } } else { - return Err(SshdConfigError::ParserError(format!("key {key} is not repeatable"))); + return Err(SshdConfigError::ParserError(t!("parser.keyNotRepeatable", key = key).to_string())); } } else { self.map.insert(key.to_string(), value); @@ -142,32 +143,32 @@ fn parse_arguments_node(arg_node: tree_sitter::Node, input: &str, input_bytes: & for node in arg_node.named_children(&mut cursor) { if node.is_error() { - return Err(SshdConfigError::ParserError(format!("failed to parse child node: {input}"))); + return Err(SshdConfigError::ParserError(t!("parser.failedToParseChildNode", input = input).to_string())); } let argument: Value = match node.kind() { "boolean" | "string" => { let Ok(arg) = node.utf8_text(input_bytes) else { - return Err(SshdConfigError::ParserError(format!( - "failed to parse string node: {input}" - ))); + return Err(SshdConfigError::ParserError( + t!("parser.failedToParseNode", input = input).to_string() + )); }; Value::String(arg.to_string()) } "number" => { let Ok(arg) = node.utf8_text(input_bytes) else { - return Err(SshdConfigError::ParserError(format!( - "failed to parse string node: {input}" - ))); + return Err(SshdConfigError::ParserError( + t!("parser.failedToParseNode", input = input).to_string() + )); }; Value::Number(arg.parse::<u64>()?.into()) } "operator" => { // TODO: handle operator if not parsing from SSHD -T - return Err(SshdConfigError::ParserError(format!( - "todo - unsuported node: {}", node.kind() - ))); + return Err(SshdConfigError::ParserError( + t!("parser.invalidValue").to_string() + )); } - _ => return Err(SshdConfigError::ParserError(format!("unknown node: {}", node.kind()))) + _ => return Err(SshdConfigError::ParserError(t!("parser.unknownNode", kind = node.kind()).to_string())) }; if is_vec { vec.push(argument); diff --git a/sshdconfig/src/set.rs b/sshdconfig/src/set.rs new file mode 100644 index 000000000..349e36cf3 --- /dev/null +++ b/sshdconfig/src/set.rs @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#[cfg(windows)] +use { + std::path::Path, + registry_lib::{config::RegistryValueData, RegistryHelper}, + crate::metadata::windows::{DEFAULT_SHELL, DEFAULT_SHELL_CMD_OPTION, DEFAULT_SHELL_ESCAPE_ARGS, REGISTRY_PATH}, +}; + +use crate::args::DefaultShell; +use crate::error::SshdConfigError; +use rust_i18n::t; + +/// Invoke the set command. +/// +/// # Errors +/// +/// This function will return an error if the desired settings cannot be applied. +pub fn invoke_set(input: &str) -> Result<(), SshdConfigError> { + match serde_json::from_str::<DefaultShell>(input) { + Ok(default_shell) => { + set_default_shell(default_shell.shell, default_shell.cmd_option, default_shell.escape_arguments) + }, + Err(e) => { + Err(SshdConfigError::InvalidInput(t!("set.failedToParseInput", error = e).to_string())) + } + } +} + +#[cfg(windows)] +fn set_default_shell(shell: Option<String>, cmd_option: Option<String>, escape_arguments: Option<bool>) -> Result<(), SshdConfigError> { + if let Some(shell) = shell { + // TODO: if shell contains quotes, we need to remove them + let shell_path = Path::new(&shell); + if shell_path.is_relative() && shell_path.components().any(|c| c == std::path::Component::ParentDir) { + return Err(SshdConfigError::InvalidInput(t!("set.shellPathMustNotBeRelative").to_string())); + } + if !shell_path.exists() { + return Err(SshdConfigError::InvalidInput(t!("set.shellPathDoesNotExist", shell = shell).to_string())); + } + + set_registry(DEFAULT_SHELL, RegistryValueData::String(shell))?; + } else { + remove_registry(DEFAULT_SHELL)?; + } + + + if let Some(cmd_option) = cmd_option { + set_registry(DEFAULT_SHELL_CMD_OPTION, RegistryValueData::String(cmd_option.clone()))?; + } else { + remove_registry(DEFAULT_SHELL_CMD_OPTION)?; + } + + if let Some(escape_args) = escape_arguments { + let mut escape_data = 0; + if escape_args { + escape_data = 1; + } + set_registry(DEFAULT_SHELL_ESCAPE_ARGS, RegistryValueData::DWord(escape_data))?; + } else { + remove_registry(DEFAULT_SHELL_ESCAPE_ARGS)?; + } + + Ok(()) +} + +#[cfg(not(windows))] +fn set_default_shell(_shell: Option<String>, _cmd_option: Option<String>, _escape_arguments: Option<bool>) -> Result<(), SshdConfigError> { + Err(SshdConfigError::InvalidInput(t!("get.windowsOnly").to_string())) +} + +#[cfg(windows)] +fn set_registry(name: &str, data: RegistryValueData) -> Result<(), SshdConfigError> { + let registry_helper = RegistryHelper::new(REGISTRY_PATH, Some(name.to_string()), Some(data))?; + registry_helper.set()?; + Ok(()) +} + +#[cfg(windows)] +fn remove_registry(name: &str) -> Result<(), SshdConfigError> { + let registry_helper = RegistryHelper::new(REGISTRY_PATH, Some(name.to_string()), None)?; + registry_helper.remove()?; + Ok(()) +} diff --git a/sshdconfig/src/util.rs b/sshdconfig/src/util.rs index 1e1235784..cc3ca84b5 100644 --- a/sshdconfig/src/util.rs +++ b/sshdconfig/src/util.rs @@ -4,6 +4,7 @@ use std::process::Command; use crate::error::SshdConfigError; +use rust_i18n::t; /// Invoke sshd -T. /// @@ -29,9 +30,9 @@ pub fn invoke_sshd_config_validation() -> Result<String, SshdConfigError> { } else { let stderr = String::from_utf8(output.stderr) .map_err(|e| SshdConfigError::CommandError(e.to_string()))?; - if stderr.contains("sshd: no hostkeys available") { + if stderr.contains("sshd: no hostkeys available") || stderr.contains("Permission denied") { return Err(SshdConfigError::CommandError( - "sshd: no hostkeys available, please run as admin".to_string(), + t!("util.sshdElevation").to_string() )); } Err(SshdConfigError::CommandError(stderr)) diff --git a/sshdconfig/sshd-windows.dsc.resource.json b/sshdconfig/sshd-windows.dsc.resource.json new file mode 100644 index 000000000..574dc28c6 --- /dev/null +++ b/sshdconfig/sshd-windows.dsc.resource.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json", + "type": "Microsoft.OpenSSH.SSHD/Windows", + "description": "Manage SSH Server Configuration Global Settings", + "tags": [ + "Windows" + ], + "version": "0.1.0", + "get": { + "executable": "sshdconfig", + "args": [ + "get", + "-s", + "windows-global" + ] + }, + "set": { + "executable": "sshdconfig", + "args": [ + "set", + { + "jsonInputArg": "--input", + "mandatory": true + } + ] + }, + "schema": { + "command": { + "executable": "sshdconfig", + "args": [ + "schema", + "-s", + "windows-global" + ] + } + } +} diff --git a/sshdconfig/sshdconfig.dsc.resource.json b/sshdconfig/sshd_config.dsc.resource.json similarity index 85% rename from sshdconfig/sshdconfig.dsc.resource.json rename to sshdconfig/sshd_config.dsc.resource.json index fa97f9be4..c18dd7d9a 100644 --- a/sshdconfig/sshdconfig.dsc.resource.json +++ b/sshdconfig/sshd_config.dsc.resource.json @@ -13,7 +13,9 @@ "command": { "executable": "sshdconfig", "args": [ - "schema" + "schema", + "-s", + "sshd-config" ] } } diff --git a/sshdconfig/tests/defaultshell.tests.ps1 b/sshdconfig/tests/defaultshell.tests.ps1 new file mode 100644 index 000000000..baa97524b --- /dev/null +++ b/sshdconfig/tests/defaultshell.tests.ps1 @@ -0,0 +1,209 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +Describe 'Default Shell Configuration Tests' -Skip:(!$IsWindows) { + BeforeAll { + # Store original registry values to restore later + $OriginalValues = @{} + $RegistryPath = "HKLM:\SOFTWARE\OpenSSH" + $ValueNames = @("DefaultShell", "DefaultShellCommandOption", "DefaultShellEscapeArguments") + $CreatedOpenSSHKey = $false + + # Create OpenSSH registry key if it doesn't exist + if (-not (Test-Path $RegistryPath)) { + $CreatedOpenSSHKey = $true + New-Item -Path $RegistryPath -Force | Out-Null + } + else { + # Store existing values + foreach ($valueName in $ValueNames) { + try { + $value = Get-ItemProperty -Path $RegistryPath -Name $valueName -ErrorAction SilentlyContinue + if ($value) { + $OriginalValues[$valueName] = $value.$valueName + Remove-ItemProperty -Path $RegistryPath -Name $valueName -ErrorAction SilentlyContinue + } + } + catch { + # Value doesn't currently exist, nothing to store + } + } + } + } + + AfterAll { + # Restore original registry values + if ($CreatedOpenSSHKey) { + # Remove the OpenSSH key if it was created for the tests + Remove-Item -Path $RegistryPath -Force -ErrorAction SilentlyContinue + } else { + foreach ($valueName in $ValueNames) { + if ($OriginalValues.ContainsKey($valueName)) { + New-ItemProperty -Path $RegistryPath -Name $valueName -Value $OriginalValues[$valueName] + } + } + } + } + + AfterEach { + # Clean up any properties set during the tests + foreach ($valueName in $ValueNames) { + try { + Remove-ItemProperty -Path $RegistryPath -Name $valueName -ErrorAction SilentlyContinue + } + catch { + # Ignore if value doesn't exist + } + } + } + + Context 'Get Default Shell' { + It 'Should get default shell without args when registry value exists' { + $testShell = "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" + New-ItemProperty -Path $RegistryPath -Name "DefaultShell" -Value $testShell + + $output = sshdconfig get -s windows-global + $LASTEXITCODE | Should -Be 0 + + $result = $output | ConvertFrom-Json + $result.shell | Should -Be $testShell + $result.cmdOption | Should -BeNullOrEmpty + $result.escapeArguments | Should -BeNullOrEmpty + } + + It 'Should get default shell with args when registry value exists' { + $testShell = "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" + $testShellWithArgs = "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" + New-ItemProperty -Path $RegistryPath -Name "DefaultShell" -Value $testShellWithArgs + New-ItemProperty -Path $RegistryPath -Name "DefaultShellCommandOption" -Value "/c" + New-ItemProperty -Path $RegistryPath -Name "DefaultShellEscapeArguments" -Value 0 -Type DWord + + $output = sshdconfig get -s windows-global + $LASTEXITCODE | Should -Be 0 + + $result = $output | ConvertFrom-Json + $result.shell | Should -Be $testShell + $result.cmdOption | Should -Be "/c" + $result.escapeArguments | Should -Be $false + } + + It 'Should handle empty default shell registry values' -Skip:(!$IsWindows) { + $output = sshdconfig get -s windows-global + $LASTEXITCODE | Should -Be 0 + + $result = $output | ConvertFrom-Json + $result.shell | Should -BeNullOrEmpty + $result.cmdOption | Should -BeNullOrEmpty + $result.escapeArguments | Should -BeNullOrEmpty + } + } + + Context 'Set Default Shell' { + It 'Should set default shell with valid configuration' { + $testShell = "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" + + $inputConfig = @{ + shell = $testShell + cmdOption = "/c" + escapeArguments = $false + } | ConvertTo-Json + + sshdconfig set --input $inputConfig + $LASTEXITCODE | Should -Be 0 + + $defaultShell = Get-ItemProperty -Path $RegistryPath -Name "DefaultShell" -ErrorAction SilentlyContinue + $defaultShell.DefaultShell | Should -Be $testShell + + $cmdOption = Get-ItemProperty -Path $RegistryPath -Name "DefaultShellCommandOption" -ErrorAction SilentlyContinue + $cmdOption.DefaultShellCommandOption | Should -Be "/c" + + $escapeArgs = Get-ItemProperty -Path $RegistryPath -Name "DefaultShellEscapeArguments" -ErrorAction SilentlyContinue + $escapeArgs.DefaultShellEscapeArguments | Should -Be 0 + } + + It 'Should set default shell with minimal configuration' { + $inputConfig = @{ + shell = "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" + } | ConvertTo-Json + + sshdconfig set --input $inputConfig + $LASTEXITCODE | Should -Be 0 + + $defaultShell = Get-ItemProperty -Path $RegistryPath -Name "DefaultShell" -ErrorAction SilentlyContinue + $defaultShell.DefaultShell | Should -Be "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" + } + + It 'Should handle invalid JSON input gracefully' { + $invalidJson = "{ invalid json }" + + sshdconfig set --input $invalidJson + $LASTEXITCODE | Should -Not -Be 0 + } + + It 'Should clear default shell when set to null' { + Set-ItemProperty -Path $RegistryPath -Name "DefaultShell" -Value "C:\Windows\System32\cmd.exe" + + $inputConfig = @{ shell = $null } | ConvertTo-Json + + sshdconfig set --input $inputConfig + $LASTEXITCODE | Should -Be 0 + + $result = Get-ItemProperty -Path $RegistryPath -Name "DefaultShell" -ErrorAction SilentlyContinue + $result | Should -BeNullOrEmpty + } + } + + Context 'Set then get default shell' { + It 'Should maintain configuration consistency between set and get' { + $originalConfig = @{ + shell = "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" + cmdOption = "/c" + escapeArguments = $true + } + $inputJson = $originalConfig | ConvertTo-Json + + sshdconfig set --input $inputJson + $LASTEXITCODE | Should -Be 0 + + $getOutput = sshdconfig get -s windows-global + $LASTEXITCODE | Should -Be 0 + + $retrievedConfig = $getOutput | ConvertFrom-Json + + $retrievedConfig.shell | Should -Be $originalConfig.shell + $retrievedConfig.cmdOption | Should -Be $originalConfig.cmdOption + $retrievedConfig.escapeArguments | Should -Be $originalConfig.escapeArguments + } + } + + Context 'Set default shell with null value' { + It 'Should clear existing default shell when set to null' { + $testShell = "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" + New-ItemProperty -Path $RegistryPath -Name "DefaultShell" -Value $testShell + + $inputConfig = @{ shell = $null } | ConvertTo-Json + + sshdconfig set --input $inputConfig + $LASTEXITCODE | Should -Be 0 + + $result = Get-ItemProperty -Path $RegistryPath -Name "DefaultShell" -ErrorAction SilentlyContinue + $result | Should -BeNullOrEmpty + } + } +} + +Describe 'Default Shell Configuration Error Handling on Non-Windows Platforms' -Skip:($IsWindows) { + It 'Should return error for set command' { + $inputConfig = @{ shell = $null } | ConvertTo-Json + + $out = sshdconfig set --input $inputConfig 2>&1 + $LASTEXITCODE | Should -Not -Be 0 + $out | Should -BeLike '*is only applicable to Windows*' + } + + It 'Should return error for get command' { + $out = sshdconfig get -s windows-global 2>&1 + $LASTEXITCODE | Should -Not -Be 0 + $out | Should -BeLike '*is only applicable to Windows*' + } +}