Skip to content
10 changes: 7 additions & 3 deletions dsc/tests/dsc_sshdconfig.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,16 @@ BeforeDiscovery {
$identity = [System.Security.Principal.WindowsIdentity]::GetCurrent()
$principal = [System.Security.Principal.WindowsPrincipal]::new($identity)
$isElevated = $principal.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator)
$sshdExists = ($null -ne (Get-Command sshd -CommandType Application -ErrorAction Ignore))
$skipTest = !$isElevated -or !$sshdExists
}
else {
$isElevated = (id -u) -eq 0
}

$sshdExists = ($null -ne (Get-Command sshd -CommandType Application -ErrorAction Ignore))
$skipTest = !$isElevated -or !$sshdExists
}

Describe 'SSHDConfig resource tests' -Skip:(!$IsWindows -or $skipTest) {
Describe 'SSHDConfig resource tests' -Skip:($skipTest) {
BeforeAll {
# set a non-default value in a temporary sshd_config file
"LogLevel Debug3`nPasswordAuthentication no" | Set-Content -Path $TestDrive/test_sshd_config
Expand Down
9 changes: 6 additions & 3 deletions grammars/tree-sitter-ssh-server-config/grammar.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,23 @@ export default grammar({

// check for an empty line that is just a /n character
_empty_line: $ => '\n',
comment: $ => /#.*\n/,
comment: $ => seq(/#.*/, '\n'),
_inline_comment: $ => /#.*/,

keyword: $ => seq(
field('keyword', $.alphanumeric),
choice(seq(/[ \t]/, optional('=')), '='),
optional(field('operator', $.operator)),
field('arguments', $.arguments),
optional(alias($._inline_comment, $.comment)),
"\n"
),

match: $ => seq(
token(prec(PREC.MATCH, /match/i)),
seq(repeat1($.criteria), $._empty_line),
seq(repeat1($.criteria), optional(alias($._inline_comment, $.comment)), $._empty_line),
repeat1(choice($.comment, $.keyword)),
optional($._empty_line)
),

criteria: $ => seq(
Expand All @@ -48,7 +51,7 @@ export default grammar({
boolean: $ => choice('yes', 'no'),
number: $ => /\d+/,
operator: $ => token(prec(PREC.OPERATOR, /[-+\^]/)),
string: $ => /[^\r\n,"'\s]+/, /* cannot contain spaces */
string: $ => /[^\n\r\s,"'#]+/, /* cannot contain spaces */

_quotedString: $ => /[^\r\n,"']+/, /* can contain spaces */
_doublequotedString: $ => seq('"', alias($._quotedString, $.string), repeat(seq(',', alias($._quotedString, $.string))), '"'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -429,3 +429,136 @@ passwordauthentication no
(alphanumeric)
(arguments
(boolean)))))
====
parse comments between keywords
====
port 2222 # use non-default port

---
(server_config
(keyword
(alphanumeric)
(arguments
(number))
(comment)))
====
parse comments in match blocks
====
port 2222
passwordauthentication no

match User testuser # comment about criteria
PasswordAuthentication yes
AllowTcpForwarding no
# comment between match blocks
match Address 192.168.1.0/24
X11Forwarding yes
MaxAuthTries 3

---
(server_config
(keyword
(alphanumeric)
(arguments
(number)))
(keyword
(alphanumeric)
(arguments
(boolean)))
(match
(criteria
(alpha)
(argument
(string)))
(comment)
(keyword
(alphanumeric)
(arguments
(boolean)))
(keyword
(alphanumeric)
(arguments
(boolean)))
(comment))
(match
(criteria
(alpha)
(argument
(string)))
(keyword
(alphanumeric)
(arguments
(boolean)))
(keyword
(alphanumeric)
(arguments
(number)))))
====
parse newlines between match blocks
====
port 2222
passwordauthentication no

match User testuser
PasswordAuthentication yes
AllowTcpForwarding no

match Address 192.168.1.0/24
X11Forwarding yes
MaxAuthTries 3

---
(server_config
(keyword
(alphanumeric)
(arguments
(number)))
(keyword
(alphanumeric)
(arguments
(boolean)))
(match
(criteria
(alpha)
(argument
(string)))
(keyword
(alphanumeric)
(arguments
(boolean)))
(keyword
(alphanumeric)
(arguments
(boolean))))
(match
(criteria
(alpha)
(argument
(string)))
(keyword
(alphanumeric)
(arguments
(boolean)))
(keyword
(alphanumeric)
(arguments
(number)))))
====
parse comment within match block
====
match user developer
# Enable password authentication for developers - comment ignored
passwordauthentication yes

---
(server_config
(match
(criteria
(alpha)
(argument
(string)))
(comment)
(keyword
(alphanumeric)
(arguments
(boolean)))))
1 change: 1 addition & 0 deletions resources/sshdconfig/locales/en-us.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ 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"
includeWarning = "Include directive found in sshd_config. This resource uses 'sshd -T' to process the overall configuration state, which merges all included files but does not return the Include directive itself"
traceInput = "Get input:"
windowsOnly = "Microsoft.OpenSSH.SSHD/Windows is only applicable to Windows"

Expand Down
9 changes: 7 additions & 2 deletions resources/sshdconfig/src/get.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use {

use rust_i18n::t;
use serde_json::{Map, Value};
use tracing::{debug, trace};
use tracing::{debug, trace, warn};

use crate::args::Setting;
use crate::error::SshdConfigError;
Expand Down Expand Up @@ -121,10 +121,15 @@ pub fn get_sshd_settings(cmd_info: &CommandInfo, is_get: bool) -> Result<Map<Str
let mut defaults = extract_sshd_defaults()?;

// remove any explicit keys from default settings list
for key in explicit_settings.keys() {
for (key, value) in &explicit_settings {
if defaults.contains_key(key) {
defaults.remove(key);
}
if key == "include" {
warn!("{}", t!("get.includeWarning").to_string());
} else if key == "match" {
result.insert(key.clone(), value.clone());
}
}

if cmd_info.include_defaults {
Expand Down
1 change: 1 addition & 0 deletions resources/sshdconfig/src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@ pub fn build_command_info(input: Option<&String>, is_get: bool) -> Result<Comman
});
if is_get && !sshd_config.is_empty() {
warn!("{}", t!("util.getIgnoresInputFilters"));
sshd_config.clear();
}
return Ok(CommandInfo {
clobber,
Expand Down
150 changes: 150 additions & 0 deletions resources/sshdconfig/tests/sshdconfig.get.tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

BeforeDiscovery {
if ($IsWindows) {
$identity = [System.Security.Principal.WindowsIdentity]::GetCurrent()
$principal = [System.Security.Principal.WindowsPrincipal]::new($identity)
$isElevated = $principal.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator)
}
else {
$isElevated = (id -u) -eq 0
}

$sshdExists = ($null -ne (Get-Command sshd -CommandType Application -ErrorAction Ignore))
$skipTest = !$isElevated -or !$sshdExists
}


Describe 'sshd_config Get and Export Tests' -Skip:($skipTest) {
BeforeAll {
$TestConfigPath = Join-Path $TestDrive 'test_sshd_config'
"LogLevel Debug3`nPasswordAuthentication no" | Set-Content -Path $TestConfigPath
$configWithMatch = @"
Port 2222
PasswordAuthentication no
Match User testuser
PasswordAuthentication yes
AllowTcpForwarding no
Match Address 192.168.1.0/24
X11Forwarding yes
MaxAuthTries 3
"@
$TestConfigPathWithMatch = Join-Path $TestDrive 'test_sshd_config_match'
$configWithMatch | Set-Content -Path $TestConfigPathWithMatch
$configWithInclude = @"
Port 3333
Include /etc/ssh/sshd_config.d/*.conf
PasswordAuthentication no
"@
$TestConfigPathWithInclude = Join-Path $TestDrive 'test_sshd_config_include'
$configWithInclude | Set-Content -Path $TestConfigPathWithInclude
}

AfterAll {
if (Test-Path $TestConfigPath) {
Remove-Item -Path $TestConfigPath -Force -ErrorAction SilentlyContinue
}
if (Test-Path $TestConfigPathWithMatch) {
Remove-Item -Path $TestConfigPathWithMatch -Force -ErrorAction SilentlyContinue
}
if (Test-Path $TestConfigPathWithInclude) {
Remove-Item -Path $TestConfigPathWithInclude -Force -ErrorAction SilentlyContinue
}
}

It '<Command> command <Description>' -TestCases @(
@{
Command = 'get'
Description = 'ignores input filtering and returns all properties'
}
@{
Command = 'export'
Description = 'uses input filtering and returns only specified properties'
}
) {
param($Command, $Description)

$inputData = @{
_metadata = @{
filepath = $TestConfigPath
}
passwordAuthentication = $false
} | ConvertTo-Json

if ($Command -eq 'get') {
$result = sshdconfig $Command --input $inputData -s sshd-config 2>$null | ConvertFrom-Json
}
else {
$result = sshdconfig $Command --input $inputData 2>$null | ConvertFrom-Json
}

if ($command -eq 'get') {
# Get should return all properties regardless of input
$result.LogLevel | Should -Be "Debug3"
$result.PasswordAuthentication | Should -Be $false
}
else {
# Export should return only specified properties
$result.PasswordAuthentication | Should -Be $false
$result.PSObject.Properties.Name | Should -Not -Contain "loglevel"
}
}

It '<Command> command returns config with match blocks' -TestCases @(
@{ Command = 'get' }
@{ Command = 'export' }
) {
param($Command)

$inputData = @{
_metadata = @{
filepath = $TestConfigPathWithMatch
}
} | ConvertTo-Json

if ($Command -eq 'get') {
$result = sshdconfig $Command --input $inputData -s sshd-config 2>$null | ConvertFrom-Json
}
else {
$result = sshdconfig $Command --input $inputData 2>$null | ConvertFrom-Json
}
$result.Port | Should -Be "2222"
$result.PasswordAuthentication | Should -Be $false
$result.Match | Should -Not -BeNullOrEmpty
$result.Match.Count | Should -Be 2
$result.Match[0].Criteria.User | Should -Be "testuser"
$result.Match[0].PasswordAuthentication | Should -Be $true
$result.Match[0].AllowTcpForwarding | Should -Be $false
$result.Match[1].Criteria.Address | Should -Be "192.168.1.0/24"
$result.Match[1].X11Forwarding | Should -Be $true
$result.Match[1].MaxAuthTries | Should -Be "3"
}

It '<Command> command displays warning when Include directive is present' -TestCases @(
@{ Command = 'get' }
@{ Command = 'export' }
) {
param($Command)

$inputData = @{
_metadata = @{
filepath = $TestConfigPathWithInclude
}
} | ConvertTo-Json

$stderrFile = Join-Path $TestDrive "stderr_$Command.txt"
if ($Command -eq 'get') {
$result = sshdconfig $Command --input $inputData -s sshd-config 2>$stderrFile | ConvertFrom-Json
}
else {
$result = sshdconfig $Command --input $inputData 2>$stderrFile | ConvertFrom-Json
}

$stderr = Get-Content -Path $stderrFile -Raw -ErrorAction SilentlyContinue
$stderr | Should -BeLike "*WARN*Include directive found in sshd_config*"
Remove-Item -Path $stderrFile -Force -ErrorAction SilentlyContinue
}
}
Loading