From 9563096a7268a293e7a0a17439bb4d5b128941e9 Mon Sep 17 00:00:00 2001 From: Dominik Richter Date: Sun, 4 Feb 2024 20:20:32 -0800 Subject: [PATCH 1/3] =?UTF-8?q?=E2=AD=90=20introduce=20sshd.config.blocks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This introduces support for querying individual blocks in SSHd configs. It's a more direct way of adressing feedback in https://github.com/mondoohq/cnspec-policies/issues/340 by exposing the underlying block entirely, while also supporting aggregate values in the existing params structure. Example: Let's assume we have an existing `/etc/ssh/sshd_config` on our system with a bunch of existing configuration. If we added a new match group at the end of the file like this: ```ini Match Group sftp-users X11Forwarding no PermitRootLogin no AllowTCPForwarding yes ``` We can now query this match block both explicitly and implicitly. Implicitly it's (already) represented in the existing `params` field: ```coffee > sshd.config.params.AllowTcpForwarding "no,yes" ``` In the above example you can see, that we already had this field set above the match block with the value set to `no`. After adding our match group, it was additionally set to `yes`. The field aggregates both values. This implicit access to config values has already existed in MQL as the default behavior. With the new `blocks` field, we are extending implicit match block access to become explicit: ```coffee > sshd.config.blocks sshd.config.blocks: [ 0: sshd.config.matchBlock criteria="" 1: sshd.config.matchBlock criteria="Group sftp-users" ] ``` This first match block is the default block, which is always present. It has no criteria set and applies to everything. The second match block has a `criteria` field that shows it only matches for `Group sftp-users`. You can easily access its configuration: ```coffee > sshd.config.blocks { criteria params } sshd.config.blocks: [ 0: { criteria: "" params: { AllowTcpForwarding: "no" ... } } 1: { criteria: "Group sftp-users" params: { AllowTcpForwarding: "yes" PermitRootLogin: "no" X11Forwarding: "no" } } ] ``` In this example you can see that each block contains its own set of parameters. These are now restricted to the configuration of the block only. Thus the `AllowTcpForwarding` setting is not an aggregate of values anymore, it now only contains the value defined in the block. Added Note: As a consequence of this change we are now also consistently structuring the `Match` field in the `sshd.config.params` structure to behave like all other fields: It combines any match group separated by commas: ```coffee > sshd.config.params.Match sshd.config.params[Match]: "Group sftp-users,User myservice" ``` Signed-off-by: Dominik Richter --- providers-sdk/v1/testutils/testdata/arch.json | 4 +- providers/os/resources/os.lr | 9 ++ providers/os/resources/os.lr.go | 102 +++++++++++++++++- providers/os/resources/os.lr.manifest.yaml | 9 ++ providers/os/resources/sshd.go | 54 ++++++++-- providers/os/resources/sshd/params.go | 93 ++++++++++++++-- providers/os/resources/sshd/params_test.go | 4 +- providers/os/resources/sshd_test.go | 22 +++- 8 files changed, 271 insertions(+), 26 deletions(-) diff --git a/providers-sdk/v1/testutils/testdata/arch.json b/providers-sdk/v1/testutils/testdata/arch.json index 5bf5507d91..67603051ae 100644 --- a/providers-sdk/v1/testutils/testdata/arch.json +++ b/providers-sdk/v1/testutils/testdata/arch.json @@ -488,7 +488,7 @@ "Fields": { "content": { "type": "\u0007", - "value": "# #\n# Ansible managed\n#\n\n# This is the ssh client system-wide configuration file.\n# See sshd_config(5) for more information on any settings used. Comments will be added only to clarify why a configuration was chosen.\n\n\n# Basic configuration\n# ===================\n\n# Either disable or only allow root login via certificates.\nPermitRootLogin no,no\n\n# Define which port sshd should listen to. Default to `22`.\nPort 22\n\n# Address family should always be limited to the active network configuration.\nAddressFamily inet\n\n# Define which addresses sshd should listen to. Default to `0.0.0.0`, ie make sure you put your desired address in here, since otherwise sshd will listen to everyone.\nListenAddress 0.0.0.0\n\n# List HostKeys here.\nHostKey /etc/ssh/ssh_host_rsa_key\nHostKey /etc/ssh/ssh_host_ecdsa_key\nHostKey /etc/ssh/ssh_host_ed25519_key\n\n# Specifies the host key algorithms that the server offers.\n#\n# HostKeyAlgorithms\n#\n\n# Security configuration\n# ======================\n\n# Set the protocol version to 2 for security reasons. Disables legacy support.\nProtocol 2\n\n# Make sure sshd checks file modes and ownership before accepting logins. This prevents accidental misconfiguration.\nStrictModes yes\n\n# Logging, obsoletes QuietMode and FascistLogging\nSyslogFacility AUTH\nLogLevel VERBOSE\n\n# Cryptography\n# ------------\n\n# **Ciphers** -- If your clients don't support CTR (eg older versions), cbc will be added\n# CBC: is true if you want to connect with OpenSSL-base libraries\n# eg ruby Net::SSH::Transport::CipherFactory requires cbc-versions of the given openssh ciphers to work\n# -- see: (http://net-ssh.github.com/net-ssh/classes/Net/SSH/Transport/CipherFactory.html)\n#\n\nCiphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr\n\n# **Hash algorithms** -- Make sure not to use SHA1 for hashing, unless it is really necessary.\n# Weak HMAC is sometimes required if older package versions are used\n# eg Ruby's Net::SSH at around 2.2.* doesn't support sha2 for hmac, so this will have to be set true in this case.\n#\n\nMACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-512,hmac-sha2-256\n\n# Alternative setting, if OpenSSH version is below v5.9\n#MACs hmac-ripemd160\n\n# **Key Exchange Algorithms** -- Make sure not to use SHA1 for kex, unless it is really necessary\n# Weak kex is sometimes required if older package versions are used\n# eg ruby's Net::SSH at around 2.2.* doesn't support sha2 for kex, so this will have to be set true in this case.\n# based on: https://bettercrypto.org/static/applied-crypto-hardening.pdf\n\nKexAlgorithms curve25519-sha256@libssh.org,diffie-hellman-group-exchange-sha256\n\n# Authentication\n# --------------\n\n# Secure Login directives.\n\nLoginGraceTime 30s\nMaxAuthTries 2\nMaxSessions 10\nMaxStartups 10:30:60\n\n# Enable public key authentication\nPubkeyAuthentication yes\n\n# Never use host-based authentication. It can be exploited.\nIgnoreRhosts yes\nIgnoreUserKnownHosts yes\nHostbasedAuthentication no\n\n# Enable PAM to enforce system wide rules\nUsePAM yes\n\n# Set AuthenticationMethods per default to publickey\n# AuthenticationMethods was introduced in OpenSSH 6.2 - https://www.openssh.com/txt/release-6.2\nAuthenticationMethods publickey\n\n# Disable password-based authentication, it can allow for potentially easier brute-force attacks.\nPasswordAuthentication no\nPermitEmptyPasswords no\nChallengeResponseAuthentication no\n\n# Only enable Kerberos authentication if it is configured.\nKerberosAuthentication no\nKerberosOrLocalPasswd no\nKerberosTicketCleanup yes\n#KerberosGetAFSToken no\n\n# Only enable GSSAPI authentication if it is configured.\nGSSAPIAuthentication no\nGSSAPICleanupCredentials yes\n\n# In case you don't use PAM (`UsePAM no`), you can alternatively restrict users and groups here. For key-based authentication this is not necessary, since all keys must be explicitely enabled.\n\n\n\n\n\n\n# Network\n# -------\n\n# Disable TCP keep alive since it is spoofable. Use ClientAlive messages instead, they use the encrypted channel\nTCPKeepAlive no\n\n# Manage `ClientAlive..` signals via interval and maximum count. This will periodically check up to a `..CountMax` number of times within `..Interval` timeframe, and abort the connection once these fail.\nClientAliveInterval 300\nClientAliveCountMax 3\n\n# Disable tunneling\nPermitTunnel no\n\n# Disable forwarding tcp connections.\n# no real advantage without denied shell access\nAllowTcpForwarding no\n\n# Disable agent forwarding, since local agent could be accessed through forwarded connection.\n# no real advantage without denied shell access\nAllowAgentForwarding no\n\n# Do not allow remote port forwardings to bind to non-loopback addresses.\nGatewayPorts no\n\n# Disable X11 forwarding, since local X11 display could be accessed through forwarded connection.\nX11Forwarding no\nX11UseLocalhost yes\n\n# User environment configuration\n# ==============================\n\nPermitUserEnvironment no\n\n\n# Misc. configuration\n# ===================\n\nCompression no\n\nUseDNS no\n\nPrintMotd no\n\nPrintLastLog no\n\nBanner none\n\n\n# Reject keys that are explicitly blacklisted\nRevokedKeys /etc/ssh/revoked_keys\n\n" + "value": "# #\n# Ansible managed\n#\n\n# This is the ssh client system-wide configuration file.\n# See sshd_config(5) for more information on any settings used. Comments will be added only to clarify why a configuration was chosen.\n\n\n# Basic configuration\n# ===================\n\n# Either disable or only allow root login via certificates.\nPermitRootLogin no\n\n# Define which port sshd should listen to. Default to `22`.\nPort 22\n\n# Address family should always be limited to the active network configuration.\nAddressFamily inet\n\n# Define which addresses sshd should listen to. Default to `0.0.0.0`, ie make sure you put your desired address in here, since otherwise sshd will listen to everyone.\nListenAddress 0.0.0.0\n\n# List HostKeys here.\nHostKey /etc/ssh/ssh_host_rsa_key\nHostKey /etc/ssh/ssh_host_ecdsa_key\nHostKey /etc/ssh/ssh_host_ed25519_key\n\n# Specifies the host key algorithms that the server offers.\n#\n# HostKeyAlgorithms\n#\n\n# Security configuration\n# ======================\n\n# Set the protocol version to 2 for security reasons. Disables legacy support.\nProtocol 2\n\n# Make sure sshd checks file modes and ownership before accepting logins. This prevents accidental misconfiguration.\nStrictModes yes\n\n# Logging, obsoletes QuietMode and FascistLogging\nSyslogFacility AUTH\nLogLevel VERBOSE\n\n# Cryptography\n# ------------\n\n# **Ciphers** -- If your clients don't support CTR (eg older versions), cbc will be added\n# CBC: is true if you want to connect with OpenSSL-base libraries\n# eg ruby Net::SSH::Transport::CipherFactory requires cbc-versions of the given openssh ciphers to work\n# -- see: (http://net-ssh.github.com/net-ssh/classes/Net/SSH/Transport/CipherFactory.html)\n#\n\nCiphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr\n\n# **Hash algorithms** -- Make sure not to use SHA1 for hashing, unless it is really necessary.\n# Weak HMAC is sometimes required if older package versions are used\n# eg Ruby's Net::SSH at around 2.2.* doesn't support sha2 for hmac, so this will have to be set true in this case.\n#\n\nMACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-512,hmac-sha2-256\n\n# Alternative setting, if OpenSSH version is below v5.9\n#MACs hmac-ripemd160\n\n# **Key Exchange Algorithms** -- Make sure not to use SHA1 for kex, unless it is really necessary\n# Weak kex is sometimes required if older package versions are used\n# eg ruby's Net::SSH at around 2.2.* doesn't support sha2 for kex, so this will have to be set true in this case.\n# based on: https://bettercrypto.org/static/applied-crypto-hardening.pdf\n\nKexAlgorithms curve25519-sha256@libssh.org,diffie-hellman-group-exchange-sha256\n\n# Authentication\n# --------------\n\n# Secure Login directives.\n\nLoginGraceTime 30s\nMaxAuthTries 2\nMaxSessions 10\nMaxStartups 10:30:60\n\n# Enable public key authentication\nPubkeyAuthentication yes\n\n# Never use host-based authentication. It can be exploited.\nIgnoreRhosts yes\nIgnoreUserKnownHosts yes\nHostbasedAuthentication no\n\n# Enable PAM to enforce system wide rules\nUsePAM yes\n\n# Set AuthenticationMethods per default to publickey\n# AuthenticationMethods was introduced in OpenSSH 6.2 - https://www.openssh.com/txt/release-6.2\nAuthenticationMethods publickey\n\n# Disable password-based authentication, it can allow for potentially easier brute-force attacks.\nPasswordAuthentication no\nPermitEmptyPasswords no\nChallengeResponseAuthentication no\n\n# Only enable Kerberos authentication if it is configured.\nKerberosAuthentication no\nKerberosOrLocalPasswd no\nKerberosTicketCleanup yes\n#KerberosGetAFSToken no\n\n# Only enable GSSAPI authentication if it is configured.\nGSSAPIAuthentication no\nGSSAPICleanupCredentials yes\n\n# In case you don't use PAM (`UsePAM no`), you can alternatively restrict users and groups here. For key-based authentication this is not necessary, since all keys must be explicitely enabled.\n\n\n\n\n\n\n# Network\n# -------\n\n# Disable TCP keep alive since it is spoofable. Use ClientAlive messages instead, they use the encrypted channel\nTCPKeepAlive no\n\n# Manage `ClientAlive..` signals via interval and maximum count. This will periodically check up to a `..CountMax` number of times within `..Interval` timeframe, and abort the connection once these fail.\nClientAliveInterval 300\nClientAliveCountMax 3\n\n# Disable tunneling\nPermitTunnel no\n\n# Disable forwarding tcp connections.\n# no real advantage without denied shell access\nAllowTcpForwarding no\n\n# Disable agent forwarding, since local agent could be accessed through forwarded connection.\n# no real advantage without denied shell access\nAllowAgentForwarding no\n\n# Do not allow remote port forwardings to bind to non-loopback addresses.\nGatewayPorts no\n\n# Disable X11 forwarding, since local X11 display could be accessed through forwarded connection.\nX11Forwarding no\nX11UseLocalhost yes\n\n# User environment configuration\n# ==============================\n\nPermitUserEnvironment no\n\n\n# Misc. configuration\n# ===================\n\nCompression no\n\nUseDNS no\n\nPrintMotd no\n\nPrintLastLog no\n\nBanner none\n\n\n# Reject keys that are explicitly blacklisted\nRevokedKeys /etc/ssh/revoked_keys\n\nMatch Group sftp-users\n X11Forwarding no\n PermitRootLogin no\n AllowTCPForwarding yes\n\nMatch User myservice\n" }, "exists": { "type": "\u0004", @@ -1075,7 +1075,7 @@ "Fields": { "content": { "type": "\u0007", - "value": "# #\n# Ansible managed\n#\n\n# This is the ssh client system-wide configuration file.\n# See sshd_config(5) for more information on any settings used. Comments will be added only to clarify why a configuration was chosen.\n\n\n# Basic configuration\n# ===================\n\n# Either disable or only allow root login via certificates.\nPermitRootLogin no,no\n\n# Define which port sshd should listen to. Default to `22`.\nPort 22\n\n# Address family should always be limited to the active network configuration.\nAddressFamily inet\n\n# Define which addresses sshd should listen to. Default to `0.0.0.0`, ie make sure you put your desired address in here, since otherwise sshd will listen to everyone.\nListenAddress 0.0.0.0\n\n# List HostKeys here.\nHostKey /etc/ssh/ssh_host_rsa_key\nHostKey /etc/ssh/ssh_host_ecdsa_key\nHostKey /etc/ssh/ssh_host_ed25519_key\n\n# Specifies the host key algorithms that the server offers.\n#\n# HostKeyAlgorithms\n#\n\n# Security configuration\n# ======================\n\n# Set the protocol version to 2 for security reasons. Disables legacy support.\nProtocol 2\n\n# Make sure sshd checks file modes and ownership before accepting logins. This prevents accidental misconfiguration.\nStrictModes yes\n\n# Logging, obsoletes QuietMode and FascistLogging\nSyslogFacility AUTH\nLogLevel VERBOSE\n\n# Cryptography\n# ------------\n\n# **Ciphers** -- If your clients don't support CTR (eg older versions), cbc will be added\n# CBC: is true if you want to connect with OpenSSL-base libraries\n# eg ruby Net::SSH::Transport::CipherFactory requires cbc-versions of the given openssh ciphers to work\n# -- see: (http://net-ssh.github.com/net-ssh/classes/Net/SSH/Transport/CipherFactory.html)\n#\n\nCiphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr\n\n# **Hash algorithms** -- Make sure not to use SHA1 for hashing, unless it is really necessary.\n# Weak HMAC is sometimes required if older package versions are used\n# eg Ruby's Net::SSH at around 2.2.* doesn't support sha2 for hmac, so this will have to be set true in this case.\n#\n\nMACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-512,hmac-sha2-256\n\n# Alternative setting, if OpenSSH version is below v5.9\n#MACs hmac-ripemd160\n\n# **Key Exchange Algorithms** -- Make sure not to use SHA1 for kex, unless it is really necessary\n# Weak kex is sometimes required if older package versions are used\n# eg ruby's Net::SSH at around 2.2.* doesn't support sha2 for kex, so this will have to be set true in this case.\n# based on: https://bettercrypto.org/static/applied-crypto-hardening.pdf\n\nKexAlgorithms curve25519-sha256@libssh.org,diffie-hellman-group-exchange-sha256\n\n# Authentication\n# --------------\n\n# Secure Login directives.\n\nLoginGraceTime 30s\nMaxAuthTries 2\nMaxSessions 10\nMaxStartups 10:30:60\n\n# Enable public key authentication\nPubkeyAuthentication yes\n\n# Never use host-based authentication. It can be exploited.\nIgnoreRhosts yes\nIgnoreUserKnownHosts yes\nHostbasedAuthentication no\n\n# Enable PAM to enforce system wide rules\nUsePAM yes\n\n# Set AuthenticationMethods per default to publickey\n# AuthenticationMethods was introduced in OpenSSH 6.2 - https://www.openssh.com/txt/release-6.2\nAuthenticationMethods publickey\n\n# Disable password-based authentication, it can allow for potentially easier brute-force attacks.\nPasswordAuthentication no\nPermitEmptyPasswords no\nChallengeResponseAuthentication no\n\n# Only enable Kerberos authentication if it is configured.\nKerberosAuthentication no\nKerberosOrLocalPasswd no\nKerberosTicketCleanup yes\n#KerberosGetAFSToken no\n\n# Only enable GSSAPI authentication if it is configured.\nGSSAPIAuthentication no\nGSSAPICleanupCredentials yes\n\n# In case you don't use PAM (`UsePAM no`), you can alternatively restrict users and groups here. For key-based authentication this is not necessary, since all keys must be explicitely enabled.\n\n\n\n\n\n\n# Network\n# -------\n\n# Disable TCP keep alive since it is spoofable. Use ClientAlive messages instead, they use the encrypted channel\nTCPKeepAlive no\n\n# Manage `ClientAlive..` signals via interval and maximum count. This will periodically check up to a `..CountMax` number of times within `..Interval` timeframe, and abort the connection once these fail.\nClientAliveInterval 300\nClientAliveCountMax 3\n\n# Disable tunneling\nPermitTunnel no\n\n# Disable forwarding tcp connections.\n# no real advantage without denied shell access\nAllowTcpForwarding no\n\n# Disable agent forwarding, since local agent could be accessed through forwarded connection.\n# no real advantage without denied shell access\nAllowAgentForwarding no\n\n# Do not allow remote port forwardings to bind to non-loopback addresses.\nGatewayPorts no\n\n# Disable X11 forwarding, since local X11 display could be accessed through forwarded connection.\nX11Forwarding no\nX11UseLocalhost yes\n\n# User environment configuration\n# ==============================\n\nPermitUserEnvironment no\n\n\n# Misc. configuration\n# ===================\n\nCompression no\n\nUseDNS no\n\nPrintMotd no\n\nPrintLastLog no\n\nBanner none\n\n\n# Reject keys that are explicitly blacklisted\nRevokedKeys /etc/ssh/revoked_keys\n\n" + "value": "# #\n# Ansible managed\n#\n\n# This is the ssh client system-wide configuration file.\n# See sshd_config(5) for more information on any settings used. Comments will be added only to clarify why a configuration was chosen.\n\n\n# Basic configuration\n# ===================\n\n# Either disable or only allow root login via certificates.\nPermitRootLogin no\n\n# Define which port sshd should listen to. Default to `22`.\nPort 22\n\n# Address family should always be limited to the active network configuration.\nAddressFamily inet\n\n# Define which addresses sshd should listen to. Default to `0.0.0.0`, ie make sure you put your desired address in here, since otherwise sshd will listen to everyone.\nListenAddress 0.0.0.0\n\n# List HostKeys here.\nHostKey /etc/ssh/ssh_host_rsa_key\nHostKey /etc/ssh/ssh_host_ecdsa_key\nHostKey /etc/ssh/ssh_host_ed25519_key\n\n# Specifies the host key algorithms that the server offers.\n#\n# HostKeyAlgorithms\n#\n\n# Security configuration\n# ======================\n\n# Set the protocol version to 2 for security reasons. Disables legacy support.\nProtocol 2\n\n# Make sure sshd checks file modes and ownership before accepting logins. This prevents accidental misconfiguration.\nStrictModes yes\n\n# Logging, obsoletes QuietMode and FascistLogging\nSyslogFacility AUTH\nLogLevel VERBOSE\n\n# Cryptography\n# ------------\n\n# **Ciphers** -- If your clients don't support CTR (eg older versions), cbc will be added\n# CBC: is true if you want to connect with OpenSSL-base libraries\n# eg ruby Net::SSH::Transport::CipherFactory requires cbc-versions of the given openssh ciphers to work\n# -- see: (http://net-ssh.github.com/net-ssh/classes/Net/SSH/Transport/CipherFactory.html)\n#\n\nCiphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr\n\n# **Hash algorithms** -- Make sure not to use SHA1 for hashing, unless it is really necessary.\n# Weak HMAC is sometimes required if older package versions are used\n# eg Ruby's Net::SSH at around 2.2.* doesn't support sha2 for hmac, so this will have to be set true in this case.\n#\n\nMACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-512,hmac-sha2-256\n\n# Alternative setting, if OpenSSH version is below v5.9\n#MACs hmac-ripemd160\n\n# **Key Exchange Algorithms** -- Make sure not to use SHA1 for kex, unless it is really necessary\n# Weak kex is sometimes required if older package versions are used\n# eg ruby's Net::SSH at around 2.2.* doesn't support sha2 for kex, so this will have to be set true in this case.\n# based on: https://bettercrypto.org/static/applied-crypto-hardening.pdf\n\nKexAlgorithms curve25519-sha256@libssh.org,diffie-hellman-group-exchange-sha256\n\n# Authentication\n# --------------\n\n# Secure Login directives.\n\nLoginGraceTime 30s\nMaxAuthTries 2\nMaxSessions 10\nMaxStartups 10:30:60\n\n# Enable public key authentication\nPubkeyAuthentication yes\n\n# Never use host-based authentication. It can be exploited.\nIgnoreRhosts yes\nIgnoreUserKnownHosts yes\nHostbasedAuthentication no\n\n# Enable PAM to enforce system wide rules\nUsePAM yes\n\n# Set AuthenticationMethods per default to publickey\n# AuthenticationMethods was introduced in OpenSSH 6.2 - https://www.openssh.com/txt/release-6.2\nAuthenticationMethods publickey\n\n# Disable password-based authentication, it can allow for potentially easier brute-force attacks.\nPasswordAuthentication no\nPermitEmptyPasswords no\nChallengeResponseAuthentication no\n\n# Only enable Kerberos authentication if it is configured.\nKerberosAuthentication no\nKerberosOrLocalPasswd no\nKerberosTicketCleanup yes\n#KerberosGetAFSToken no\n\n# Only enable GSSAPI authentication if it is configured.\nGSSAPIAuthentication no\nGSSAPICleanupCredentials yes\n\n# In case you don't use PAM (`UsePAM no`), you can alternatively restrict users and groups here. For key-based authentication this is not necessary, since all keys must be explicitely enabled.\n\n\n\n\n\n\n# Network\n# -------\n\n# Disable TCP keep alive since it is spoofable. Use ClientAlive messages instead, they use the encrypted channel\nTCPKeepAlive no\n\n# Manage `ClientAlive..` signals via interval and maximum count. This will periodically check up to a `..CountMax` number of times within `..Interval` timeframe, and abort the connection once these fail.\nClientAliveInterval 300\nClientAliveCountMax 3\n\n# Disable tunneling\nPermitTunnel no\n\n# Disable forwarding tcp connections.\n# no real advantage without denied shell access\nAllowTcpForwarding no\n\n# Disable agent forwarding, since local agent could be accessed through forwarded connection.\n# no real advantage without denied shell access\nAllowAgentForwarding no\n\n# Do not allow remote port forwardings to bind to non-loopback addresses.\nGatewayPorts no\n\n# Disable X11 forwarding, since local X11 display could be accessed through forwarded connection.\nX11Forwarding no\nX11UseLocalhost yes\n\n# User environment configuration\n# ==============================\n\nPermitUserEnvironment no\n\n\n# Misc. configuration\n# ===================\n\nCompression no\n\nUseDNS no\n\nPrintMotd no\n\nPrintLastLog no\n\nBanner none\n\n\n# Reject keys that are explicitly blacklisted\nRevokedKeys /etc/ssh/revoked_keys\n\nMatch Group sftp-users\n X11Forwarding no\n PermitRootLogin no\n AllowTCPForwarding yes\n\nMatch User myservice\n" }, "files": { "type": "\u0019\u001bfile", diff --git a/providers/os/resources/os.lr b/providers/os/resources/os.lr index 59f7d230d7..12ec4ab9c1 100644 --- a/providers/os/resources/os.lr +++ b/providers/os/resources/os.lr @@ -672,6 +672,8 @@ sshd.config { content(files) string // Configuration values of this SSH server params(content) map[string]string + // Blocks with match conditions in this SSH server config + blocks(content) []sshd.config.matchBlock // Ciphers configured for this SSH server ciphers(params) []string // MACs configured for this SSH server @@ -684,6 +686,13 @@ sshd.config { permitRootLogin(params) []string } +private sshd.config.matchBlock @defaults("criteria") { + // The match criteria for this block + criteria string + // Configuration values in this block + params map[string]string +} + // Service on this system service @defaults("name running enabled type") { init(name string) diff --git a/providers/os/resources/os.lr.go b/providers/os/resources/os.lr.go index 6fa704fd7e..7f6172ca28 100644 --- a/providers/os/resources/os.lr.go +++ b/providers/os/resources/os.lr.go @@ -218,6 +218,10 @@ func init() { Init: initSshdConfig, Create: createSshdConfig, }, + "sshd.config.matchBlock": { + // to override args, implement: initSshdConfigMatchBlock(runtime *plugin.Runtime, args map[string]*llx.RawData) (map[string]*llx.RawData, plugin.Resource, error) + Create: createSshdConfigMatchBlock, + }, "service": { Init: initService, Create: createService, @@ -1170,6 +1174,9 @@ var getDataFields = map[string]func(r plugin.Resource) *plugin.DataRes{ "sshd.config.params": func(r plugin.Resource) *plugin.DataRes { return (r.(*mqlSshdConfig).GetParams()).ToDataRes(types.Map(types.String, types.String)) }, + "sshd.config.blocks": func(r plugin.Resource) *plugin.DataRes { + return (r.(*mqlSshdConfig).GetBlocks()).ToDataRes(types.Array(types.Resource("sshd.config.matchBlock"))) + }, "sshd.config.ciphers": func(r plugin.Resource) *plugin.DataRes { return (r.(*mqlSshdConfig).GetCiphers()).ToDataRes(types.Array(types.String)) }, @@ -1185,6 +1192,12 @@ var getDataFields = map[string]func(r plugin.Resource) *plugin.DataRes{ "sshd.config.permitRootLogin": func(r plugin.Resource) *plugin.DataRes { return (r.(*mqlSshdConfig).GetPermitRootLogin()).ToDataRes(types.Array(types.String)) }, + "sshd.config.matchBlock.criteria": func(r plugin.Resource) *plugin.DataRes { + return (r.(*mqlSshdConfigMatchBlock).GetCriteria()).ToDataRes(types.String) + }, + "sshd.config.matchBlock.params": func(r plugin.Resource) *plugin.DataRes { + return (r.(*mqlSshdConfigMatchBlock).GetParams()).ToDataRes(types.Map(types.String, types.String)) + }, "service.name": func(r plugin.Resource) *plugin.DataRes { return (r.(*mqlService).GetName()).ToDataRes(types.String) }, @@ -3122,6 +3135,10 @@ var setDataFields = map[string]func(r plugin.Resource, v *llx.RawData) bool { r.(*mqlSshdConfig).Params, ok = plugin.RawToTValue[map[string]interface{}](v.Value, v.Error) return }, + "sshd.config.blocks": func(r plugin.Resource, v *llx.RawData) (ok bool) { + r.(*mqlSshdConfig).Blocks, ok = plugin.RawToTValue[[]interface{}](v.Value, v.Error) + return + }, "sshd.config.ciphers": func(r plugin.Resource, v *llx.RawData) (ok bool) { r.(*mqlSshdConfig).Ciphers, ok = plugin.RawToTValue[[]interface{}](v.Value, v.Error) return @@ -3142,6 +3159,18 @@ var setDataFields = map[string]func(r plugin.Resource, v *llx.RawData) bool { r.(*mqlSshdConfig).PermitRootLogin, ok = plugin.RawToTValue[[]interface{}](v.Value, v.Error) return }, + "sshd.config.matchBlock.__id": func(r plugin.Resource, v *llx.RawData) (ok bool) { + r.(*mqlSshdConfigMatchBlock).__id, ok = v.Value.(string) + return + }, + "sshd.config.matchBlock.criteria": func(r plugin.Resource, v *llx.RawData) (ok bool) { + r.(*mqlSshdConfigMatchBlock).Criteria, ok = plugin.RawToTValue[string](v.Value, v.Error) + return + }, + "sshd.config.matchBlock.params": func(r plugin.Resource, v *llx.RawData) (ok bool) { + r.(*mqlSshdConfigMatchBlock).Params, ok = plugin.RawToTValue[map[string]interface{}](v.Value, v.Error) + return + }, "service.__id": func(r plugin.Resource, v *llx.RawData) (ok bool) { r.(*mqlService).__id, ok = v.Value.(string) return @@ -8357,11 +8386,12 @@ func (c *mqlSshd) MqlID() string { type mqlSshdConfig struct { MqlRuntime *plugin.Runtime __id string - // optional: if you define mqlSshdConfigInternal it will be used here + mqlSshdConfigInternal File plugin.TValue[*mqlFile] Files plugin.TValue[[]interface{}] Content plugin.TValue[string] Params plugin.TValue[map[string]interface{}] + Blocks plugin.TValue[[]interface{}] Ciphers plugin.TValue[[]interface{}] Macs plugin.TValue[[]interface{}] Kexs plugin.TValue[[]interface{}] @@ -8465,6 +8495,27 @@ func (c *mqlSshdConfig) GetParams() *plugin.TValue[map[string]interface{}] { }) } +func (c *mqlSshdConfig) GetBlocks() *plugin.TValue[[]interface{}] { + return plugin.GetOrCompute[[]interface{}](&c.Blocks, func() ([]interface{}, error) { + if c.MqlRuntime.HasRecording { + d, err := c.MqlRuntime.FieldResourceFromRecording("sshd.config", c.__id, "blocks") + if err != nil { + return nil, err + } + if d != nil { + return d.Value.([]interface{}), nil + } + } + + vargContent := c.GetContent() + if vargContent.Error != nil { + return nil, vargContent.Error + } + + return c.blocks(vargContent.Data) + }) +} + func (c *mqlSshdConfig) GetCiphers() *plugin.TValue[[]interface{}] { return plugin.GetOrCompute[[]interface{}](&c.Ciphers, func() ([]interface{}, error) { vargParams := c.GetParams() @@ -8520,6 +8571,55 @@ func (c *mqlSshdConfig) GetPermitRootLogin() *plugin.TValue[[]interface{}] { }) } +// mqlSshdConfigMatchBlock for the sshd.config.matchBlock resource +type mqlSshdConfigMatchBlock struct { + MqlRuntime *plugin.Runtime + __id string + // optional: if you define mqlSshdConfigMatchBlockInternal it will be used here + Criteria plugin.TValue[string] + Params plugin.TValue[map[string]interface{}] +} + +// createSshdConfigMatchBlock creates a new instance of this resource +func createSshdConfigMatchBlock(runtime *plugin.Runtime, args map[string]*llx.RawData) (plugin.Resource, error) { + res := &mqlSshdConfigMatchBlock{ + MqlRuntime: runtime, + } + + err := SetAllData(res, args) + if err != nil { + return res, err + } + + // to override __id implement: id() (string, error) + + if runtime.HasRecording { + args, err = runtime.ResourceFromRecording("sshd.config.matchBlock", res.__id) + if err != nil || args == nil { + return res, err + } + return res, SetAllData(res, args) + } + + return res, nil +} + +func (c *mqlSshdConfigMatchBlock) MqlName() string { + return "sshd.config.matchBlock" +} + +func (c *mqlSshdConfigMatchBlock) MqlID() string { + return c.__id +} + +func (c *mqlSshdConfigMatchBlock) GetCriteria() *plugin.TValue[string] { + return &c.Criteria +} + +func (c *mqlSshdConfigMatchBlock) GetParams() *plugin.TValue[map[string]interface{}] { + return &c.Params +} + // mqlService for the service resource type mqlService struct { MqlRuntime *plugin.Runtime diff --git a/providers/os/resources/os.lr.manifest.yaml b/providers/os/resources/os.lr.manifest.yaml index 0e08bbcb76..d20b09156f 100644 --- a/providers/os/resources/os.lr.manifest.yaml +++ b/providers/os/resources/os.lr.manifest.yaml @@ -838,6 +838,8 @@ resources: min_mondoo_version: 5.15.0 sshd.config: fields: + blocks: + min_mondoo_version: latest ciphers: {} content: {} file: {} @@ -853,6 +855,13 @@ resources: snippets: - query: sshd.config.params['Banner'] == '/etc/ssh/sshd-banner' title: Check that the SSH banner is sourced from /etc/ssh/sshd-banner + sshd.config.matchBlock: + fields: + condition: {} + criteria: {} + params: {} + is_private: true + min_mondoo_version: latest user: fields: authorizedkeys: {} diff --git a/providers/os/resources/sshd.go b/providers/os/resources/sshd.go index 37d1d4f58a..53c173e5fb 100644 --- a/providers/os/resources/sshd.go +++ b/providers/os/resources/sshd.go @@ -8,13 +8,19 @@ import ( "errors" "fmt" "strings" + "sync" "go.mondoo.com/cnquery/v10/llx" "go.mondoo.com/cnquery/v10/providers-sdk/v1/plugin" "go.mondoo.com/cnquery/v10/providers/os/connection/shared" "go.mondoo.com/cnquery/v10/providers/os/resources/sshd" + "go.mondoo.com/cnquery/v10/types" ) +type mqlSshdConfigInternal struct { + lock sync.Mutex +} + func initSshdConfig(runtime *plugin.Runtime, args map[string]*llx.RawData) (map[string]*llx.RawData, plugin.Resource, error) { if x, ok := args["path"]; ok { path, ok := x.Value.(string) @@ -117,19 +123,49 @@ func (s *mqlSshdConfig) content(files []interface{}) (string, error) { return fullContent, nil } -func (s *mqlSshdConfig) params(content string) (map[string]interface{}, error) { - params, err := sshd.Params(content) - if err != nil { - return nil, err +func matchBlocks2Resources(m sshd.MatchBlocks, runtime *plugin.Runtime, ownerID string) ([]any, error) { + res := make([]any, len(m)) + for i := range m { + cur := m[i] + obj, err := CreateResource(runtime, "sshd.config.matchBlock", map[string]*llx.RawData{ + "__id": llx.StringData(ownerID + "\x00" + cur.Criteria), + "criteria": llx.StringData(cur.Criteria), + "params": llx.MapData(cur.Params, types.String), + }) + if err != nil { + return nil, err + } + res[i] = obj } + return res, nil +} - // convert map - res := map[string]interface{}{} - for k, v := range params { - res[k] = v +func (s *mqlSshdConfig) parse(content string) error { + s.lock.Lock() + defer s.lock.Unlock() + + params, err := sshd.ParseBlocks(content) + if err != nil { + s.Params = plugin.TValue[map[string]any]{Error: err, State: plugin.StateIsSet | plugin.StateIsNull} + s.Blocks = plugin.TValue[[]any]{Error: err, State: plugin.StateIsSet | plugin.StateIsNull} + } else { + blocks, err := matchBlocks2Resources(params, s.MqlRuntime, s.__id) + if err != nil { + return err + } + s.Params = plugin.TValue[map[string]any]{Data: params.Flatten(), State: plugin.StateIsSet} + s.Blocks = plugin.TValue[[]any]{Data: blocks, State: plugin.StateIsSet} } - return res, nil + return err +} + +func (s *mqlSshdConfig) params(content string) (map[string]any, error) { + return nil, s.parse(content) +} + +func (s *mqlSshdConfig) blocks(content string) ([]any, error) { + return nil, s.parse(content) } func (s *mqlSshdConfig) parseConfigEntrySlice(raw interface{}) ([]interface{}, error) { diff --git a/providers/os/resources/sshd/params.go b/providers/os/resources/sshd/params.go index fa7f95cded..1de4c2cd91 100644 --- a/providers/os/resources/sshd/params.go +++ b/providers/os/resources/sshd/params.go @@ -7,31 +7,104 @@ import ( "strings" ) -func Params(content string) (map[string]string, error) { +type MatchBlock struct { + Criteria string + // Note: we set the value type to any, but it must be a string. + // This is done due to type limitations in go and MQL's internal processing + Params map[string]any +} + +type MatchBlocks []*MatchBlock + +func (m MatchBlocks) Flatten() map[string]any { + if len(m) == 0 { + return nil + } + if len(m) == 1 { + return m[0].Params + } + + // We are using the first block as a starting point for the size. + // We can't just add the sizes of params across all blocks, because keys + // may be used across multiple blocks. It is likely that the size will + // have to grow, but it's the floor and a good starting point. + res := make(map[string]any, len(m[0].Params)) + matchConditions := []string{} + for i := range m { + cur := m[i] + + if cur.Criteria != "" { + matchConditions = append(matchConditions, cur.Criteria) + } + + for k, v := range cur.Params { + if x, ok := res[k]; ok { + res[k] = x.(string) + "," + v.(string) + } else { + res[k] = v + } + } + } + + // We are adding one flattened key for all match groups. This is + // more to be informative and consistent, rather than useful. + // The most useful way to access conditions is to cycle over all match blocks. + if len(matchConditions) != 0 { + res["Match"] = strings.Join(matchConditions, ",") + } + + return res +} + +func ParseBlocks(content string) (MatchBlocks, error) { lines := strings.Split(content, "\n") - res := make(map[string]string) + curBlock := &MatchBlock{ + Criteria: "", + Params: map[string]any{}, + } + res := []*MatchBlock{curBlock} + matchConditions := map[string]*MatchBlock{ + "": curBlock, + } + for _, textLine := range lines { l, err := ParseLine([]rune(textLine)) if err != nil { return nil, err } - k := l.key - if k == "" { + key := l.key + if key == "" { continue } // handle lower case entries and use proper ssh camel case - if sshKey, ok := SSH_Keywords[strings.ToLower(k)]; ok { - k = sshKey + if sshKey, ok := SSH_Keywords[strings.ToLower(key)]; ok { + key = sshKey + } + + if key == "Match" { + // This key is the only that we don't add to any params. It is stored + // in the condition of each block and can be accessed there. + condition := l.args + if b, ok := matchConditions[condition]; ok { + curBlock = b + } else { + curBlock = &MatchBlock{ + Criteria: condition, + Params: map[string]any{}, + } + matchConditions[condition] = curBlock + res = append(res, curBlock) + } + continue } - // check if we have an entry already - if val, ok := res[k]; ok { - res[k] = val + "," + l.args + if x, ok := curBlock.Params[key]; ok { + curBlock.Params[key] = x.(string) + "," + l.args } else { - res[k] = l.args + curBlock.Params[key] = l.args } } diff --git a/providers/os/resources/sshd/params_test.go b/providers/os/resources/sshd/params_test.go index f90e5dcb69..813e6a6d1b 100644 --- a/providers/os/resources/sshd/params_test.go +++ b/providers/os/resources/sshd/params_test.go @@ -15,7 +15,7 @@ func TestSSHParser(t *testing.T) { raw, err := os.ReadFile("./testdata/sshd_config") require.NoError(t, err) - sshParams, err := Params(string(raw)) + sshParams, err := ParseBlocks(string(raw)) if err != nil { t.Fatalf("cannot request file %v", err) } @@ -32,7 +32,7 @@ func TestSSHParseCaseInsensitive(t *testing.T) { raw, err := os.ReadFile("./testdata/case_insensitive") require.NoError(t, err) - sshParams, err := Params(string(raw)) + sshParams, err := ParseBlocks(string(raw)) if err != nil { t.Fatalf("cannot request file %v", err) } diff --git a/providers/os/resources/sshd_test.go b/providers/os/resources/sshd_test.go index 669908f1dc..d5e401e477 100644 --- a/providers/os/resources/sshd_test.go +++ b/providers/os/resources/sshd_test.go @@ -13,9 +13,9 @@ import ( func TestResource_SSHD(t *testing.T) { x.TestSimpleErrors(t, []testutils.SimpleTest{ { - Code: "sshd.config('1').params['2'] == '3'", + Code: "sshd.config('nopath').params['2'] == '3'", ResultIndex: 0, - Expectation: "sshd config does not exist in 1", + Expectation: "sshd config does not exist in nopath", }, }) @@ -77,4 +77,22 @@ func TestResource_SSHD(t *testing.T) { assert.Empty(t, res[0].Result().Error) assert.Equal(t, []interface{}{"no", "no"}, res[0].Data.Value) }) + + t.Run("parse blocks", func(t *testing.T) { + res := x.TestQuery(t, "sshd.config.blocks.map(criteria)") + assert.NotEmpty(t, res) + assert.Empty(t, res[0].Result().Error) + assert.Equal(t, []any{"", "Group sftp-users", "User myservice"}, res[0].Data.Value) + + res = x.TestQuery(t, "sshd.config.blocks.map(params.AllowTcpForwarding)") + assert.NotEmpty(t, res) + assert.Empty(t, res[0].Result().Error) + assert.Equal(t, []any{"no", "yes", nil}, res[0].Data.Value) + }) + t.Run("expose block match criteria in params.Match", func(t *testing.T) { + res := x.TestQuery(t, "sshd.config.params.Match") + assert.NotEmpty(t, res) + assert.Empty(t, res[0].Result().Error) + assert.Equal(t, "Group sftp-users,User myservice", res[0].Data.Value) + }) } From 28721f7b394131257e5db59c0f5f2fc821d44413 Mon Sep 17 00:00:00 2001 From: Dominik Richter Date: Sun, 4 Feb 2024 20:43:59 -0800 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=9F=A2=20fix=20ssh=20params=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Dominik Richter --- providers/os/resources/sshd/params_test.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/providers/os/resources/sshd/params_test.go b/providers/os/resources/sshd/params_test.go index 813e6a6d1b..a981a90305 100644 --- a/providers/os/resources/sshd/params_test.go +++ b/providers/os/resources/sshd/params_test.go @@ -23,9 +23,9 @@ func TestSSHParser(t *testing.T) { assert.NotNil(t, sshParams, "params are not nil") // check result for multiple host-keys - assert.Equal(t, "/etc/ssh/ssh_host_rsa_key,/etc/ssh/ssh_host_ecdsa_key,/etc/ssh/ssh_host_ed25519_key", sshParams["HostKey"]) - assert.Equal(t, "yes", sshParams["X11Forwarding"]) - assert.Equal(t, "60", sshParams["LoginGraceTime"]) + assert.Equal(t, "/etc/ssh/ssh_host_rsa_key,/etc/ssh/ssh_host_ecdsa_key,/etc/ssh/ssh_host_ed25519_key", sshParams[0].Params["HostKey"]) + assert.Equal(t, "yes", sshParams[0].Params["X11Forwarding"]) + assert.Equal(t, "60", sshParams[0].Params["LoginGraceTime"]) } func TestSSHParseCaseInsensitive(t *testing.T) { @@ -39,7 +39,7 @@ func TestSSHParseCaseInsensitive(t *testing.T) { assert.NotNil(t, sshParams, "params are not nil") - assert.Equal(t, "22", sshParams["Port"]) - assert.Equal(t, "any", sshParams["AddressFamily"]) - assert.Equal(t, "0.0.0.0", sshParams["ListenAddress"]) + assert.Equal(t, "22", sshParams[0].Params["Port"]) + assert.Equal(t, "any", sshParams[0].Params["AddressFamily"]) + assert.Equal(t, "0.0.0.0", sshParams[0].Params["ListenAddress"]) } From f13bdb390a19ddad32827d663e7af0ee07d2f473 Mon Sep 17 00:00:00 2001 From: Dominik Richter Date: Sun, 4 Feb 2024 20:48:19 -0800 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=9F=A2=20fix=20mqlc=20tests=20for=20n?= =?UTF-8?q?ew=20fields?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Dominik Richter --- mqlc/mqlc_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mqlc/mqlc_test.go b/mqlc/mqlc_test.go index 065a38dd50..cd3aa8fc5f 100644 --- a/mqlc/mqlc_test.go +++ b/mqlc/mqlc_test.go @@ -1912,7 +1912,7 @@ func TestSuggestions(t *testing.T) { { // resource suggestions "ssh", - []string{"os.unix.sshd", "sshd", "sshd.config", "windows.security.health"}, + []string{"os.unix.sshd", "sshd", "sshd.config", "sshd.config.matchBlock", "windows.security.health"}, errors.New("cannot find resource for identifier 'ssh'"), nil, },