mirror of
https://github.com/jtesta/ssh-audit.git
synced 2026-05-25 15:31:23 +02:00
Compare commits
30 Commits
v3.2.0
..
3b8a75e407
| Author | SHA1 | Date | |
|---|---|---|---|
| 3b8a75e407 | |||
| 67e11f82b3 | |||
| 2cd96f1785 | |||
| a4b78b752e | |||
| ac540c8b5f | |||
| e11492b7a3 | |||
| 02bc48c574 | |||
| 24d7d46c42 | |||
| e97bbd9782 | |||
| 6d57c7c0f7 | |||
| ea3258151e | |||
| f9032c8277 | |||
| d7398baad7 | |||
| 4621d52223 | |||
| 2a7cb13895 | |||
| 06ebdbd0fe | |||
| 7752023dc2 | |||
| a6f02ae8e8 | |||
| 9049c8476a | |||
| bbbdf71e50 | |||
| 92db5f0138 | |||
| bc2a89eb11 | |||
| ea117b203b | |||
| d8f8b7c57c | |||
| e42961fa9a | |||
| dcbc43acdf | |||
| 87e22ae26b | |||
| 46ec4e3edc | |||
| d19b154a46 | |||
| c5d90106e8 |
@@ -0,0 +1 @@
|
|||||||
|
github: jtesta
|
||||||
@@ -7,12 +7,12 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
|
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13.0-rc.2"]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
# ssh-audit
|
# ssh-audit
|
||||||
[](https://github.com/jtesta/ssh-audit/blob/master/LICENSE)
|
[](https://github.com/jtesta/ssh-audit/blob/master/LICENSE)
|
||||||
[](https://pypi.org/project/ssh-audit/)
|
|
||||||
[](https://hub.docker.com/r/positronsecurity/ssh-audit)
|
|
||||||
[](https://github.com/jtesta/ssh-audit/actions)
|
[](https://github.com/jtesta/ssh-audit/actions)
|
||||||
[](https://github.com/jtesta/ssh-audit/blob/master/CONTRIBUTING.md)
|
[](https://github.com/jtesta/ssh-audit/blob/master/CONTRIBUTING.md)
|
||||||
|
|
||||||
|
[](https://pypi.org/project/ssh-audit/)
|
||||||
|
[](https://formulae.brew.sh/formula/ssh-audit)
|
||||||
|
[](https://hub.docker.com/r/positronsecurity/ssh-audit)
|
||||||
|
[](https://snapcraft.io/ssh-audit)
|
||||||
|
|
||||||
|
[](https://github.com/sponsors/jtesta)
|
||||||
|
|
||||||
**ssh-audit** is a tool for ssh server & client configuration auditing.
|
**ssh-audit** is a tool for ssh server & client configuration auditing.
|
||||||
|
|
||||||
[jtesta/ssh-audit](https://github.com/jtesta/ssh-audit/) (v2.0+) is the updated and maintained version of ssh-audit forked from [arthepsy/ssh-audit](https://github.com/arthepsy/ssh-audit) (v1.x) due to inactivity.
|
[jtesta/ssh-audit](https://github.com/jtesta/ssh-audit/) (v2.0+) is the updated and maintained version of ssh-audit forked from [arthepsy/ssh-audit](https://github.com/arthepsy/ssh-audit) (v1.x) due to inactivity.
|
||||||
@@ -32,7 +37,7 @@
|
|||||||
- historical information from OpenSSH, Dropbear SSH and libssh;
|
- historical information from OpenSSH, Dropbear SSH and libssh;
|
||||||
- policy scans to ensure adherence to a hardened/standard configuration;
|
- policy scans to ensure adherence to a hardened/standard configuration;
|
||||||
- runs on Linux and Windows;
|
- runs on Linux and Windows;
|
||||||
- supports Python 3.8 - 3.12;
|
- supports Python 3.8 - 3.13;
|
||||||
- no dependencies
|
- no dependencies
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
@@ -89,7 +94,9 @@ usage: ssh-audit.py [options] <host>
|
|||||||
-t, --timeout=<secs> timeout (in seconds) for connection and reading
|
-t, --timeout=<secs> timeout (in seconds) for connection and reading
|
||||||
(default: 5)
|
(default: 5)
|
||||||
-T, --targets=<hosts.txt> a file containing a list of target hosts (one
|
-T, --targets=<hosts.txt> a file containing a list of target hosts (one
|
||||||
per line, format HOST[:PORT])
|
per line, format HOST[:PORT]). Use -p/--port
|
||||||
|
to set the default port for all hosts. Use
|
||||||
|
--threads to control concurrent scans.
|
||||||
--threads=<threads> number of threads to use when scanning multiple
|
--threads=<threads> number of threads to use when scanning multiple
|
||||||
targets (-T/--targets) (default: 32)
|
targets (-T/--targets) (default: 32)
|
||||||
-v, --verbose verbose output
|
-v, --verbose verbose output
|
||||||
@@ -202,7 +209,7 @@ To install from Dockerhub:
|
|||||||
```
|
```
|
||||||
$ docker pull positronsecurity/ssh-audit
|
$ docker pull positronsecurity/ssh-audit
|
||||||
```
|
```
|
||||||
(Then run with: `docker run -it -p 2222:2222 positronsecurity/ssh-audit 10.1.1.1`)
|
(Then run with: `docker run -it --rm -p 2222:2222 positronsecurity/ssh-audit 10.1.1.1`)
|
||||||
|
|
||||||
The status of various other platform packages can be found below (via Repology):
|
The status of various other platform packages can be found below (via Repology):
|
||||||
|
|
||||||
@@ -213,6 +220,21 @@ For convenience, a web front-end on top of the command-line tool is available at
|
|||||||
|
|
||||||
## ChangeLog
|
## ChangeLog
|
||||||
|
|
||||||
|
### v3.3.0-dev (???)
|
||||||
|
- Added Python 3.13 support.
|
||||||
|
- Added built-in policies for Ubuntu 24.04 LTS server & client, OpenSSH 9.8, and OpenSSH 9.9.
|
||||||
|
- Added IPv6 support for DHEat and connection rate tests.
|
||||||
|
- Added TCP port information to JSON policy scan results; credit [Fabian Malte Kopp](https://github.com/dreizehnutters).
|
||||||
|
- Added LANcom LCOS server recognition and Ed448 key extraction; credit [Daniel Lenski](https://github.com/dlenskiSB).
|
||||||
|
- Fixed crash when running with `-P` and `-T` options simultaneously.
|
||||||
|
- Fixed host key tests from only reporting a key type at most once despite multiple hosts supporting it; credit [Daniel Lenski](https://github.com/dlenskiSB).
|
||||||
|
- Fixed DHEat connection rate testing on MacOS X and BSD platforms; credit [Drew Noel](https://github.com/drewmnoel) and [Michael Osipov](https://github.com/michael-o).
|
||||||
|
- Fixed invalid JSON output when a socket error occurs while performing a client audit.
|
||||||
|
- When scanning multiple targets (using `-T`/`--targets`), the `-p`/`--port` option will now be used as the default port (set to 22 if `-p`/`--port` is not given). Hosts specified in the file can override this default with an explicit port number (i.e.: "host1:1234"). For example, when using `-T targets.txt -p 222`, all hosts in `targets.txt` that do not explicitly include a port number will default to 222; when using `-T targets.txt` (without `-p`), all hosts will use a default of 22.
|
||||||
|
- Now reports ECDSA and DSS fingerprints when in verbose mode; partial credit [Daniel Lenski](https://github.com/dlenskiSB).
|
||||||
|
- Added 1 new cipher: `grasshopper-ctr128`.
|
||||||
|
- Added 2 new key exchanges: `mlkem768x25519-sha256`, `sntrup761x25519-sha512`.
|
||||||
|
|
||||||
### v3.2.0 (2024-04-22)
|
### v3.2.0 (2024-04-22)
|
||||||
- Added implementation of the DHEat denial-of-service attack (see `--dheat` option; [CVE-2002-20001](https://nvd.nist.gov/vuln/detail/CVE-2002-20001)).
|
- Added implementation of the DHEat denial-of-service attack (see `--dheat` option; [CVE-2002-20001](https://nvd.nist.gov/vuln/detail/CVE-2002-20001)).
|
||||||
- Expanded filter of CBC ciphers to flag for the Terrapin vulnerability. It now includes more rarely found ciphers.
|
- Expanded filter of CBC ciphers to flag for the Terrapin vulnerability. It now includes more rarely found ciphers.
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ classifiers =
|
|||||||
Programming Language :: Python :: 3.10
|
Programming Language :: Python :: 3.10
|
||||||
Programming Language :: Python :: 3.11
|
Programming Language :: Python :: 3.11
|
||||||
Programming Language :: Python :: 3.12
|
Programming Language :: Python :: 3.12
|
||||||
|
Programming Language :: Python :: 3.13
|
||||||
Programming Language :: Python :: Implementation :: CPython
|
Programming Language :: Python :: Implementation :: CPython
|
||||||
Programming Language :: Python :: Implementation :: PyPy
|
Programming Language :: Python :: Implementation :: PyPy
|
||||||
Topic :: Security
|
Topic :: Security
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ BUILTIN_POLICIES: Dict[str, Dict[str, Union[Optional[str], Optional[List[str]],
|
|||||||
|
|
||||||
'Hardened Ubuntu Server 22.04 LTS (version 5)': {'version': '5', 'changelog': 'Added kex-strict-s-v00@openssh.com to kex list.', 'banner': None, 'compressions': None, 'host_keys': ['rsa-sha2-512', 'rsa-sha2-256', 'ssh-ed25519'], 'optional_host_keys': ['sk-ssh-ed25519@openssh.com', 'ssh-ed25519-cert-v01@openssh.com', 'sk-ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com'], 'kex': ['sntrup761x25519-sha512@openssh.com', 'curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256', 'kex-strict-s-v00@openssh.com'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes128-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-ctr'], 'macs': ['hmac-sha2-256-etm@openssh.com', 'hmac-sha2-512-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': {"rsa-sha2-256": {"hostkey_size": 4096}, "rsa-sha2-256-cert-v01@openssh.com": {"ca_key_size": 4096, "ca_key_type": "ssh-rsa", "hostkey_size": 4096}, "rsa-sha2-512": {"hostkey_size": 4096}, "rsa-sha2-512-cert-v01@openssh.com": {"ca_key_size": 4096, "ca_key_type": "ssh-rsa", "hostkey_size": 4096}, "sk-ssh-ed25519-cert-v01@openssh.com": {"ca_key_size": 256, "ca_key_type": "ssh-ed25519", "hostkey_size": 256}, "sk-ssh-ed25519@openssh.com": {"hostkey_size": 256}, "ssh-ed25519": {"hostkey_size": 256}, "ssh-ed25519-cert-v01@openssh.com": {"ca_key_size": 256, "ca_key_type": "ssh-ed25519", "hostkey_size": 256}}, 'dh_modulus_sizes': {'diffie-hellman-group-exchange-sha256': 3072}, 'server_policy': True},
|
'Hardened Ubuntu Server 22.04 LTS (version 5)': {'version': '5', 'changelog': 'Added kex-strict-s-v00@openssh.com to kex list.', 'banner': None, 'compressions': None, 'host_keys': ['rsa-sha2-512', 'rsa-sha2-256', 'ssh-ed25519'], 'optional_host_keys': ['sk-ssh-ed25519@openssh.com', 'ssh-ed25519-cert-v01@openssh.com', 'sk-ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com'], 'kex': ['sntrup761x25519-sha512@openssh.com', 'curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256', 'kex-strict-s-v00@openssh.com'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes128-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-ctr'], 'macs': ['hmac-sha2-256-etm@openssh.com', 'hmac-sha2-512-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': {"rsa-sha2-256": {"hostkey_size": 4096}, "rsa-sha2-256-cert-v01@openssh.com": {"ca_key_size": 4096, "ca_key_type": "ssh-rsa", "hostkey_size": 4096}, "rsa-sha2-512": {"hostkey_size": 4096}, "rsa-sha2-512-cert-v01@openssh.com": {"ca_key_size": 4096, "ca_key_type": "ssh-rsa", "hostkey_size": 4096}, "sk-ssh-ed25519-cert-v01@openssh.com": {"ca_key_size": 256, "ca_key_type": "ssh-ed25519", "hostkey_size": 256}, "sk-ssh-ed25519@openssh.com": {"hostkey_size": 256}, "ssh-ed25519": {"hostkey_size": 256}, "ssh-ed25519-cert-v01@openssh.com": {"ca_key_size": 256, "ca_key_type": "ssh-ed25519", "hostkey_size": 256}}, 'dh_modulus_sizes': {'diffie-hellman-group-exchange-sha256': 3072}, 'server_policy': True},
|
||||||
|
|
||||||
|
'Hardened Ubuntu Server 24.04 LTS (version 1)': {'version': '1', 'changelog': 'Initial version.', 'banner': None, 'compressions': None, 'host_keys': ['ssh-ed25519', 'rsa-sha2-512', 'rsa-sha2-256'], 'optional_host_keys': ['sk-ssh-ed25519@openssh.com', 'ssh-ed25519-cert-v01@openssh.com', 'sk-ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com'], 'kex': ['sntrup761x25519-sha512@openssh.com', 'curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256', 'diffie-hellman-group16-sha512', 'ext-info-s', 'kex-strict-s-v00@openssh.com'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-gcm@openssh.com', 'aes128-ctr'], 'macs': ['hmac-sha2-512-etm@openssh.com', 'hmac-sha2-256-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': {"rsa-sha2-256": {"hostkey_size": 4096}, "rsa-sha2-256-cert-v01@openssh.com": {"ca_key_size": 4096, "ca_key_type": "ssh-rsa", "hostkey_size": 4096}, "rsa-sha2-512": {"hostkey_size": 4096}, "rsa-sha2-512-cert-v01@openssh.com": {"ca_key_size": 4096, "ca_key_type": "ssh-rsa", "hostkey_size": 4096}, "sk-ssh-ed25519-cert-v01@openssh.com": {"ca_key_size": 256, "ca_key_type": "ssh-ed25519", "hostkey_size": 256}, "sk-ssh-ed25519@openssh.com": {"hostkey_size": 256}, "ssh-ed25519": {"hostkey_size": 256}, "ssh-ed25519-cert-v01@openssh.com": {"ca_key_size": 256, "ca_key_type": "ssh-ed25519", "hostkey_size": 256}}, 'dh_modulus_sizes': {'diffie-hellman-group-exchange-sha256': 3072}, 'server_policy': True},
|
||||||
|
|
||||||
# Generic OpenSSH Server policies
|
# Generic OpenSSH Server policies
|
||||||
|
|
||||||
@@ -94,6 +95,9 @@ BUILTIN_POLICIES: Dict[str, Dict[str, Union[Optional[str], Optional[List[str]],
|
|||||||
|
|
||||||
'Hardened OpenSSH Server v9.7 (version 1)': {'version': '1', 'changelog': 'Initial version.', 'banner': None, 'compressions': None, 'host_keys': ['rsa-sha2-512', 'rsa-sha2-256', 'ssh-ed25519'], 'optional_host_keys': ['sk-ssh-ed25519@openssh.com', 'ssh-ed25519-cert-v01@openssh.com', 'sk-ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com'], 'kex': ['sntrup761x25519-sha512@openssh.com', 'curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256', 'ext-info-s', 'kex-strict-s-v00@openssh.com'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes128-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-ctr'], 'macs': ['hmac-sha2-256-etm@openssh.com', 'hmac-sha2-512-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': {"rsa-sha2-256": {"hostkey_size": 4096}, "rsa-sha2-256-cert-v01@openssh.com": {"ca_key_size": 4096, "ca_key_type": "ssh-rsa", "hostkey_size": 4096}, "rsa-sha2-512": {"hostkey_size": 4096}, "rsa-sha2-512-cert-v01@openssh.com": {"ca_key_size": 4096, "ca_key_type": "ssh-rsa", "hostkey_size": 4096}, "sk-ssh-ed25519-cert-v01@openssh.com": {"ca_key_size": 256, "ca_key_type": "ssh-ed25519", "hostkey_size": 256}, "sk-ssh-ed25519@openssh.com": {"hostkey_size": 256}, "ssh-ed25519": {"hostkey_size": 256}, "ssh-ed25519-cert-v01@openssh.com": {"ca_key_size": 256, "ca_key_type": "ssh-ed25519", "hostkey_size": 256}}, 'dh_modulus_sizes': {'diffie-hellman-group-exchange-sha256': 3072}, 'server_policy': True},
|
'Hardened OpenSSH Server v9.7 (version 1)': {'version': '1', 'changelog': 'Initial version.', 'banner': None, 'compressions': None, 'host_keys': ['rsa-sha2-512', 'rsa-sha2-256', 'ssh-ed25519'], 'optional_host_keys': ['sk-ssh-ed25519@openssh.com', 'ssh-ed25519-cert-v01@openssh.com', 'sk-ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com'], 'kex': ['sntrup761x25519-sha512@openssh.com', 'curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256', 'ext-info-s', 'kex-strict-s-v00@openssh.com'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes128-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-ctr'], 'macs': ['hmac-sha2-256-etm@openssh.com', 'hmac-sha2-512-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': {"rsa-sha2-256": {"hostkey_size": 4096}, "rsa-sha2-256-cert-v01@openssh.com": {"ca_key_size": 4096, "ca_key_type": "ssh-rsa", "hostkey_size": 4096}, "rsa-sha2-512": {"hostkey_size": 4096}, "rsa-sha2-512-cert-v01@openssh.com": {"ca_key_size": 4096, "ca_key_type": "ssh-rsa", "hostkey_size": 4096}, "sk-ssh-ed25519-cert-v01@openssh.com": {"ca_key_size": 256, "ca_key_type": "ssh-ed25519", "hostkey_size": 256}, "sk-ssh-ed25519@openssh.com": {"hostkey_size": 256}, "ssh-ed25519": {"hostkey_size": 256}, "ssh-ed25519-cert-v01@openssh.com": {"ca_key_size": 256, "ca_key_type": "ssh-ed25519", "hostkey_size": 256}}, 'dh_modulus_sizes': {'diffie-hellman-group-exchange-sha256': 3072}, 'server_policy': True},
|
||||||
|
|
||||||
|
'Hardened OpenSSH Server v9.8 (version 1)': {'version': '1', 'changelog': 'Initial version.', 'banner': None, 'compressions': None, 'host_keys': ['rsa-sha2-512', 'rsa-sha2-256', 'ssh-ed25519'], 'optional_host_keys': ['sk-ssh-ed25519@openssh.com', 'ssh-ed25519-cert-v01@openssh.com', 'sk-ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com'], 'kex': ['sntrup761x25519-sha512@openssh.com', 'curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256', 'ext-info-s', 'kex-strict-s-v00@openssh.com'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes128-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-ctr'], 'macs': ['hmac-sha2-256-etm@openssh.com', 'hmac-sha2-512-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': {"rsa-sha2-256": {"hostkey_size": 4096}, "rsa-sha2-256-cert-v01@openssh.com": {"ca_key_size": 4096, "ca_key_type": "ssh-rsa", "hostkey_size": 4096}, "rsa-sha2-512": {"hostkey_size": 4096}, "rsa-sha2-512-cert-v01@openssh.com": {"ca_key_size": 4096, "ca_key_type": "ssh-rsa", "hostkey_size": 4096}, "sk-ssh-ed25519-cert-v01@openssh.com": {"ca_key_size": 256, "ca_key_type": "ssh-ed25519", "hostkey_size": 256}, "sk-ssh-ed25519@openssh.com": {"hostkey_size": 256}, "ssh-ed25519": {"hostkey_size": 256}, "ssh-ed25519-cert-v01@openssh.com": {"ca_key_size": 256, "ca_key_type": "ssh-ed25519", "hostkey_size": 256}}, 'dh_modulus_sizes': {'diffie-hellman-group-exchange-sha256': 3072}, 'server_policy': True},
|
||||||
|
|
||||||
|
'Hardened OpenSSH Server v9.9 (version 1)': {'version': '1', 'changelog': 'Initial version.', 'banner': None, 'compressions': None, 'host_keys': ['rsa-sha2-512', 'rsa-sha2-256', 'ssh-ed25519'], 'optional_host_keys': ['sk-ssh-ed25519@openssh.com', 'ssh-ed25519-cert-v01@openssh.com', 'sk-ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com'], 'kex': ['sntrup761x25519-sha512', 'sntrup761x25519-sha512@openssh.com', 'mlkem768x25519-sha256', 'curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256', 'ext-info-s', 'kex-strict-s-v00@openssh.com'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes128-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-ctr'], 'macs': ['hmac-sha2-256-etm@openssh.com', 'hmac-sha2-512-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': {"rsa-sha2-256": {"hostkey_size": 4096}, "rsa-sha2-256-cert-v01@openssh.com": {"ca_key_size": 4096, "ca_key_type": "ssh-rsa", "hostkey_size": 4096}, "rsa-sha2-512": {"hostkey_size": 4096}, "rsa-sha2-512-cert-v01@openssh.com": {"ca_key_size": 4096, "ca_key_type": "ssh-rsa", "hostkey_size": 4096}, "sk-ssh-ed25519-cert-v01@openssh.com": {"ca_key_size": 256, "ca_key_type": "ssh-ed25519", "hostkey_size": 256}, "sk-ssh-ed25519@openssh.com": {"hostkey_size": 256}, "ssh-ed25519": {"hostkey_size": 256}, "ssh-ed25519-cert-v01@openssh.com": {"ca_key_size": 256, "ca_key_type": "ssh-ed25519", "hostkey_size": 256}}, 'dh_modulus_sizes': {'diffie-hellman-group-exchange-sha256': 3072}, 'server_policy': True},
|
||||||
|
|
||||||
# Amazon Linux Policies
|
# Amazon Linux Policies
|
||||||
|
|
||||||
@@ -109,7 +113,6 @@ BUILTIN_POLICIES: Dict[str, Dict[str, Union[Optional[str], Optional[List[str]],
|
|||||||
|
|
||||||
'Hardened Rocky Linux Client 9 (version 1)': {'version': '1', 'changelog': 'Initial version.', 'banner': None, 'compressions': None, 'host_keys': ['sk-ssh-ed25519-cert-v01@openssh.com', 'ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'sk-ssh-ed25519@openssh.com', 'ssh-ed25519', 'rsa-sha2-512', 'rsa-sha2-256'], 'optional_host_keys': None, 'kex': ['sntrup761x25519-sha512@openssh.com', 'curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256', 'ext-info-c', 'kex-strict-c-v00@openssh.com'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes128-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-ctr'], 'macs': ['hmac-sha2-256-etm@openssh.com', 'hmac-sha2-512-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': None, 'dh_modulus_sizes': None, 'server_policy': False},
|
'Hardened Rocky Linux Client 9 (version 1)': {'version': '1', 'changelog': 'Initial version.', 'banner': None, 'compressions': None, 'host_keys': ['sk-ssh-ed25519-cert-v01@openssh.com', 'ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'sk-ssh-ed25519@openssh.com', 'ssh-ed25519', 'rsa-sha2-512', 'rsa-sha2-256'], 'optional_host_keys': None, 'kex': ['sntrup761x25519-sha512@openssh.com', 'curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256', 'ext-info-c', 'kex-strict-c-v00@openssh.com'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes128-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-ctr'], 'macs': ['hmac-sha2-256-etm@openssh.com', 'hmac-sha2-512-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': None, 'dh_modulus_sizes': None, 'server_policy': False},
|
||||||
|
|
||||||
|
|
||||||
# Ubuntu Client policies
|
# Ubuntu Client policies
|
||||||
|
|
||||||
'Hardened Ubuntu Client 16.04 LTS (version 2)': {'version': '2', 'changelog': 'No change log available.', 'banner': None, 'compressions': None, 'host_keys': ['ssh-ed25519', 'ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256', 'rsa-sha2-512'], 'optional_host_keys': None, 'kex': ['curve25519-sha256@libssh.org', 'diffie-hellman-group-exchange-sha256', 'ext-info-c'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes128-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-ctr'], 'macs': ['hmac-sha2-256-etm@openssh.com', 'hmac-sha2-512-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': None, 'dh_modulus_sizes': None, 'server_policy': False},
|
'Hardened Ubuntu Client 16.04 LTS (version 2)': {'version': '2', 'changelog': 'No change log available.', 'banner': None, 'compressions': None, 'host_keys': ['ssh-ed25519', 'ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256', 'rsa-sha2-512'], 'optional_host_keys': None, 'kex': ['curve25519-sha256@libssh.org', 'diffie-hellman-group-exchange-sha256', 'ext-info-c'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes128-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-ctr'], 'macs': ['hmac-sha2-256-etm@openssh.com', 'hmac-sha2-512-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': None, 'dh_modulus_sizes': None, 'server_policy': False},
|
||||||
@@ -120,5 +123,6 @@ BUILTIN_POLICIES: Dict[str, Dict[str, Union[Optional[str], Optional[List[str]],
|
|||||||
|
|
||||||
'Hardened Ubuntu Client 22.04 LTS (version 4)': {'version': '4', 'changelog': 'Added kex-strict-c-v00@openssh.com to kex list.', 'banner': None, 'compressions': None, 'host_keys': ['sk-ssh-ed25519-cert-v01@openssh.com', 'ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'sk-ssh-ed25519@openssh.com', 'ssh-ed25519', 'rsa-sha2-512', 'rsa-sha2-256'], 'optional_host_keys': None, 'kex': ['sntrup761x25519-sha512@openssh.com', 'curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256', 'ext-info-c', 'kex-strict-c-v00@openssh.com'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes128-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-ctr'], 'macs': ['hmac-sha2-256-etm@openssh.com', 'hmac-sha2-512-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': None, 'dh_modulus_sizes': None, 'server_policy': False},
|
'Hardened Ubuntu Client 22.04 LTS (version 4)': {'version': '4', 'changelog': 'Added kex-strict-c-v00@openssh.com to kex list.', 'banner': None, 'compressions': None, 'host_keys': ['sk-ssh-ed25519-cert-v01@openssh.com', 'ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'sk-ssh-ed25519@openssh.com', 'ssh-ed25519', 'rsa-sha2-512', 'rsa-sha2-256'], 'optional_host_keys': None, 'kex': ['sntrup761x25519-sha512@openssh.com', 'curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256', 'ext-info-c', 'kex-strict-c-v00@openssh.com'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes128-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-ctr'], 'macs': ['hmac-sha2-256-etm@openssh.com', 'hmac-sha2-512-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': None, 'dh_modulus_sizes': None, 'server_policy': False},
|
||||||
|
|
||||||
|
'Hardened Ubuntu Client 24.04 LTS (version 1)': {'version': '1', 'changelog': 'Initial version.', 'banner': None, 'compressions': None, 'host_keys': ['sk-ssh-ed25519-cert-v01@openssh.com', 'ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'sk-ssh-ed25519@openssh.com', 'ssh-ed25519', 'rsa-sha2-512', 'rsa-sha2-256'], 'optional_host_keys': None, 'kex': ['sntrup761x25519-sha512@openssh.com', 'curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256', 'diffie-hellman-group16-sha512', 'ext-info-c', 'kex-strict-c-v00@openssh.com'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-gcm@openssh.com', 'aes128-ctr'], 'macs': ['hmac-sha2-512-etm@openssh.com', 'hmac-sha2-256-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': None, 'dh_modulus_sizes': None, 'server_policy': False},
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+38
-10
@@ -21,6 +21,7 @@
|
|||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
THE SOFTWARE.
|
THE SOFTWARE.
|
||||||
"""
|
"""
|
||||||
|
import errno
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
import os
|
import os
|
||||||
import queue
|
import queue
|
||||||
@@ -160,6 +161,11 @@ class DHEat:
|
|||||||
# The SSH2_Kex object that we recieved from the server in a prior connection. We'll use it as a template to craft our own kex.
|
# The SSH2_Kex object that we recieved from the server in a prior connection. We'll use it as a template to craft our own kex.
|
||||||
self.kex = kex
|
self.kex = kex
|
||||||
|
|
||||||
|
# Resolve the target to an IP address depending on the user preferences (IPv4 or IPv6).
|
||||||
|
self.debug("Resolving target %s..." % self.target)
|
||||||
|
self.target_address_family, self.target_ip_address = DHEat._resolve_hostname(self.target, aconf.ip_version_preference)
|
||||||
|
self.debug("Resolved %s to %s (address family %u)" % (self.target, self.target_ip_address, self.target_address_family))
|
||||||
|
|
||||||
# The connection and read timeouts.
|
# The connection and read timeouts.
|
||||||
self.connect_timeout = aconf.timeout
|
self.connect_timeout = aconf.timeout
|
||||||
self.read_timeout = aconf.timeout
|
self.read_timeout = aconf.timeout
|
||||||
@@ -324,6 +330,11 @@ class DHEat:
|
|||||||
print("\n%sUnfortunately, this feature is not currently functional under Windows.%s This should get fixed in a future release. See: <https://github.com/jtesta/ssh-audit/issues/261>" % (DHEat.YELLOWB, DHEat.CLEAR))
|
print("\n%sUnfortunately, this feature is not currently functional under Windows.%s This should get fixed in a future release. See: <https://github.com/jtesta/ssh-audit/issues/261>" % (DHEat.YELLOWB, DHEat.CLEAR))
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
# Resolve the target into an IP address
|
||||||
|
out.d("Resolving target %s..." % aconf.host)
|
||||||
|
target_address_family, target_ip_address = DHEat._resolve_hostname(aconf.host, aconf.ip_version_preference)
|
||||||
|
out.d("Resolved %s to %s (address family %u)" % (aconf.host, target_ip_address, target_address_family))
|
||||||
|
|
||||||
spinner = ["-", "\\", "|", "/"]
|
spinner = ["-", "\\", "|", "/"]
|
||||||
spinner_index = 0
|
spinner_index = 0
|
||||||
|
|
||||||
@@ -349,7 +360,7 @@ class DHEat:
|
|||||||
rate_str = " at a max rate of %s%u%s connections per second" % (DHEat.WHITEB, aconf.conn_rate_test_target_rate, DHEat.CLEAR)
|
rate_str = " at a max rate of %s%u%s connections per second" % (DHEat.WHITEB, aconf.conn_rate_test_target_rate, DHEat.CLEAR)
|
||||||
|
|
||||||
print()
|
print()
|
||||||
print("Performing non-disruptive rate test against %s[%s]:%u%s with %s%u%s concurrent sockets%s. No Diffie-Hellman requests will be sent." % (DHEat.WHITEB, aconf.host, aconf.port, DHEat.CLEAR, DHEat.WHITEB, concurrent_sockets, DHEat.CLEAR, rate_str))
|
print("Performing non-disruptive rate test against %s[%s]:%u%s with %s%u%s concurrent sockets%s. No Diffie-Hellman requests will be sent." % (DHEat.WHITEB, target_ip_address, aconf.port, DHEat.CLEAR, DHEat.WHITEB, concurrent_sockets, DHEat.CLEAR, rate_str))
|
||||||
print()
|
print()
|
||||||
|
|
||||||
# Make room for the multi-line output.
|
# Make room for the multi-line output.
|
||||||
@@ -426,16 +437,16 @@ class DHEat:
|
|||||||
|
|
||||||
# Open new sockets until we've hit the number of concurrent sockets, or if we exceeded the number of maximum connections.
|
# Open new sockets until we've hit the number of concurrent sockets, or if we exceeded the number of maximum connections.
|
||||||
while (len(socket_dict) < concurrent_sockets) and (len(socket_dict) + num_opened_connections < max_connections):
|
while (len(socket_dict) < concurrent_sockets) and (len(socket_dict) + num_opened_connections < max_connections):
|
||||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
s = socket.socket(target_address_family, socket.SOCK_STREAM)
|
||||||
s.setblocking(False)
|
s.setblocking(False)
|
||||||
|
|
||||||
# out.d("Creating socket (%u of %u already exist)..." % (len(socket_dict), concurrent_sockets), write_now=True)
|
# out.d("Creating socket (%u of %u already exist)..." % (len(socket_dict), concurrent_sockets), write_now=True)
|
||||||
ret = s.connect_ex((aconf.host, aconf.port))
|
ret = s.connect_ex((target_ip_address, aconf.port))
|
||||||
num_attempted_connections += 1
|
num_attempted_connections += 1
|
||||||
if ret in [0, 115]: # Check if connection is successful or EINPROGRESS.
|
if ret in [0, errno.EINPROGRESS]:
|
||||||
socket_dict[s] = now
|
socket_dict[s] = now
|
||||||
else:
|
else:
|
||||||
out.d("connect_ex() returned: %d" % ret, write_now=True)
|
out.d("connect_ex() returned: %s (%d)" % (os.strerror(ret), ret), write_now=True)
|
||||||
|
|
||||||
# out.d("Calling select() on %u sockets..." % len(socket_dict), write_now=True)
|
# out.d("Calling select() on %u sockets..." % len(socket_dict), write_now=True)
|
||||||
socket_list: List[socket.socket] = [*socket_dict] # Get a list of sockets from the dictionary.
|
socket_list: List[socket.socket] = [*socket_dict] # Get a list of sockets from the dictionary.
|
||||||
@@ -743,6 +754,22 @@ class DHEat:
|
|||||||
print()
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _resolve_hostname(host: str, ip_version_preference: List[int]) -> Tuple[int, str]:
|
||||||
|
'''Resolves a hostname to its IPv4 or IPv6 address, depending on user preference.'''
|
||||||
|
|
||||||
|
family = socket.AF_UNSPEC
|
||||||
|
if len(ip_version_preference) == 1:
|
||||||
|
family = socket.AF_INET if ip_version_preference[0] == 4 else socket.AF_INET6
|
||||||
|
|
||||||
|
r = socket.getaddrinfo(host, 0, family, socket.SOCK_STREAM)
|
||||||
|
for address_family, socktype, _, _, addr in r:
|
||||||
|
if socktype == socket.SOCK_STREAM:
|
||||||
|
return address_family, addr[0]
|
||||||
|
|
||||||
|
return -1, ''
|
||||||
|
|
||||||
|
|
||||||
def _run(self) -> bool:
|
def _run(self) -> bool:
|
||||||
'''Where all the magic happens.'''
|
'''Where all the magic happens.'''
|
||||||
|
|
||||||
@@ -751,7 +778,7 @@ class DHEat:
|
|||||||
if sys.platform == "win32":
|
if sys.platform == "win32":
|
||||||
self.output("%sWARNING:%s this feature has not been thoroughly tested on Windows. It may perform worse than on UNIX OSes." % (self.YELLOWB, self.CLEAR))
|
self.output("%sWARNING:%s this feature has not been thoroughly tested on Windows. It may perform worse than on UNIX OSes." % (self.YELLOWB, self.CLEAR))
|
||||||
|
|
||||||
self.output("Running DHEat test against %s[%s]:%u%s with %s%u%s concurrent sockets..." % (self.WHITEB, self.target, self.port, self.CLEAR, self.WHITEB, self.concurrent_connections, self.CLEAR))
|
self.output("Running DHEat test against %s[%s]:%u%s with %s%u%s concurrent sockets..." % (self.WHITEB, self.target_ip_address, self.port, self.CLEAR, self.WHITEB, self.concurrent_connections, self.CLEAR))
|
||||||
|
|
||||||
# If the user didn't specify an exact kex algorithm to test, check our prioritized list against what the server supports. Larger p-values (such as group18: 8192-bits) cause the most strain on the server.
|
# If the user didn't specify an exact kex algorithm to test, check our prioritized list against what the server supports. Larger p-values (such as group18: 8192-bits) cause the most strain on the server.
|
||||||
chosen_alg = ""
|
chosen_alg = ""
|
||||||
@@ -894,7 +921,8 @@ class DHEat:
|
|||||||
|
|
||||||
# Copy variables from the object (which might exist in another process?). This might cut down on inter-process overhead.
|
# Copy variables from the object (which might exist in another process?). This might cut down on inter-process overhead.
|
||||||
connect_timeout = self.connect_timeout
|
connect_timeout = self.connect_timeout
|
||||||
target = self.target
|
target_ip_address = self.target_ip_address
|
||||||
|
target_address_family = self.target_address_family
|
||||||
port = self.port
|
port = self.port
|
||||||
|
|
||||||
# Determine if we are attacking with a GEX.
|
# Determine if we are attacking with a GEX.
|
||||||
@@ -945,17 +973,17 @@ class DHEat:
|
|||||||
num_socket_exceptions = 0
|
num_socket_exceptions = 0
|
||||||
num_openssh_throttled_connections = 0
|
num_openssh_throttled_connections = 0
|
||||||
|
|
||||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
s = socket.socket(target_address_family, socket.SOCK_STREAM)
|
||||||
s.settimeout(connect_timeout)
|
s.settimeout(connect_timeout)
|
||||||
|
|
||||||
# Loop until a successful TCP connection is made.
|
# Loop until a successful TCP connection is made.
|
||||||
connected = False
|
connected = False
|
||||||
while not connected:
|
while not connected:
|
||||||
|
|
||||||
# self.debug("Connecting to %s:%d" % (self.target, self.port))
|
# self.debug("Connecting to %s:%d" % (self.target_ip_address, self.port))
|
||||||
try:
|
try:
|
||||||
num_attempted_tcp_connections += 1
|
num_attempted_tcp_connections += 1
|
||||||
s.connect((target, port))
|
s.connect((target_ip_address, port))
|
||||||
connected = True
|
connected = True
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
self.debug("Failed to connect: %s" % str(e))
|
self.debug("Failed to connect: %s" % str(e))
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
THE SOFTWARE.
|
THE SOFTWARE.
|
||||||
"""
|
"""
|
||||||
# The version to display.
|
# The version to display.
|
||||||
VERSION = 'v3.2.0'
|
VERSION = 'v3.3.0-dev'
|
||||||
|
|
||||||
# SSH software to impersonate
|
# SSH software to impersonate
|
||||||
SSH_HEADER = 'SSH-{0}-OpenSSH_8.2'
|
SSH_HEADER = 'SSH-{0}-OpenSSH_8.2'
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (C) 2017-2023 Joe Testa (jtesta@positronsecurity.com)
|
Copyright (C) 2017-2024 Joe Testa (jtesta@positronsecurity.com)
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
@@ -40,7 +40,7 @@ class HostKeyTest:
|
|||||||
# Tracks the RSA host key types. As of this writing, testing one in this family yields valid results for the rest.
|
# Tracks the RSA host key types. As of this writing, testing one in this family yields valid results for the rest.
|
||||||
RSA_FAMILY = ['ssh-rsa', 'rsa-sha2-256', 'rsa-sha2-512']
|
RSA_FAMILY = ['ssh-rsa', 'rsa-sha2-256', 'rsa-sha2-512']
|
||||||
|
|
||||||
# Dict holding the host key types we should extract & parse. 'cert' is True to denote that a host key type handles certificates (thus requires additional parsing). 'variable_key_len' is True for host key types that can have variable sizes (True only for RSA types, as the rest are of fixed-size). After the host key type is fully parsed, the key 'parsed' is added with a value of True.
|
# Dict holding the host key types we should extract & parse. 'cert' is True to denote that a host key type handles certificates (thus requires additional parsing). 'variable_key_len' is True for host key types that can have variable sizes (True only for RSA types, as the rest are of fixed-size).
|
||||||
HOST_KEY_TYPES = {
|
HOST_KEY_TYPES = {
|
||||||
'ssh-rsa': {'cert': False, 'variable_key_len': True},
|
'ssh-rsa': {'cert': False, 'variable_key_len': True},
|
||||||
'rsa-sha2-256': {'cert': False, 'variable_key_len': True},
|
'rsa-sha2-256': {'cert': False, 'variable_key_len': True},
|
||||||
@@ -52,6 +52,20 @@ class HostKeyTest:
|
|||||||
|
|
||||||
'ssh-ed25519': {'cert': False, 'variable_key_len': False},
|
'ssh-ed25519': {'cert': False, 'variable_key_len': False},
|
||||||
'ssh-ed25519-cert-v01@openssh.com': {'cert': True, 'variable_key_len': False},
|
'ssh-ed25519-cert-v01@openssh.com': {'cert': True, 'variable_key_len': False},
|
||||||
|
|
||||||
|
'ssh-ed448': {'cert': False, 'variable_key_len': False},
|
||||||
|
# 'ssh-ed448-cert-v01@openssh.com': {'cert': True, 'variable_key_len': False},
|
||||||
|
|
||||||
|
'ecdsa-sha2-nistp256': {'cert': False, 'variable_key_len': False},
|
||||||
|
'ecdsa-sha2-nistp384': {'cert': False, 'variable_key_len': False},
|
||||||
|
'ecdsa-sha2-nistp521': {'cert': False, 'variable_key_len': False},
|
||||||
|
|
||||||
|
'ecdsa-sha2-nistp256-cert-v01@openssh.com': {'cert': True, 'variable_key_len': False},
|
||||||
|
'ecdsa-sha2-nistp384-cert-v01@openssh.com': {'cert': True, 'variable_key_len': False},
|
||||||
|
'ecdsa-sha2-nistp521-cert-v01@openssh.com': {'cert': True, 'variable_key_len': False},
|
||||||
|
|
||||||
|
'ssh-dss': {'cert': False, 'variable_key_len': True},
|
||||||
|
'ssh-dss-cert-v01@openssh.com': {'cert': True, 'variable_key_len': True},
|
||||||
}
|
}
|
||||||
|
|
||||||
TWO2K_MODULUS_WARNING = '2048-bit modulus only provides 112-bits of symmetric strength'
|
TWO2K_MODULUS_WARNING = '2048-bit modulus only provides 112-bits of symmetric strength'
|
||||||
@@ -93,6 +107,7 @@ class HostKeyTest:
|
|||||||
def perform_test(out: 'OutputBuffer', s: 'SSH_Socket', server_kex: 'SSH2_Kex', kex_str: str, kex_group: 'KexDH', host_key_types: Dict[str, Dict[str, bool]]) -> None:
|
def perform_test(out: 'OutputBuffer', s: 'SSH_Socket', server_kex: 'SSH2_Kex', kex_str: str, kex_group: 'KexDH', host_key_types: Dict[str, Dict[str, bool]]) -> None:
|
||||||
hostkey_modulus_size = 0
|
hostkey_modulus_size = 0
|
||||||
ca_modulus_size = 0
|
ca_modulus_size = 0
|
||||||
|
parsed_host_key_types = set()
|
||||||
|
|
||||||
# If the connection still exists, close it so we can test
|
# If the connection still exists, close it so we can test
|
||||||
# using a clean slate (otherwise it may exist in a non-testable
|
# using a clean slate (otherwise it may exist in a non-testable
|
||||||
@@ -106,7 +121,7 @@ class HostKeyTest:
|
|||||||
key_warn_comments = []
|
key_warn_comments = []
|
||||||
|
|
||||||
# Skip those already handled (i.e.: those in the RSA family, as testing one tests them all).
|
# Skip those already handled (i.e.: those in the RSA family, as testing one tests them all).
|
||||||
if 'parsed' in host_key_types[host_key_type] and host_key_types[host_key_type]['parsed']:
|
if host_key_type in parsed_host_key_types:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# If this host key type is supported by the server, we test it.
|
# If this host key type is supported by the server, we test it.
|
||||||
@@ -136,7 +151,12 @@ class HostKeyTest:
|
|||||||
_, payload = s.read_packet()
|
_, payload = s.read_packet()
|
||||||
SSH2_Kex.parse(out, payload)
|
SSH2_Kex.parse(out, payload)
|
||||||
except Exception:
|
except Exception:
|
||||||
out.v("Failed to parse server's kex. Stack trace:\n%s" % str(traceback.format_exc()), write_now=True)
|
msg = "Failed to parse server's kex."
|
||||||
|
if not out.debug:
|
||||||
|
msg += " Re-run in debug mode to see stack trace."
|
||||||
|
|
||||||
|
out.v(msg, write_now=True)
|
||||||
|
out.d("Stack trace:\n%s" % str(traceback.format_exc()), write_now=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Do the initial DH exchange. The server responds back
|
# Do the initial DH exchange. The server responds back
|
||||||
@@ -147,7 +167,12 @@ class HostKeyTest:
|
|||||||
kex_reply = kex_group.recv_reply(s)
|
kex_reply = kex_group.recv_reply(s)
|
||||||
raw_hostkey_bytes = kex_reply if kex_reply is not None else b''
|
raw_hostkey_bytes = kex_reply if kex_reply is not None else b''
|
||||||
except KexDHException:
|
except KexDHException:
|
||||||
out.v("Failed to parse server's host key. Stack trace:\n%s" % str(traceback.format_exc()), write_now=True)
|
msg = "Failed to parse server's host key."
|
||||||
|
if not out.debug:
|
||||||
|
msg += " Re-run in debug mode to see stack trace."
|
||||||
|
|
||||||
|
out.v(msg, write_now=True)
|
||||||
|
out.d("Stack trace:\n%s" % str(traceback.format_exc()), write_now=True)
|
||||||
|
|
||||||
# Since parsing this host key failed, there's nothing more to do but close the socket and move on to the next host key type.
|
# Since parsing this host key failed, there's nothing more to do but close the socket and move on to the next host key type.
|
||||||
s.close()
|
s.close()
|
||||||
@@ -157,6 +182,7 @@ class HostKeyTest:
|
|||||||
ca_key_type = kex_group.get_ca_type()
|
ca_key_type = kex_group.get_ca_type()
|
||||||
ca_modulus_size = kex_group.get_ca_size()
|
ca_modulus_size = kex_group.get_ca_size()
|
||||||
out.d("Hostkey type: [%s]; hostkey size: %u; CA type: [%s]; CA modulus size: %u" % (host_key_type, hostkey_modulus_size, ca_key_type, ca_modulus_size), write_now=True)
|
out.d("Hostkey type: [%s]; hostkey size: %u; CA type: [%s]; CA modulus size: %u" % (host_key_type, hostkey_modulus_size, ca_key_type, ca_modulus_size), write_now=True)
|
||||||
|
out.d("Raw hostkey bytes (%d): [%s]" % (len(raw_hostkey_bytes), raw_hostkey_bytes.hex()), write_now=True)
|
||||||
|
|
||||||
# Record all the host key info.
|
# Record all the host key info.
|
||||||
server_kex.set_host_key(host_key_type, raw_hostkey_bytes, hostkey_modulus_size, ca_key_type, ca_modulus_size)
|
server_kex.set_host_key(host_key_type, raw_hostkey_bytes, hostkey_modulus_size, ca_key_type, ca_modulus_size)
|
||||||
@@ -186,7 +212,7 @@ class HostKeyTest:
|
|||||||
cakey_warn_str = HostKeyTest.SMALL_ECC_MODULUS_WARNING
|
cakey_warn_str = HostKeyTest.SMALL_ECC_MODULUS_WARNING
|
||||||
|
|
||||||
# Keys smaller than 2048 result in a failure. Keys smaller 3072 result in a warning. Update the database accordingly.
|
# Keys smaller than 2048 result in a failure. Keys smaller 3072 result in a warning. Update the database accordingly.
|
||||||
if (cert is False) and (hostkey_modulus_size < hostkey_min_good):
|
if (cert is False) and (hostkey_modulus_size < hostkey_min_good) and (host_key_type != 'ssh-dss'): # Skip ssh-dss, otherwise we get duplicate failure messages (SSH2_KexDB will always flag it).
|
||||||
|
|
||||||
# If the key is under 2048, add to the failure list.
|
# If the key is under 2048, add to the failure list.
|
||||||
if hostkey_modulus_size < hostkey_min_warn:
|
if hostkey_modulus_size < hostkey_min_warn:
|
||||||
@@ -216,7 +242,7 @@ class HostKeyTest:
|
|||||||
# If this host key type is in the RSA family, then mark them all as parsed (since results in one are valid for them all).
|
# If this host key type is in the RSA family, then mark them all as parsed (since results in one are valid for them all).
|
||||||
if host_key_type in HostKeyTest.RSA_FAMILY:
|
if host_key_type in HostKeyTest.RSA_FAMILY:
|
||||||
for rsa_type in HostKeyTest.RSA_FAMILY:
|
for rsa_type in HostKeyTest.RSA_FAMILY:
|
||||||
host_key_types[rsa_type]['parsed'] = True
|
parsed_host_key_types.add(rsa_type)
|
||||||
|
|
||||||
# If the current key is a member of the RSA family, then populate all RSA family members with the same
|
# If the current key is a member of the RSA family, then populate all RSA family members with the same
|
||||||
# failure and/or warning comments.
|
# failure and/or warning comments.
|
||||||
@@ -228,7 +254,7 @@ class HostKeyTest:
|
|||||||
db['key'][rsa_type][2].extend(key_warn_comments)
|
db['key'][rsa_type][2].extend(key_warn_comments)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
host_key_types[host_key_type]['parsed'] = True
|
parsed_host_key_types.add(host_key_type)
|
||||||
db = SSH2_KexDB.get_db()
|
db = SSH2_KexDB.get_db()
|
||||||
while len(db['key'][host_key_type]) < 3:
|
while len(db['key'][host_key_type]) < 3:
|
||||||
db['key'][host_key_type].append([])
|
db['key'][host_key_type].append([])
|
||||||
|
|||||||
@@ -134,6 +134,9 @@ class KexDH: # pragma: nocover
|
|||||||
if self.__hostkey_type == 'ssh-ed25519':
|
if self.__hostkey_type == 'ssh-ed25519':
|
||||||
self.out.d("%s has a fixed host key modulus of 32." % self.__hostkey_type)
|
self.out.d("%s has a fixed host key modulus of 32." % self.__hostkey_type)
|
||||||
self.__hostkey_n_len = 32
|
self.__hostkey_n_len = 32
|
||||||
|
elif self.__hostkey_type == 'ssh-ed448':
|
||||||
|
self.out.d("%s has a fixed host key modulus of 57." % self.__hostkey_type)
|
||||||
|
self.__hostkey_n_len = 57
|
||||||
else:
|
else:
|
||||||
# Here is the modulus size & actual modulus of the host key public key.
|
# Here is the modulus size & actual modulus of the host key public key.
|
||||||
hostkey_n, self.__hostkey_n_len, ptr = KexDH.__get_bytes(hostkey, ptr)
|
hostkey_n, self.__hostkey_n_len, ptr = KexDH.__get_bytes(hostkey, ptr)
|
||||||
|
|||||||
@@ -605,3 +605,29 @@ macs = %s
|
|||||||
dh_modulus_sizes_str = str(self._dh_modulus_sizes)
|
dh_modulus_sizes_str = str(self._dh_modulus_sizes)
|
||||||
|
|
||||||
return "Name: %s\nVersion: %s\nAllow Algorithm Subset and/or Reordering: %r\nBanner: %s\nCompressions: %s\nHost Keys: %s\nOptional Host Keys: %s\nKey Exchanges: %s\nCiphers: %s\nMACs: %s\nHost Key Sizes: %s\nDH Modulus Sizes: %s\nServer Policy: %r" % (name, version, self._allow_algorithm_subset_and_reordering, banner, compressions_str, host_keys_str, optional_host_keys_str, kex_str, ciphers_str, macs_str, hostkey_sizes_str, dh_modulus_sizes_str, self._server_policy)
|
return "Name: %s\nVersion: %s\nAllow Algorithm Subset and/or Reordering: %r\nBanner: %s\nCompressions: %s\nHost Keys: %s\nOptional Host Keys: %s\nKey Exchanges: %s\nCiphers: %s\nMACs: %s\nHost Key Sizes: %s\nDH Modulus Sizes: %s\nServer Policy: %r" % (name, version, self._allow_algorithm_subset_and_reordering, banner, compressions_str, host_keys_str, optional_host_keys_str, kex_str, ciphers_str, macs_str, hostkey_sizes_str, dh_modulus_sizes_str, self._server_policy)
|
||||||
|
|
||||||
|
|
||||||
|
def __getstate__(self) -> Dict[str, Any]:
|
||||||
|
'''Called when pickling this object. The file descriptor isn't serializable, so we'll remove it from the state and include a string representation.'''
|
||||||
|
|
||||||
|
state = self.__dict__.copy()
|
||||||
|
|
||||||
|
if state['_warning_target'] == sys.stdout:
|
||||||
|
state['_warning_target_type'] = 'stdout'
|
||||||
|
else:
|
||||||
|
state['_warning_target_type'] = 'stderr'
|
||||||
|
|
||||||
|
del state['_warning_target']
|
||||||
|
return state
|
||||||
|
|
||||||
|
|
||||||
|
def __setstate__(self, state: Dict[str, Any]) -> None:
|
||||||
|
'''Called when unpickling this object. Based on the string representation of the file descriptor, we'll restore the right handle.'''
|
||||||
|
|
||||||
|
if state['_warning_target_type'] == 'stdout':
|
||||||
|
state['_warning_target'] = sys.stdout
|
||||||
|
else:
|
||||||
|
state['_warning_target'] = sys.stderr
|
||||||
|
|
||||||
|
del state['_warning_target_type']
|
||||||
|
self.__dict__.update(state)
|
||||||
|
|||||||
@@ -224,4 +224,8 @@ class Software:
|
|||||||
mx = re.match(r'^PuTTY_Release_(.*)', software)
|
mx = re.match(r'^PuTTY_Release_(.*)', software)
|
||||||
if mx:
|
if mx:
|
||||||
return cls(None, Product.PuTTY, mx.group(1), None, None)
|
return cls(None, Product.PuTTY, mx.group(1), None, None)
|
||||||
|
mx = re.match(r'^lancom(.*)', software)
|
||||||
|
if mx:
|
||||||
|
v, p = 'LANcom', 'LCOS sshd'
|
||||||
|
return cls(v, p, mx.group(1), None, None)
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (C) 2017-2020 Joe Testa (jtesta@positronsecurity.com)
|
Copyright (C) 2017-2024 Joe Testa (jtesta@positronsecurity.com)
|
||||||
Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu)
|
Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu)
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
@@ -132,3 +132,16 @@ class SSH2_Kex:
|
|||||||
srv = SSH2_KexParty(srv_enc, srv_mac, srv_compression, srv_languages)
|
srv = SSH2_KexParty(srv_enc, srv_mac, srv_compression, srv_languages)
|
||||||
kex = cls(outputbuffer, cookie, kex_algs, key_algs, cli, srv, follows, unused)
|
kex = cls(outputbuffer, cookie, kex_algs, key_algs, cli, srv, follows, unused)
|
||||||
return kex
|
return kex
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
ret = "----\nSSH2_Kex object:"
|
||||||
|
ret += "\nHost keys: "
|
||||||
|
ret += ", ".join(self.__key_algs)
|
||||||
|
ret += "\nKey exchanges: "
|
||||||
|
ret += ", ".join(self.__kex_algs)
|
||||||
|
ret += "\nClient SSH2_KexParty:"
|
||||||
|
ret += "\n" + str(self.__client)
|
||||||
|
ret += "\nServer SSH2_KexParty:"
|
||||||
|
ret += "\n" + str(self.__server)
|
||||||
|
ret += "\n----"
|
||||||
|
return ret
|
||||||
|
|||||||
@@ -64,11 +64,13 @@ class SSH2_KexDB: # pylint: disable=too-few-public-methods
|
|||||||
INFO_DEFAULT_OPENSSH_CIPHER = 'default cipher since OpenSSH 6.9'
|
INFO_DEFAULT_OPENSSH_CIPHER = 'default cipher since OpenSSH 6.9'
|
||||||
INFO_DEFAULT_OPENSSH_KEX_65_TO_73 = 'default key exchange from OpenSSH 6.5 to 7.3'
|
INFO_DEFAULT_OPENSSH_KEX_65_TO_73 = 'default key exchange from OpenSSH 6.5 to 7.3'
|
||||||
INFO_DEFAULT_OPENSSH_KEX_74_TO_89 = 'default key exchange from OpenSSH 7.4 to 8.9'
|
INFO_DEFAULT_OPENSSH_KEX_74_TO_89 = 'default key exchange from OpenSSH 7.4 to 8.9'
|
||||||
INFO_DEFAULT_OPENSSH_KEX_90 = 'default key exchange since OpenSSH 9.0'
|
INFO_DEFAULT_OPENSSH_KEX_90_TO_98 = 'default key exchange from OpenSSH 9.0 to 9.8'
|
||||||
|
INFO_DEFAULT_OPENSSH_KEX_99 = 'default key exchange since OpenSSH 9.9'
|
||||||
INFO_DEPRECATED_IN_OPENSSH88 = 'deprecated in OpenSSH 8.8: https://www.openssh.com/txt/release-8.8'
|
INFO_DEPRECATED_IN_OPENSSH88 = 'deprecated in OpenSSH 8.8: https://www.openssh.com/txt/release-8.8'
|
||||||
INFO_DISABLED_IN_DBEAR67 = 'disabled in Dropbear SSH 2015.67'
|
INFO_DISABLED_IN_DBEAR67 = 'disabled in Dropbear SSH 2015.67'
|
||||||
INFO_DISABLED_IN_OPENSSH70 = 'disabled in OpenSSH 7.0: https://www.openssh.com/txt/release-7.0'
|
INFO_DISABLED_IN_OPENSSH70 = 'disabled in OpenSSH 7.0: https://www.openssh.com/txt/release-7.0'
|
||||||
INFO_NEVER_IMPLEMENTED_IN_OPENSSH = 'despite the @openssh.com tag, this was never implemented in OpenSSH'
|
INFO_NEVER_IMPLEMENTED_IN_OPENSSH = 'despite the @openssh.com tag, this was never implemented in OpenSSH'
|
||||||
|
INFO_HYBRID_PQ_X25519_KEX = 'hybrid key exchange based on post-quantum resistant algorithm and proven conventional X25519 algorithm'
|
||||||
INFO_REMOVED_IN_OPENSSH61 = 'removed since OpenSSH 6.1, removed from specification'
|
INFO_REMOVED_IN_OPENSSH61 = 'removed since OpenSSH 6.1, removed from specification'
|
||||||
INFO_REMOVED_IN_OPENSSH69 = 'removed in OpenSSH 6.9: https://www.openssh.com/txt/release-6.9'
|
INFO_REMOVED_IN_OPENSSH69 = 'removed in OpenSSH 6.9: https://www.openssh.com/txt/release-6.9'
|
||||||
INFO_REMOVED_IN_OPENSSH70 = 'removed in OpenSSH 7.0: https://www.openssh.com/txt/release-7.0'
|
INFO_REMOVED_IN_OPENSSH70 = 'removed in OpenSSH 7.0: https://www.openssh.com/txt/release-7.0'
|
||||||
@@ -189,11 +191,13 @@ class SSH2_KexDB: # pylint: disable=too-few-public-methods
|
|||||||
'kexguess2@matt.ucc.asn.au': [['d2013.57']],
|
'kexguess2@matt.ucc.asn.au': [['d2013.57']],
|
||||||
'm383-sha384@libassh.org': [[], [FAIL_UNPROVEN]],
|
'm383-sha384@libassh.org': [[], [FAIL_UNPROVEN]],
|
||||||
'm511-sha512@libassh.org': [[], [FAIL_UNPROVEN]],
|
'm511-sha512@libassh.org': [[], [FAIL_UNPROVEN]],
|
||||||
|
'mlkem768x25519-sha256': [['9.9'], [], [], [INFO_HYBRID_PQ_X25519_KEX]],
|
||||||
'rsa1024-sha1': [[], [FAIL_1024BIT_MODULUS, FAIL_SHA1]],
|
'rsa1024-sha1': [[], [FAIL_1024BIT_MODULUS, FAIL_SHA1]],
|
||||||
'rsa2048-sha256': [[], [], [WARN_2048BIT_MODULUS]],
|
'rsa2048-sha256': [[], [], [WARN_2048BIT_MODULUS]],
|
||||||
'sm2kep-sha2-nistp256': [[], [FAIL_NSA_BACKDOORED_CURVE, FAIL_UNTRUSTED]],
|
'sm2kep-sha2-nistp256': [[], [FAIL_NSA_BACKDOORED_CURVE, FAIL_UNTRUSTED]],
|
||||||
'sntrup4591761x25519-sha512@tinyssh.org': [['8.0', '8.4'], [], [WARN_EXPERIMENTAL], [INFO_WITHDRAWN_PQ_ALG]],
|
'sntrup4591761x25519-sha512@tinyssh.org': [['8.0', '8.4'], [], [WARN_EXPERIMENTAL], [INFO_WITHDRAWN_PQ_ALG]],
|
||||||
'sntrup761x25519-sha512@openssh.com': [['8.5'], [], [], [INFO_DEFAULT_OPENSSH_KEX_90]],
|
'sntrup761x25519-sha512': [['9.9'], [], [], [INFO_DEFAULT_OPENSSH_KEX_99, INFO_HYBRID_PQ_X25519_KEX]],
|
||||||
|
'sntrup761x25519-sha512@openssh.com': [['8.5'], [], [], [INFO_DEFAULT_OPENSSH_KEX_90_TO_98, INFO_HYBRID_PQ_X25519_KEX]],
|
||||||
'x25519-kyber-512r3-sha256-d00@amazon.com': [[]],
|
'x25519-kyber-512r3-sha256-d00@amazon.com': [[]],
|
||||||
'x25519-kyber512-sha512@aws.amazon.com': [[]],
|
'x25519-kyber512-sha512@aws.amazon.com': [[]],
|
||||||
},
|
},
|
||||||
@@ -346,6 +350,7 @@ class SSH2_KexDB: # pylint: disable=too-few-public-methods
|
|||||||
'des-cbc-ssh1': [[], [FAIL_DES], [WARN_CIPHER_MODE, WARN_BLOCK_SIZE]],
|
'des-cbc-ssh1': [[], [FAIL_DES], [WARN_CIPHER_MODE, WARN_BLOCK_SIZE]],
|
||||||
'des-cbc@ssh.com': [[], [FAIL_DES], [WARN_CIPHER_MODE, WARN_BLOCK_SIZE]],
|
'des-cbc@ssh.com': [[], [FAIL_DES], [WARN_CIPHER_MODE, WARN_BLOCK_SIZE]],
|
||||||
'des': [[], [FAIL_DES], [WARN_CIPHER_MODE, WARN_BLOCK_SIZE]],
|
'des': [[], [FAIL_DES], [WARN_CIPHER_MODE, WARN_BLOCK_SIZE]],
|
||||||
|
'grasshopper-ctr128': [[], [FAIL_UNTRUSTED]],
|
||||||
'idea-cbc': [[], [FAIL_IDEA], [WARN_CIPHER_MODE]],
|
'idea-cbc': [[], [FAIL_IDEA], [WARN_CIPHER_MODE]],
|
||||||
'idea-cfb': [[], [FAIL_IDEA], [WARN_CIPHER_MODE]],
|
'idea-cfb': [[], [FAIL_IDEA], [WARN_CIPHER_MODE]],
|
||||||
'idea-ctr': [[], [FAIL_IDEA]],
|
'idea-ctr': [[], [FAIL_IDEA]],
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (C) 2024 Joe Testa (jtesta@positronsecurity.com)
|
||||||
Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu)
|
Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu)
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
@@ -48,3 +49,10 @@ class SSH2_KexParty:
|
|||||||
@property
|
@property
|
||||||
def languages(self) -> List[str]:
|
def languages(self) -> List[str]:
|
||||||
return self.__languages
|
return self.__languages
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
ret = "Ciphers: " + ", ".join(self.__enc)
|
||||||
|
ret += "\nMACs: " + ", ".join(self.__mac)
|
||||||
|
ret += "\nCompressions: " + ", ".join(self.__compression)
|
||||||
|
ret += "\nLanguages: " + ", ".join(self.__languages)
|
||||||
|
return ret
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"""
|
"""
|
||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (C) 2017-2023 Joe Testa (jtesta@positronsecurity.com)
|
Copyright (C) 2017-2024 Joe Testa (jtesta@positronsecurity.com)
|
||||||
Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu)
|
Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu)
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
"""
|
"""
|
||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
import copy
|
import copy
|
||||||
import getopt
|
import getopt # pylint: disable=deprecated-module
|
||||||
import json
|
import json
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
import os
|
import os
|
||||||
@@ -130,7 +130,7 @@ def usage(uout: OutputBuffer, err: Optional[str] = None) -> None:
|
|||||||
uout.info(' -P, --policy=<policy.txt> run a policy test using the specified policy')
|
uout.info(' -P, --policy=<policy.txt> run a policy test using the specified policy')
|
||||||
uout.info(' --skip-rate-test skip the connection rate test during standard audits\n (used to safely infer whether the DHEat attack\n is viable)')
|
uout.info(' --skip-rate-test skip the connection rate test during standard audits\n (used to safely infer whether the DHEat attack\n is viable)')
|
||||||
uout.info(' -t, --timeout=<secs> timeout (in seconds) for connection and reading\n (default: 5)')
|
uout.info(' -t, --timeout=<secs> timeout (in seconds) for connection and reading\n (default: 5)')
|
||||||
uout.info(' -T, --targets=<hosts.txt> a file containing a list of target hosts (one\n per line, format HOST[:PORT]). Use --threads\n to control concurrent scans.')
|
uout.info(' -T, --targets=<hosts.txt> a file containing a list of target hosts (one\n per line, format HOST[:PORT]). Use -p/--port\n to set the default port for all hosts. Use\n --threads to control concurrent scans.')
|
||||||
uout.info(' --threads=<threads> number of threads to use when scanning multiple\n targets (-T/--targets) (default: 32)')
|
uout.info(' --threads=<threads> number of threads to use when scanning multiple\n targets (-T/--targets) (default: 32)')
|
||||||
uout.info(' -v, --verbose verbose output')
|
uout.info(' -v, --verbose verbose output')
|
||||||
uout.sep()
|
uout.sep()
|
||||||
@@ -360,11 +360,19 @@ def output_fingerprints(out: OutputBuffer, algs: Algorithms, is_json_output: boo
|
|||||||
fp_types = sorted(fps.keys())
|
fp_types = sorted(fps.keys())
|
||||||
for fp_type in fp_types:
|
for fp_type in fp_types:
|
||||||
fp = fps[fp_type]
|
fp = fps[fp_type]
|
||||||
out.good('(fin) {}: {}'.format(fp_type, fp.sha256))
|
|
||||||
|
# Don't output any ECDSA or DSS fingerprints unless verbose mode is enabled.
|
||||||
|
if fp_type.startswith("ecdsa-") or (fp_type == "ssh-dss"):
|
||||||
|
if out.verbose:
|
||||||
|
out.warn('(fin) {}: {} -- [info] this fingerprint type is insecure and should not be relied upon'.format(fp_type, fp.sha256))
|
||||||
|
else:
|
||||||
|
continue # If verbose mode is not enabled, skip this type entirely.
|
||||||
|
else:
|
||||||
|
out.good('(fin) {}: {}'.format(fp_type, fp.sha256))
|
||||||
|
|
||||||
# Output the MD5 hash too if verbose mode is enabled.
|
# Output the MD5 hash too if verbose mode is enabled.
|
||||||
if out.verbose:
|
if out.verbose:
|
||||||
out.info('(fin) {}: {} -- [info] do not rely on MD5 fingerprints for server identification; it is insecure for this use case'.format(fp_type, fp.md5))
|
out.warn('(fin) {}: {} -- [info] do not rely on MD5 fingerprints for server identification; it is insecure for this use case'.format(fp_type, fp.md5))
|
||||||
|
|
||||||
if not out.is_section_empty() and not is_json_output:
|
if not out.is_section_empty() and not is_json_output:
|
||||||
out.head('# fingerprints')
|
out.head('# fingerprints')
|
||||||
@@ -421,6 +429,8 @@ def output_recommendations(out: OutputBuffer, algs: Algorithms, algorithm_recomm
|
|||||||
|
|
||||||
fn = level_to_output[level]
|
fn = level_to_output[level]
|
||||||
|
|
||||||
|
an = '?'
|
||||||
|
sg = '?'
|
||||||
if action == 'del':
|
if action == 'del':
|
||||||
an, sg = 'remove', '-'
|
an, sg = 'remove', '-'
|
||||||
ret = False
|
ret = False
|
||||||
@@ -721,7 +731,7 @@ def output(out: OutputBuffer, aconf: AuditConf, banner: Optional[Banner], header
|
|||||||
# Build & write the JSON struct.
|
# Build & write the JSON struct.
|
||||||
out.info(json.dumps(build_struct(aconf.host + ":" + str(aconf.port), banner, cves, kex=kex, client_host=client_host, software=software, algorithms=algs, algorithm_recommendation_suppress_list=algorithm_recommendation_suppress_list, additional_notes=additional_notes), indent=4 if aconf.json_print_indent else None, sort_keys=True))
|
out.info(json.dumps(build_struct(aconf.host + ":" + str(aconf.port), banner, cves, kex=kex, client_host=client_host, software=software, algorithms=algs, algorithm_recommendation_suppress_list=algorithm_recommendation_suppress_list, additional_notes=additional_notes), indent=4 if aconf.json_print_indent else None, sort_keys=True))
|
||||||
elif len(unknown_algorithms) > 0: # If we encountered any unknown algorithms, ask the user to report them.
|
elif len(unknown_algorithms) > 0: # If we encountered any unknown algorithms, ask the user to report them.
|
||||||
out.warn("\n\n!!! WARNING: unknown algorithm(s) found!: %s. Please email the full output above to the maintainer (jtesta@positronsecurity.com), or create a Github issue at <https://github.com/jtesta/ssh-audit/issues>.\n" % ','.join(unknown_algorithms))
|
out.warn("\n\n!!! WARNING: unknown algorithm(s) found!: %s. If this is the latest version of ssh-audit (see <https://github.com/jtesta/ssh-audit/releases>), please create a new Github issue at <https://github.com/jtesta/ssh-audit/issues> with the full output above.\n" % ','.join(unknown_algorithms))
|
||||||
|
|
||||||
return program_retval
|
return program_retval
|
||||||
|
|
||||||
@@ -733,7 +743,7 @@ def evaluate_policy(out: OutputBuffer, aconf: AuditConf, banner: Optional['Banne
|
|||||||
|
|
||||||
passed, error_struct, error_str = aconf.policy.evaluate(banner, kex)
|
passed, error_struct, error_str = aconf.policy.evaluate(banner, kex)
|
||||||
if aconf.json:
|
if aconf.json:
|
||||||
json_struct = {'host': aconf.host, 'policy': aconf.policy.get_name_and_version(), 'passed': passed, 'errors': error_struct}
|
json_struct = {'host': aconf.host, 'port': aconf.port, 'policy': aconf.policy.get_name_and_version(), 'passed': passed, 'errors': error_struct}
|
||||||
out.info(json.dumps(json_struct, indent=4 if aconf.json_print_indent else None, sort_keys=True))
|
out.info(json.dumps(json_struct, indent=4 if aconf.json_print_indent else None, sort_keys=True))
|
||||||
else:
|
else:
|
||||||
spacing = ''
|
spacing = ''
|
||||||
@@ -1315,6 +1325,7 @@ def audit(out: OutputBuffer, aconf: AuditConf, sshv: Optional[int] = None, print
|
|||||||
elif sshv == 2:
|
elif sshv == 2:
|
||||||
try:
|
try:
|
||||||
kex = SSH2_Kex.parse(out, payload)
|
kex = SSH2_Kex.parse(out, payload)
|
||||||
|
out.d(str(kex))
|
||||||
except Exception:
|
except Exception:
|
||||||
out.fail("Failed to parse server's kex. Stack trace:\n%s" % str(traceback.format_exc()))
|
out.fail("Failed to parse server's kex. Stack trace:\n%s" % str(traceback.format_exc()))
|
||||||
return exitcodes.CONNECTION_ERROR
|
return exitcodes.CONNECTION_ERROR
|
||||||
@@ -1584,10 +1595,10 @@ def main() -> int:
|
|||||||
if aconf.json:
|
if aconf.json:
|
||||||
print('[', end='')
|
print('[', end='')
|
||||||
|
|
||||||
# Loop through each target in the list.
|
# Loop through each target in the list. Entries can specify a port number to use, otherwise the value provided on the command line (--port=N) will be used by default (set to 22 if --port is not used).
|
||||||
target_servers = []
|
target_servers = []
|
||||||
for _, target in enumerate(aconf.target_list):
|
for _, target in enumerate(aconf.target_list):
|
||||||
host, port = Utils.parse_host_and_port(target, default_port=22)
|
host, port = Utils.parse_host_and_port(target, default_port=aconf.port)
|
||||||
target_servers.append((host, port))
|
target_servers.append((host, port))
|
||||||
|
|
||||||
# A ranked list of return codes. Those with higher indices will take precedence over lower ones. For example, if three servers are scanned, yielding WARNING, GOOD, and UNKNOWN_ERROR, the overall result will be UNKNOWN_ERROR, since its index is the highest. Errors have highest priority, followed by failures, then warnings.
|
# A ranked list of return codes. Those with higher indices will take precedence over lower ones. For example, if three servers are scanned, yielding WARNING, GOOD, and UNKNOWN_ERROR, the overall result will be UNKNOWN_ERROR, since its index is the highest. Errors have highest priority, followed by failures, then warnings.
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ class SSH_Socket(ReadBuf, WriteBuf):
|
|||||||
s.listen()
|
s.listen()
|
||||||
self.__sock_map[s.fileno()] = s
|
self.__sock_map[s.fileno()] = s
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("Warning: failed to listen on any IPv4 interfaces: %s" % str(e))
|
print("Warning: failed to listen on any IPv4 interfaces: %s" % str(e), file=sys.stderr)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Socket to listen on all IPv6 addresses.
|
# Socket to listen on all IPv6 addresses.
|
||||||
@@ -119,11 +119,11 @@ class SSH_Socket(ReadBuf, WriteBuf):
|
|||||||
s.listen()
|
s.listen()
|
||||||
self.__sock_map[s.fileno()] = s
|
self.__sock_map[s.fileno()] = s
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("Warning: failed to listen on any IPv6 interfaces: %s" % str(e))
|
print("Warning: failed to listen on any IPv6 interfaces: %s" % str(e), file=sys.stderr)
|
||||||
|
|
||||||
# If we failed to listen on any interfaces, terminate.
|
# If we failed to listen on any interfaces, terminate.
|
||||||
if len(self.__sock_map.keys()) == 0:
|
if len(self.__sock_map.keys()) == 0:
|
||||||
print("Error: failed to listen on any IPv4 and IPv6 interfaces!")
|
print("Error: failed to listen on any IPv4 and IPv6 interfaces!", file=sys.stderr)
|
||||||
sys.exit(exitcodes.CONNECTION_ERROR)
|
sys.exit(exitcodes.CONNECTION_ERROR)
|
||||||
|
|
||||||
# Wait for an incoming connection. If a timeout was explicitly
|
# Wait for an incoming connection. If a timeout was explicitly
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
.TH SSH-AUDIT 1 "April 18, 2024"
|
.TH SSH-AUDIT 1 "September 24, 2024"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
\fBssh-audit\fP \- SSH server & client configuration auditor
|
\fBssh-audit\fP \- SSH server & client configuration auditor
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
@@ -149,7 +149,7 @@ The timeout, in seconds, for creating connections and reading data from the sock
|
|||||||
.TP
|
.TP
|
||||||
.B -T, \-\-targets=<hosts.txt>
|
.B -T, \-\-targets=<hosts.txt>
|
||||||
.br
|
.br
|
||||||
A file containing a list of target hosts. Each line must have one host, in the format of HOST[:PORT]. Use --threads to control concurrent scans.
|
A file containing a list of target hosts. Each line must have one host, in the format of HOST[:PORT]. Use -p/--port to set the default port for all hosts. Use --threads to control concurrent scans.
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.B \-\-threads=<threads>
|
.B \-\-threads=<threads>
|
||||||
|
|||||||
@@ -78,6 +78,26 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"fingerprints": [
|
"fingerprints": [
|
||||||
|
{
|
||||||
|
"hash": "jdUfqoGCDOY1drQcoqIJm/pEix2r09hqwOs9E9GimZQ",
|
||||||
|
"hash_alg": "SHA256",
|
||||||
|
"hostkey": "ecdsa-sha2-nistp256"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hash": "98:27:f3:12:20:f6:23:6d:1a:00:2a:6c:71:7c:1e:6b",
|
||||||
|
"hash_alg": "MD5",
|
||||||
|
"hostkey": "ecdsa-sha2-nistp256"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hash": "NBzry0uMAX8BRsn4mv9CHpeivMOdwzGFEKrf6Hg7tIQ",
|
||||||
|
"hash_alg": "SHA256",
|
||||||
|
"hostkey": "ssh-dss"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hash": "16:60:9e:54:d7:1e:b3:0d:97:60:12:ad:fe:83:a2:40",
|
||||||
|
"hash_alg": "MD5",
|
||||||
|
"hostkey": "ssh-dss"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"hash": "CDfAU12pjQS7/91kg7gYacza0U/6PDbE04Ic3IpYxkM",
|
"hash": "CDfAU12pjQS7/91kg7gYacza0U/6PDbE04Ic3IpYxkM",
|
||||||
"hash_alg": "SHA256",
|
"hash_alg": "SHA256",
|
||||||
|
|||||||
@@ -253,6 +253,16 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"fingerprints": [
|
"fingerprints": [
|
||||||
|
{
|
||||||
|
"hash": "sqDDYhzYz7YIQeFDc0WF8SeXtrEz+iwsV7d/FdIgztM",
|
||||||
|
"hash_alg": "SHA256",
|
||||||
|
"hostkey": "ssh-dss"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hash": "5c:de:62:f0:60:c8:93:13:87:71:78:95:56:3f:61:51",
|
||||||
|
"hash_alg": "MD5",
|
||||||
|
"hostkey": "ssh-dss"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"hash": "YZ457EBcJTSxRKI3yXRgtAj3PBf5B9/F36b1SVooml4",
|
"hash": "YZ457EBcJTSxRKI3yXRgtAj3PBf5B9/F36b1SVooml4",
|
||||||
"hash_alg": "SHA256",
|
"hash_alg": "SHA256",
|
||||||
|
|||||||
@@ -2,5 +2,6 @@
|
|||||||
"errors": [],
|
"errors": [],
|
||||||
"host": "localhost",
|
"host": "localhost",
|
||||||
"passed": true,
|
"passed": true,
|
||||||
"policy": "Docker policy: test1 (version 1)"
|
"policy": "Docker policy: test1 (version 1)",
|
||||||
|
"port": 2222
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,5 +27,6 @@
|
|||||||
],
|
],
|
||||||
"host": "localhost",
|
"host": "localhost",
|
||||||
"passed": false,
|
"passed": false,
|
||||||
"policy": "Docker poliicy: test10 (version 1)"
|
"policy": "Docker poliicy: test10 (version 1)",
|
||||||
|
"port": 2222
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,5 +19,6 @@
|
|||||||
],
|
],
|
||||||
"host": "localhost",
|
"host": "localhost",
|
||||||
"passed": false,
|
"passed": false,
|
||||||
"policy": "Docker policy: test2 (version 1)"
|
"policy": "Docker policy: test2 (version 1)",
|
||||||
|
"port": 2222
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,5 +18,6 @@
|
|||||||
],
|
],
|
||||||
"host": "localhost",
|
"host": "localhost",
|
||||||
"passed": false,
|
"passed": false,
|
||||||
"policy": "Docker policy: test3 (version 1)"
|
"policy": "Docker policy: test3 (version 1)",
|
||||||
|
"port": 2222
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,5 +28,6 @@
|
|||||||
],
|
],
|
||||||
"host": "localhost",
|
"host": "localhost",
|
||||||
"passed": false,
|
"passed": false,
|
||||||
"policy": "Docker policy: test4 (version 1)"
|
"policy": "Docker policy: test4 (version 1)",
|
||||||
|
"port": 2222
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,5 +27,6 @@
|
|||||||
],
|
],
|
||||||
"host": "localhost",
|
"host": "localhost",
|
||||||
"passed": false,
|
"passed": false,
|
||||||
"policy": "Docker policy: test5 (version 1)"
|
"policy": "Docker policy: test5 (version 1)",
|
||||||
|
"port": 2222
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,6 @@
|
|||||||
"errors": [],
|
"errors": [],
|
||||||
"host": "localhost",
|
"host": "localhost",
|
||||||
"passed": true,
|
"passed": true,
|
||||||
"policy": "Docker poliicy: test7 (version 1)"
|
"policy": "Docker poliicy: test7 (version 1)",
|
||||||
|
"port": 2222
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,5 +15,6 @@
|
|||||||
],
|
],
|
||||||
"host": "localhost",
|
"host": "localhost",
|
||||||
"passed": false,
|
"passed": false,
|
||||||
"policy": "Docker poliicy: test8 (version 1)"
|
"policy": "Docker poliicy: test8 (version 1)",
|
||||||
|
"port": 2222
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,5 +15,6 @@
|
|||||||
],
|
],
|
||||||
"host": "localhost",
|
"host": "localhost",
|
||||||
"passed": false,
|
"passed": false,
|
||||||
"policy": "Docker poliicy: test9 (version 1)"
|
"policy": "Docker poliicy: test9 (version 1)",
|
||||||
|
"port": 2222
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -240,6 +240,16 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"fingerprints": [
|
"fingerprints": [
|
||||||
|
{
|
||||||
|
"hash": "sqDDYhzYz7YIQeFDc0WF8SeXtrEz+iwsV7d/FdIgztM",
|
||||||
|
"hash_alg": "SHA256",
|
||||||
|
"hostkey": "ssh-dss"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hash": "5c:de:62:f0:60:c8:93:13:87:71:78:95:56:3f:61:51",
|
||||||
|
"hash_alg": "MD5",
|
||||||
|
"hostkey": "ssh-dss"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"hash": "YZ457EBcJTSxRKI3yXRgtAj3PBf5B9/F36b1SVooml4",
|
"hash": "YZ457EBcJTSxRKI3yXRgtAj3PBf5B9/F36b1SVooml4",
|
||||||
"hash_alg": "SHA256",
|
"hash_alg": "SHA256",
|
||||||
|
|||||||
@@ -39,5 +39,6 @@
|
|||||||
],
|
],
|
||||||
"host": "localhost",
|
"host": "localhost",
|
||||||
"passed": false,
|
"passed": false,
|
||||||
"policy": "Hardened OpenSSH Server v8.0 (version 4)"
|
"policy": "Hardened OpenSSH Server v8.0 (version 4)",
|
||||||
|
"port": 2222
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,5 +62,6 @@
|
|||||||
],
|
],
|
||||||
"host": "localhost",
|
"host": "localhost",
|
||||||
"passed": false,
|
"passed": false,
|
||||||
"policy": "Hardened OpenSSH Server v8.0 (version 4)"
|
"policy": "Hardened OpenSSH Server v8.0 (version 4)",
|
||||||
|
"port": 2222
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,6 @@
|
|||||||
"errors": [],
|
"errors": [],
|
||||||
"host": "localhost",
|
"host": "localhost",
|
||||||
"passed": true,
|
"passed": true,
|
||||||
"policy": "Docker policy: test11 (version 1)"
|
"policy": "Docker policy: test11 (version 1)",
|
||||||
|
"port": 2222
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,5 +39,6 @@
|
|||||||
],
|
],
|
||||||
"host": "localhost",
|
"host": "localhost",
|
||||||
"passed": false,
|
"passed": false,
|
||||||
"policy": "Docker policy: test12 (version 1)"
|
"policy": "Docker policy: test12 (version 1)",
|
||||||
|
"port": 2222
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,6 @@
|
|||||||
"errors": [],
|
"errors": [],
|
||||||
"host": "localhost",
|
"host": "localhost",
|
||||||
"passed": true,
|
"passed": true,
|
||||||
"policy": "Docker policy: test13 (version 1)"
|
"policy": "Docker policy: test13 (version 1)",
|
||||||
|
"port": 2222
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,5 +15,6 @@
|
|||||||
],
|
],
|
||||||
"host": "localhost",
|
"host": "localhost",
|
||||||
"passed": false,
|
"passed": false,
|
||||||
"policy": "Docker policy: test14 (version 1)"
|
"policy": "Docker policy: test14 (version 1)",
|
||||||
|
"port": 2222
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,6 @@
|
|||||||
"errors": [],
|
"errors": [],
|
||||||
"host": "localhost",
|
"host": "localhost",
|
||||||
"passed": true,
|
"passed": true,
|
||||||
"policy": "Docker policy: test15 (version 1)"
|
"policy": "Docker policy: test15 (version 1)",
|
||||||
|
"port": 2222
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,5 +82,6 @@
|
|||||||
],
|
],
|
||||||
"host": "localhost",
|
"host": "localhost",
|
||||||
"passed": false,
|
"passed": false,
|
||||||
"policy": "Docker policy: test16 (version 1)"
|
"policy": "Docker policy: test16 (version 1)",
|
||||||
|
"port": 2222
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,6 @@
|
|||||||
"errors": [],
|
"errors": [],
|
||||||
"host": "localhost",
|
"host": "localhost",
|
||||||
"passed": true,
|
"passed": true,
|
||||||
"policy": "Docker policy: test17 (version 1)"
|
"policy": "Docker policy: test17 (version 1)",
|
||||||
|
"port": 2222
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,6 @@
|
|||||||
"errors": [],
|
"errors": [],
|
||||||
"host": "localhost",
|
"host": "localhost",
|
||||||
"passed": true,
|
"passed": true,
|
||||||
"policy": "Docker policy: test6 (version 1)"
|
"policy": "Docker policy: test6 (version 1)",
|
||||||
|
"port": 2222
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,6 +87,16 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"fingerprints": [
|
"fingerprints": [
|
||||||
|
{
|
||||||
|
"hash": "Q6Llm0o4TrcUen4tnT2h4BDf2f+ina6dIJmVH8c40bg",
|
||||||
|
"hash_alg": "SHA256",
|
||||||
|
"hostkey": "ecdsa-sha2-nistp256"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hash": "cc:e0:80:84:5b:05:98:64:24:43:52:3b:17:c8:94:89",
|
||||||
|
"hash_alg": "MD5",
|
||||||
|
"hostkey": "ecdsa-sha2-nistp256"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"hash": "UrnXIVH+7dlw8UqYocl48yUEcKrthGDQG2CPCgp7MxU",
|
"hash": "UrnXIVH+7dlw8UqYocl48yUEcKrthGDQG2CPCgp7MxU",
|
||||||
"hash_alg": "SHA256",
|
"hash_alg": "SHA256",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[tox]
|
[tox]
|
||||||
envlist =
|
envlist =
|
||||||
py{py3}-{test,pylint,flake8}
|
py{py3}-{test,pylint,flake8}
|
||||||
py{38,39,310,311,312}-{test,mypy,pylint,flake8}
|
py{38,39,310,311,312,313}-{test,mypy,pylint,flake8}
|
||||||
cov
|
cov
|
||||||
skip_missing_interpreters = true
|
skip_missing_interpreters = true
|
||||||
|
|
||||||
@@ -9,10 +9,10 @@ skip_missing_interpreters = true
|
|||||||
deps =
|
deps =
|
||||||
test: pytest
|
test: pytest
|
||||||
test,cov: {[testenv:cov]deps}
|
test,cov: {[testenv:cov]deps}
|
||||||
test,py{38,39,310,311,312}-{type,mypy}: colorama
|
test,py{38,39,310,311,312,313}-{type,mypy}: colorama
|
||||||
py{38,39,310,311,312}-{type,mypy}: {[testenv:mypy]deps}
|
py{38,39,310,311,312,313}-{type,mypy}: {[testenv:mypy]deps}
|
||||||
py{py3,38,39,310,311,312}-{lint,pylint},lint: {[testenv:pylint]deps}
|
py{py3,38,39,310,311,312,313}-{lint,pylint},lint: {[testenv:pylint]deps}
|
||||||
py{py3,38,39,310,311,312}-{lint,flake8},lint: {[testenv:flake8]deps}
|
py{py3,38,39,310,311,312,313}-{lint,flake8},lint: {[testenv:flake8]deps}
|
||||||
setenv =
|
setenv =
|
||||||
SSHAUDIT = {toxinidir}/src
|
SSHAUDIT = {toxinidir}/src
|
||||||
test: COVERAGE_FILE = {toxinidir}/.coverage.{envname}
|
test: COVERAGE_FILE = {toxinidir}/.coverage.{envname}
|
||||||
@@ -24,9 +24,9 @@ commands =
|
|||||||
test: coverage combine
|
test: coverage combine
|
||||||
test: coverage report --show-missing
|
test: coverage report --show-missing
|
||||||
test: coverage html -d {toxinidir}/reports/html/coverage.{envname}
|
test: coverage html -d {toxinidir}/reports/html/coverage.{envname}
|
||||||
py{38,39,310,311,312}-{type,mypy}: {[testenv:mypy]commands}
|
py{38,39,310,311,312,313}-{type,mypy}: {[testenv:mypy]commands}
|
||||||
py{py3,38,39,310,311,312}-{lint,pylint},lint: {[testenv:pylint]commands}
|
py{py3,38,39,310,311,312,313}-{lint,pylint},lint: {[testenv:pylint]commands}
|
||||||
py{py3,38,39,310,311,312}-{lint,flake8},lint: {[testenv:flake8]commands}
|
py{py3,38,39,310,311,312,313}-{lint,flake8},lint: {[testenv:flake8]commands}
|
||||||
|
|
||||||
#ignore_outcome =
|
#ignore_outcome =
|
||||||
# type: true
|
# type: true
|
||||||
@@ -96,6 +96,7 @@ disable =
|
|||||||
too-many-lines,
|
too-many-lines,
|
||||||
too-many-locals,
|
too-many-locals,
|
||||||
too-many-nested-blocks,
|
too-many-nested-blocks,
|
||||||
|
too-many-positional-arguments,
|
||||||
too-many-return-statements,
|
too-many-return-statements,
|
||||||
too-many-statements,
|
too-many-statements,
|
||||||
consider-using-f-string
|
consider-using-f-string
|
||||||
|
|||||||
Reference in New Issue
Block a user