├── .dockerignore ├── .github ├── FUNDING.yml └── workflows │ └── tox.yaml ├── .gitignore ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── Makefile.docker ├── Makefile.pypi ├── PACKAGING.md ├── README.md ├── add_builtin_man_page.sh ├── build_snap.sh ├── build_windows_executable.sh ├── docker_test.sh ├── pyproject.toml ├── setup.cfg ├── setup.py ├── snapcraft.yaml ├── src └── ssh_audit │ ├── __init__.py │ ├── __main__.py │ ├── algorithm.py │ ├── algorithms.py │ ├── auditconf.py │ ├── banner.py │ ├── builtin_policies.py │ ├── dheat.py │ ├── exitcodes.py │ ├── fingerprint.py │ ├── gextest.py │ ├── globals.py │ ├── hostkeytest.py │ ├── kexdh.py │ ├── outputbuffer.py │ ├── policy.py │ ├── product.py │ ├── protocol.py │ ├── readbuf.py │ ├── software.py │ ├── ssh1.py │ ├── ssh1_crc32.py │ ├── ssh1_kexdb.py │ ├── ssh1_publickeymessage.py │ ├── ssh2_kex.py │ ├── ssh2_kexdb.py │ ├── ssh2_kexparty.py │ ├── ssh_audit.py │ ├── ssh_socket.py │ ├── timeframe.py │ ├── utils.py │ └── writebuf.py ├── ssh-audit.1 ├── ssh-audit.py ├── test ├── conftest.py ├── docker │ ├── .ed25519.sk │ ├── Dockerfile │ ├── debug.sh │ ├── dropbear_dss_host_key │ ├── dropbear_ecdsa_host_key │ ├── dropbear_rsa_host_key_1024 │ ├── dropbear_rsa_host_key_3072 │ ├── ed25519.pk │ ├── expected_results │ │ ├── dropbear_2019.78_test1.json │ │ ├── dropbear_2019.78_test1.txt │ │ ├── openssh_4.0p1_test1.json │ │ ├── openssh_4.0p1_test1.txt │ │ ├── openssh_5.6p1_custom_policy_test1.json │ │ ├── openssh_5.6p1_custom_policy_test1.txt │ │ ├── openssh_5.6p1_custom_policy_test10.json │ │ ├── openssh_5.6p1_custom_policy_test10.txt │ │ ├── openssh_5.6p1_custom_policy_test2.json │ │ ├── openssh_5.6p1_custom_policy_test2.txt │ │ ├── openssh_5.6p1_custom_policy_test3.json │ │ ├── openssh_5.6p1_custom_policy_test3.txt │ │ ├── openssh_5.6p1_custom_policy_test4.json │ │ ├── openssh_5.6p1_custom_policy_test4.txt │ │ ├── openssh_5.6p1_custom_policy_test5.json │ │ ├── openssh_5.6p1_custom_policy_test5.txt │ │ ├── openssh_5.6p1_custom_policy_test7.json │ │ ├── openssh_5.6p1_custom_policy_test7.txt │ │ ├── openssh_5.6p1_custom_policy_test8.json │ │ ├── openssh_5.6p1_custom_policy_test8.txt │ │ ├── openssh_5.6p1_custom_policy_test9.json │ │ ├── openssh_5.6p1_custom_policy_test9.txt │ │ ├── openssh_5.6p1_test1.json │ │ ├── openssh_5.6p1_test1.txt │ │ ├── openssh_5.6p1_test2.json │ │ ├── openssh_5.6p1_test2.txt │ │ ├── openssh_5.6p1_test3.json │ │ ├── openssh_5.6p1_test3.txt │ │ ├── openssh_5.6p1_test4.json │ │ ├── openssh_5.6p1_test4.txt │ │ ├── openssh_5.6p1_test5.json │ │ ├── openssh_5.6p1_test5.txt │ │ ├── openssh_8.0p1_builtin_policy_test1.json │ │ ├── openssh_8.0p1_builtin_policy_test1.txt │ │ ├── openssh_8.0p1_builtin_policy_test2.json │ │ ├── openssh_8.0p1_builtin_policy_test2.txt │ │ ├── openssh_8.0p1_custom_policy_test11.json │ │ ├── openssh_8.0p1_custom_policy_test11.txt │ │ ├── openssh_8.0p1_custom_policy_test12.json │ │ ├── openssh_8.0p1_custom_policy_test12.txt │ │ ├── openssh_8.0p1_custom_policy_test13.json │ │ ├── openssh_8.0p1_custom_policy_test13.txt │ │ ├── openssh_8.0p1_custom_policy_test14.json │ │ ├── openssh_8.0p1_custom_policy_test14.txt │ │ ├── openssh_8.0p1_custom_policy_test15.json │ │ ├── openssh_8.0p1_custom_policy_test15.txt │ │ ├── openssh_8.0p1_custom_policy_test16.json │ │ ├── openssh_8.0p1_custom_policy_test16.txt │ │ ├── openssh_8.0p1_custom_policy_test17.json │ │ ├── openssh_8.0p1_custom_policy_test17.txt │ │ ├── openssh_8.0p1_custom_policy_test6.json │ │ ├── openssh_8.0p1_custom_policy_test6.txt │ │ ├── openssh_8.0p1_test1.json │ │ ├── openssh_8.0p1_test1.txt │ │ ├── openssh_8.0p1_test2.json │ │ ├── openssh_8.0p1_test2.txt │ │ ├── openssh_8.0p1_test3.json │ │ ├── openssh_8.0p1_test3.txt │ │ ├── tinyssh_20190101_test1.json │ │ └── tinyssh_20190101_test1.txt │ ├── host_ca_ed25519 │ ├── host_ca_ed25519.pub │ ├── host_ca_rsa_1024 │ ├── host_ca_rsa_1024.pub │ ├── host_ca_rsa_3072 │ ├── host_ca_rsa_3072.pub │ ├── moduli_1024 │ ├── policies │ │ ├── policy_test1.txt │ │ ├── policy_test10.txt │ │ ├── policy_test11.txt │ │ ├── policy_test12.txt │ │ ├── policy_test13.txt │ │ ├── policy_test14.txt │ │ ├── policy_test15.txt │ │ ├── policy_test16.txt │ │ ├── policy_test17.txt │ │ ├── policy_test2.txt │ │ ├── policy_test3.txt │ │ ├── policy_test4.txt │ │ ├── policy_test5.txt │ │ ├── policy_test6.txt │ │ ├── policy_test7.txt │ │ ├── policy_test8.txt │ │ └── policy_test9.txt │ ├── ssh1_host_key │ ├── ssh1_host_key.pub │ ├── ssh_host_dsa_key │ ├── ssh_host_dsa_key.pub │ ├── ssh_host_ecdsa_key │ ├── ssh_host_ecdsa_key.pub │ ├── ssh_host_ed25519_key │ ├── ssh_host_ed25519_key-cert.pub │ ├── ssh_host_ed25519_key.pub │ ├── ssh_host_rsa_key_1024 │ ├── ssh_host_rsa_key_1024-cert_1024.pub │ ├── ssh_host_rsa_key_1024-cert_3072.pub │ ├── ssh_host_rsa_key_1024.pub │ ├── ssh_host_rsa_key_3072 │ ├── ssh_host_rsa_key_3072-cert_1024.pub │ ├── ssh_host_rsa_key_3072-cert_3072.pub │ └── ssh_host_rsa_key_3072.pub ├── stubs │ └── colorama.pyi ├── test_algorithm.py ├── test_auditconf.py ├── test_banner.py ├── test_buffer.py ├── test_build_struct.py ├── test_dheater.py ├── test_errors.py ├── test_outputbuffer.py ├── test_policy.py ├── test_resolve.py ├── test_socket.py ├── test_software.py ├── test_ssh1.py ├── test_ssh2.py ├── test_ssh2_kexdb.py ├── test_utils.py ├── test_version_compare.py └── tools │ └── ci-win.cmd ├── tox.ini └── windows_icon.ico /.dockerignore: -------------------------------------------------------------------------------- 1 | src/ssh_audit/__pycache__/ 2 | src/ssh_audit.egg-info/ 3 | src/ssh_audit/*~ 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: jtesta 2 | -------------------------------------------------------------------------------- /.github/workflows/tox.yaml: -------------------------------------------------------------------------------- 1 | name: ssh-audit 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | build: 6 | 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Set up Python ${{ matrix.python-version }} 15 | uses: actions/setup-python@v5 16 | with: 17 | python-version: ${{ matrix.python-version }} 18 | - name: Install dependencies 19 | run: | 20 | python3 -m pip install --upgrade pip 21 | python3 -m pip install -U codecov coveralls flake8 mypy pylint pytest tox 22 | - name: Run Tox 23 | run: | 24 | python3 -m tox 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.py[cod] 3 | *.exe 4 | *.asc 5 | venv*/ 6 | .cache/ 7 | .mypy_cache/ 8 | .tox 9 | .coverage* 10 | reports/ 11 | .scannerwork/ 12 | 13 | # PyPI packaging 14 | /build/ 15 | /dist/ 16 | *.egg-info/ 17 | *.egg 18 | 19 | # Snap packaging 20 | /parts/ 21 | /prime/ 22 | /snap/ 23 | /stage/ 24 | /ssh-audit_*.snap 25 | 26 | # Your local server config 27 | servers.txt 28 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to ssh-audit 2 | 3 | We are very much open to receiving patches from the community! To encourage participation, passing CI tests, unit tests, etc., *is OPTIONAL*. As long as the patch works properly, it can be merged. 4 | 5 | However, if you can submit patches that pass all of our automated tests, then you'll lighten the load for the project maintainer (who already has enough to do!). This document describes what tests are done and what documentation is maintained. 6 | 7 | *Anything extra you can do is appreciated!* 8 | 9 | 10 | ## Tox Tests 11 | 12 | [Tox](https://tox.wiki/) is used to automate testing. Linting is done with [pylint](http://pylint.pycqa.org/en/latest/) & [flake8](https://flake8.pycqa.org/en/latest/), and static type-checking is done with [mypy](https://mypy.readthedocs.io/en/stable/). 13 | 14 | Install the required packages with `python3 -m pip install -U codecov coveralls flake8 mypy pylint pytest tox`, then run the tests with `python3 -m tox`. Look for any error messages in the (verbose) output. 15 | 16 | 17 | ## Docker Tests 18 | 19 | Docker is used to run ssh-audit against various real SSH servers (OpenSSH, Dropbear, and TinySSH). The output is then diff'ed against the expected result. Any differences result in failure. 20 | 21 | The docker tests are run with `./docker_test.sh`. 22 | 23 | 24 | ## Man Page 25 | 26 | The `ssh-audit.1` man page documents the various features of ssh-audit. If features are added, or significant behavior is modified, the man page needs to be updated. 27 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:latest 2 | FROM scratch AS files 3 | 4 | # Copy ssh-audit code to temporary container 5 | COPY ssh-audit.py / 6 | COPY src/ / 7 | 8 | FROM python:3-alpine AS runtime 9 | 10 | # Update the image to remediate any vulnerabilities. 11 | RUN apk upgrade -U --no-cache -a -l && \ 12 | # Remove suid & sgid bits from all files. 13 | find / -xdev -perm /6000 -exec chmod ug-s {} \; 2> /dev/null || true 14 | 15 | # Copy the ssh-audit code from files container. 16 | COPY --from=files / / 17 | 18 | # Allow listening on 2222/tcp for client auditing. 19 | EXPOSE 2222 20 | 21 | # Drop root privileges. 22 | USER nobody:nogroup 23 | 24 | ENTRYPOINT ["python3", "/ssh-audit.py"] 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (C) 2017-2025 Joe Testa (jtesta@positronsecurity.com) 4 | Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu) 5 | 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /Makefile.docker: -------------------------------------------------------------------------------- 1 | VERSION = $(shell grep VERSION src/ssh_audit/globals.py | grep -E -o "'(v.*)'" | tr -d "'") 2 | ifeq ($(VERSION),) 3 | $(error "could not determine version!") 4 | endif 5 | 6 | all: 7 | ./add_builtin_man_page.sh 8 | docker buildx create --name multiarch --use || exit 0 9 | docker buildx build \ 10 | --platform linux/amd64,linux/arm64,linux/arm/v7 \ 11 | --tag positronsecurity/ssh-audit:${VERSION} \ 12 | --tag positronsecurity/ssh-audit:latest \ 13 | . 14 | docker buildx build \ 15 | --tag positronsecurity/ssh-audit:${VERSION} \ 16 | --tag positronsecurity/ssh-audit:latest \ 17 | --load \ 18 | --builder=multiarch \ 19 | . 20 | 21 | upload: 22 | docker login -u positronsecurity 23 | docker buildx build \ 24 | --platform linux/amd64,linux/arm64,linux/arm/v7 \ 25 | --tag positronsecurity/ssh-audit:${VERSION} \ 26 | --tag positronsecurity/ssh-audit:latest \ 27 | --push \ 28 | . 29 | -------------------------------------------------------------------------------- /Makefile.pypi: -------------------------------------------------------------------------------- 1 | all: 2 | ./add_builtin_man_page.sh 3 | rm -rf /tmp/pypi_upload 4 | virtualenv -p /usr/bin/python3 /tmp/pypi_upload/ 5 | cp -R src /tmp/pypi_upload/ 6 | cp setup.py setup.cfg README.md LICENSE /tmp/pypi_upload/ 7 | /bin/bash -c "pushd /tmp/pypi_upload/; source bin/activate; pip3 install -U setuptools twine build; pip3 install -U requests_toolbelt; python3 -m build" 8 | 9 | uploadtest: 10 | /bin/bash -c "pushd /tmp/pypi_upload; source bin/activate; python3 -m twine upload --repository testpypi /tmp/pypi_upload/dist/*" 11 | 12 | uploadprod: 13 | /bin/bash -c "pushd /tmp/pypi_upload; source bin/activate; twine upload /tmp/pypi_upload/dist/*" 14 | 15 | clean: 16 | rm -rf /tmp/pypi_upload/ 17 | -------------------------------------------------------------------------------- /PACKAGING.md: -------------------------------------------------------------------------------- 1 | # Windows 2 | 3 | An executable can only be made on a Windows host because the PyInstaller tool (https://www.pyinstaller.org/) does not support cross-compilation. 4 | 5 | 1.) Install Python v3.x from https://www.python.org/. To make life easier, check the option to add Python to the PATH environment variable. 6 | 7 | 2.) Install Cygwin (https://www.cygwin.com/). 8 | 9 | 3.) Install/update package dependencies and create the executable with: 10 | 11 | ``` 12 | $ ./build_windows_executable.sh 13 | ``` 14 | 15 | 16 | # PyPI 17 | 18 | To create package and upload to test server (hint: use API token for test.pypi.org): 19 | 20 | ``` 21 | $ sudo apt install python3-virtualenv python3.12-venv 22 | $ make -f Makefile.pypi 23 | $ make -f Makefile.pypi uploadtest 24 | ``` 25 | 26 | To download from test server and verify: 27 | 28 | ``` 29 | $ virtualenv /tmp/pypi_test 30 | $ cd /tmp/pypi_test; source bin/activate 31 | $ pip3 install --index-url https://test.pypi.org/simple ssh-audit 32 | ``` 33 | 34 | To upload to production server (hint: use API token for production pypi.org): 35 | 36 | ``` 37 | $ make -f Makefile.pypi uploadprod 38 | ``` 39 | 40 | To download from production server and verify: 41 | 42 | ``` 43 | $ virtualenv /tmp/pypi_prod 44 | $ cd /tmp/pypi_prod; source bin/activate 45 | $ pip3 install ssh-audit 46 | ``` 47 | 48 | 49 | # Snap 50 | 51 | To create the Snap package, run a fully-updated Ubuntu Server 24.04 VM. 52 | 53 | Create the Snap package with: 54 | ``` 55 | $ ./build_snap.sh 56 | ``` 57 | 58 | Upload the Snap with: 59 | 60 | ``` 61 | $ snapcraft export-login ~/snap_creds.txt 62 | $ export SNAPCRAFT_STORE_CREDENTIALS=$(cat ~/snap_creds.txt) 63 | $ snapcraft upload --release=beta ssh-audit_*.snap 64 | $ snapcraft status ssh-audit # Note the revision number of the beta channel. 65 | $ snapcraft release ssh-audit X stable # Fill in with the revision number. 66 | ``` 67 | 68 | 69 | # Docker 70 | 71 | Ensure that the `buildx` plugin is available by following the installation instructions available at: https://docs.docker.com/engine/install/ubuntu/ 72 | 73 | Build a local image with: 74 | 75 | ``` 76 | $ make -f Makefile.docker 77 | ``` 78 | 79 | Create a multi-architecture build and upload it to Dockerhub with (hint: use the API token as the password): 80 | 81 | ``` 82 | $ make -f Makefile.docker upload 83 | ``` 84 | -------------------------------------------------------------------------------- /add_builtin_man_page.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # 4 | # The MIT License (MIT) 5 | # 6 | # Copyright (C) 2021-2024 Joe Testa (jtesta@positronsecurity.com) 7 | # Copyright (C) 2021 Adam Russell () 8 | # 9 | # Permission is hereby granted, free of charge, to any person obtaining a copy 10 | # of this software and associated documentation files (the "Software"), to deal 11 | # in the Software without restriction, including without limitation the rights 12 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | # copies of the Software, and to permit persons to whom the Software is 14 | # furnished to do so, subject to the following conditions: 15 | # 16 | # The above copyright notice and this permission notice shall be included in 17 | # all copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | # THE SOFTWARE. 26 | # 27 | 28 | ################################################################################ 29 | # add_builtin_man_page.sh 30 | # 31 | # PURPOSE 32 | # Since some platforms lack a manual reader it's necessary to provide an 33 | # alternative means of reading the man page. 34 | # 35 | # This script should be run as part of the ssh-audit packaging process for 36 | # Docker, PyPI, Snap, and Windows. It populates the 'BUILTIN_MAN_PAGE' 37 | # variable in 'globals.py' with the contents of the man page. Users can then 38 | # see the man page with "ssh-audit [--manual|-m]". 39 | # 40 | # Linux or Cygwin is required to run this script. 41 | # 42 | # USAGE 43 | # add_builtin_man_page.sh [-m ] [-g ] 44 | # 45 | ################################################################################ 46 | 47 | usage() { 48 | echo >&2 "Usage: $0 [-m ] [-g ] [-h]" 49 | echo >&2 " -m Specify an alternate man page path (default: ./ssh-audit.1)" 50 | echo >&2 " -g Specify an alternate globals.py path (default: ./src/ssh_audit/globals.py)" 51 | echo >&2 " -h This help message" 52 | } 53 | 54 | PLATFORM="$(uname -s)" 55 | 56 | # This script is intended for use on Linux and Cygwin only. 57 | case "${PLATFORM}" in 58 | Linux | CYGWIN*) ;; 59 | *) 60 | echo "Platform not supported: ${PLATFORM}" 61 | exit 1 62 | ;; 63 | esac 64 | 65 | MAN_PAGE=./ssh-audit.1 66 | GLOBALS_PY=./src/ssh_audit/globals.py 67 | 68 | while getopts "m: g: h" OPTION; do 69 | case "${OPTION}" in 70 | m) 71 | MAN_PAGE="${OPTARG}" 72 | ;; 73 | g) 74 | GLOBALS_PY="${OPTARG}" 75 | ;; 76 | h) 77 | usage 78 | exit 0 79 | ;; 80 | *) 81 | echo >&2 "Invalid parameter(s) provided" 82 | usage 83 | exit 1 84 | ;; 85 | esac 86 | done 87 | 88 | # Check that the specified files exist. 89 | [[ -f "$MAN_PAGE" ]] || { echo >&2 "man page file not found: $MAN_PAGE"; exit 1; } 90 | [[ -f "${GLOBALS_PY}" ]] || { echo >&2 "globals.py file not found: ${GLOBALS_PY}"; exit 1; } 91 | 92 | # Check that the 'ul' (do underlining) binary exists. 93 | if [[ "${PLATFORM}" == "Linux" ]]; then 94 | command -v ul >/dev/null 2>&1 || { echo >&2 "ul not found."; exit 1; } 95 | fi 96 | 97 | # Check that the 'sed' (stream editor) binary exists. 98 | command -v sed >/dev/null 2>&1 || { echo >&2 "sed not found."; exit 1; } 99 | 100 | # Reset the globals.py file, in case it was modified from a prior run. 101 | git checkout "${GLOBALS_PY}" > /dev/null 2>&1 102 | 103 | # Remove the Windows man page placeholder from 'globals.py'. 104 | sed -i '/^BUILTIN_MAN_PAGE/d' "${GLOBALS_PY}" 105 | 106 | echo "Processing man page at ${MAN_PAGE} and placing output into ${GLOBALS_PY}..." 107 | 108 | # Append the man page content to 'globals.py'. 109 | # * man outputs a backspace-overwrite sequence rather than an ANSI escape 110 | # sequence. 111 | # * 'MAN_KEEP_FORMATTING' preserves the backspace-overwrite sequence when 112 | # redirected to a file or a pipe. 113 | # * sed converts unicode hyphens into an ASCI equivalent. 114 | 115 | echo BUILTIN_MAN_PAGE = '"""' >> "${GLOBALS_PY}" 116 | MANWIDTH=80 MAN_KEEP_FORMATTING=1 man "${MAN_PAGE}" | sed $'s/\u2010/-/g' >> "${GLOBALS_PY}" 117 | echo '"""' >> "${GLOBALS_PY}" 118 | 119 | echo "Done." 120 | exit 0 121 | -------------------------------------------------------------------------------- /build_snap.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # 4 | # The MIT License (MIT) 5 | # 6 | # Copyright (C) 2021-2024 Joe Testa (jtesta@positronsecurity.com) 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | # 26 | 27 | ################################################################################ 28 | # build_snap.sh 29 | # 30 | # Builds a Snap package. 31 | ################################################################################ 32 | 33 | 34 | # Pre-requisites 35 | sudo apt install -y make 36 | sudo snap install snapcraft --classic 37 | sudo snap install review-tools lxd 38 | 39 | # Initialize LXD. 40 | sudo lxd init --auto 41 | 42 | # Reset the filesystem from any previous runs. 43 | rm -rf parts/ prime/ snap/ stage/ build/ dist/ src/*.egg-info/ ssh-audit*.snap 44 | git checkout snapcraft.yaml 2> /dev/null 45 | git checkout src/ssh_audit/globals.py 2> /dev/null 46 | 47 | # Add the built-in manual page. 48 | ./add_builtin_man_page.sh 49 | 50 | # Get the version from the globals.py file. 51 | version=$(grep VERSION src/ssh_audit/globals.py | awk 'BEGIN {FS="="} ; {print $2}' | tr -d '[:space:]') 52 | 53 | # Strip the quotes around the version (along with the initial 'v' character) and append "-1" to make the default Snap version (i.e.: 'v2.5.0' => '2.5.0-1') 54 | default_snap_version="${version:2:-1}-1" 55 | echo -e -n "\nEnter Snap package version [default: ${default_snap_version}]: " 56 | read -r snap_version 57 | 58 | # If no version was specified, use the default version. 59 | if [[ $snap_version == '' ]]; then 60 | snap_version=$default_snap_version 61 | echo -e "Using default snap version: ${snap_version}\n" 62 | fi 63 | 64 | # Ensure that the snap version fits the format of X.X.X-X. 65 | if [[ ! $snap_version =~ ^[0-9]\.[0-9]\.[0-9]\-[0-9]$ ]]; then 66 | echo "Error: version string does not match format X.X.X-X!" 67 | exit 1 68 | fi 69 | 70 | # Append the version field to the end of the file. Not pretty, but it works. 71 | echo -e "\nversion: '${snap_version}'" >> snapcraft.yaml 72 | 73 | # Set the SNAP_PACKAGE variable to True so that file permission errors give more user-friendly 74 | sed -i 's/SNAP_PACKAGE = False/SNAP_PACKAGE = True/' src/ssh_audit/globals.py 75 | 76 | snapcraft --use-lxd && echo -e "\nDone.\n" 77 | -------------------------------------------------------------------------------- /build_windows_executable.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # 4 | # The MIT License (MIT) 5 | # 6 | # Copyright (C) 2021-2024 Joe Testa (jtesta@positronsecurity.com) 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be included in 16 | # all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | # THE SOFTWARE. 25 | # 26 | 27 | ################################################################################ 28 | # build_windows_executable.sh 29 | # 30 | # Builds a Windows executable using PyInstaller. 31 | ################################################################################ 32 | 33 | 34 | PLATFORM="$(uname -s)" 35 | 36 | # This script is intended for use on Cygwin only. 37 | case "${PLATFORM}" in 38 | CYGWIN*) ;; 39 | *) 40 | echo "Platform not supported (${PLATFORM}). This must be run in Cygwin only." 41 | exit 1 42 | ;; 43 | esac 44 | 45 | # Ensure that Python 3.x is installed. 46 | if [[ "$(python -V)" != "Python 3."* ]]; then 47 | echo "Python v3.x not found. Install the latest stable version from: https://www.python.org/" 48 | exit 1 49 | fi 50 | 51 | # Install/update package dependencies. 52 | echo "Installing/updating pyinstaller and colorama packages..." 53 | pip install -U pyinstaller colorama 54 | echo 55 | 56 | # Prompt for the version to release. 57 | echo -n "Enter the version to release, using format 'vX.X.X': " 58 | read -r version 59 | 60 | # Ensure that entered version fits required format. 61 | if [[ ! $version =~ ^v[0-9]\.[0-9]\.[0-9]$ ]]; then 62 | echo "Error: version string does not match format vX.X.X!" 63 | exit 1 64 | fi 65 | 66 | # Verify that version is correct. 67 | echo -n "Version will be set to '${version}'. Is this correct? (y/n): " 68 | read -r yn 69 | echo 70 | 71 | if [[ $yn != "y" ]]; then 72 | echo "Build cancelled." 73 | exit 1 74 | fi 75 | 76 | # Reset any local changes made to globals.py from a previous run. 77 | git checkout src/ssh_audit/globals.py 2> /dev/null 78 | 79 | # Update the man page. 80 | ./add_builtin_man_page.sh 81 | retval=$? 82 | if [[ ${retval} != 0 ]]; then 83 | echo "Failed to run ./update_windows_man_page.sh" 84 | exit 1 85 | fi 86 | 87 | # Do all operations from this point from the main source directory. 88 | pushd src/ssh_audit || exit > /dev/null 89 | 90 | # Delete the existing VERSION variable and add the value that the user entered, above. 91 | sed -i '/^VERSION/d' globals.py 92 | echo "VERSION = '$version'" >> globals.py 93 | 94 | # Delete cached files if they exist from a prior run. 95 | rm -rf dist/ build/ ssh-audit.spec 96 | 97 | # Create a hard link from ssh_audit.py to ssh-audit.py. 98 | if [[ ! -f ssh-audit.py ]]; then 99 | ln ssh_audit.py ssh-audit.py 100 | fi 101 | 102 | echo -e "\nRunning pyinstaller...\n" 103 | pyinstaller -F --icon ../../windows_icon.ico ssh-audit.py 104 | 105 | if [[ -f dist/ssh-audit.exe ]]; then 106 | echo -e "\nExecutable created in $(pwd)/dist/ssh-audit.exe\n" 107 | else 108 | echo -e "\nFAILED to create $(pwd)/dist/ssh-audit.exe!\n" 109 | exit 1 110 | fi 111 | 112 | # Ensure that the version string doesn't have '-dev' in it. 113 | dist/ssh-audit.exe | grep -E 'ssh-audit.exe v.+\-dev' > /dev/null 114 | retval=$? 115 | if [[ ${retval} == 0 ]]; then 116 | echo -e "\nError: executable's version number includes '-dev'." 117 | exit 1 118 | fi 119 | 120 | # Remove the cache files created during the build process, along with the link we created, above. 121 | rm -rf build/ ssh-audit.spec ssh-audit.py 122 | 123 | # Reset the changes we made to globals.py. 124 | git checkout globals.py 2> /dev/null 125 | 126 | popd || exit > /dev/null 127 | exit 0 128 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | # https://pip.pypa.io/en/stable/reference/pip/#pep-517-and-518-support 3 | requires = [ 4 | "setuptools>=40.8.0", 5 | "wheel" 6 | ] 7 | build-backend = "setuptools.build_meta" 8 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = ssh-audit 3 | version = attr: ssh_audit.globals.VERSION 4 | author = Joe Testa 5 | author_email = jtesta@positronsecurity.com 6 | description = An SSH server & client configuration security auditing tool 7 | long_description = file: README.md 8 | long_description_content_type = text/markdown 9 | license = MIT 10 | license_files = LICENSE 11 | url = https://github.com/jtesta/ssh-audit 12 | project_urls = 13 | Source Code = https://github.com/jtesta/ssh-audit 14 | Bug Tracker = https://github.com/jtesta/ssh-audit/issues 15 | classifiers = 16 | Development Status :: 5 - Production/Stable 17 | Intended Audience :: Information Technology 18 | Intended Audience :: System Administrators 19 | License :: OSI Approved :: MIT License 20 | Operating System :: OS Independent 21 | Programming Language :: Python :: 3 22 | Programming Language :: Python :: 3.8 23 | Programming Language :: Python :: 3.9 24 | Programming Language :: Python :: 3.10 25 | Programming Language :: Python :: 3.11 26 | Programming Language :: Python :: 3.12 27 | Programming Language :: Python :: 3.13 28 | Programming Language :: Python :: Implementation :: CPython 29 | Programming Language :: Python :: Implementation :: PyPy 30 | Topic :: Security 31 | Topic :: Security :: Cryptography 32 | 33 | [options] 34 | packages = find: 35 | package_dir = 36 | = src 37 | python_requires = >=3.8,<4 38 | 39 | [options.packages.find] 40 | where = src 41 | 42 | [options.entry_points] 43 | console_scripts = 44 | ssh-audit = ssh_audit.ssh_audit:main 45 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import re 2 | import sys 3 | from setuptools import setup 4 | 5 | print_warning = False 6 | m = re.search(r'^VERSION\s*=\s*\'v(\d\.\d\.\d)\'', open('src/ssh_audit/globals.py').read(), re.M) 7 | if m is None: 8 | # If we failed to parse the stable version, see if this is the development version. 9 | m = re.search(r'^VERSION\s*=\s*\'v(\d\.\d\.\d-dev)\'', open('src/ssh_audit/globals.py').read(), re.M) 10 | if m is None: 11 | print("Error: could not parse VERSION variable from ssh_audit.py.") 12 | sys.exit(1) 13 | else: # Continue with the development version, but print a warning later. 14 | print_warning = True 15 | 16 | version = m.group(1) 17 | print("\n\nPackaging ssh-audit v%s...\n\n" % version) 18 | 19 | # see setup.cfg 20 | setup() 21 | 22 | if print_warning: 23 | print("\n\n !!! WARNING: development version detected (%s). Are you sure you want to package this version? Probably not...\n" % version) 24 | -------------------------------------------------------------------------------- /snapcraft.yaml: -------------------------------------------------------------------------------- 1 | name: ssh-audit 2 | # 'version' field will be automatically added by build_snap.sh. 3 | license: 'MIT' 4 | summary: ssh-audit 5 | description: | 6 | SSH server and client security configuration auditor. Official repository: 7 | 8 | base: core22 9 | grade: stable 10 | confinement: strict 11 | architectures: 12 | - build-on: [amd64] 13 | build-for: [all] 14 | 15 | apps: 16 | ssh-audit: 17 | command: bin/ssh-audit 18 | plugs: [network,network-bind,home] 19 | 20 | parts: 21 | ssh-audit: 22 | plugin: python 23 | source: . 24 | -------------------------------------------------------------------------------- /src/ssh_audit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtesta/ssh-audit/b456bb31b977a9101a8b13e5f7137561c82f57ba/src/ssh_audit/__init__.py -------------------------------------------------------------------------------- /src/ssh_audit/__main__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import traceback 3 | 4 | from ssh_audit.ssh_audit import main 5 | from ssh_audit import exitcodes 6 | 7 | 8 | exit_code = exitcodes.GOOD 9 | 10 | try: 11 | exit_code = main() 12 | except Exception: 13 | exit_code = exitcodes.UNKNOWN_ERROR 14 | print(traceback.format_exc()) 15 | 16 | sys.exit(exit_code) 17 | -------------------------------------------------------------------------------- /src/ssh_audit/algorithm.py: -------------------------------------------------------------------------------- 1 | """ 2 | The MIT License (MIT) 3 | 4 | Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu) 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | # pylint: disable=unused-import 25 | from typing import Dict, List, Set, Sequence, Tuple, Iterable # noqa: F401 26 | from typing import Callable, Optional, Union, Any # noqa: F401 27 | 28 | from ssh_audit.product import Product 29 | 30 | 31 | class Algorithm: 32 | 33 | @staticmethod 34 | def get_ssh_version(version_desc: str) -> Tuple[str, str, bool]: 35 | is_client = version_desc.endswith('C') 36 | if is_client: 37 | version_desc = version_desc[:-1] 38 | if version_desc.startswith('d'): 39 | return Product.DropbearSSH, version_desc[1:], is_client 40 | elif version_desc.startswith('l1'): 41 | return Product.LibSSH, version_desc[2:], is_client 42 | else: 43 | return Product.OpenSSH, version_desc, is_client 44 | 45 | @classmethod 46 | def get_since_text(cls, versions: List[Optional[str]]) -> Optional[str]: 47 | tv = [] 48 | if len(versions) == 0 or versions[0] is None: 49 | return None 50 | for v in versions[0].split(','): 51 | ssh_prod, ssh_ver, is_cli = cls.get_ssh_version(v) 52 | if not ssh_ver: 53 | continue 54 | if ssh_prod in [Product.LibSSH]: 55 | continue 56 | if is_cli: 57 | ssh_ver = '{} (client only)'.format(ssh_ver) 58 | tv.append('{} {}'.format(ssh_prod, ssh_ver)) 59 | if len(tv) == 0: 60 | return None 61 | return 'available since ' + ', '.join(tv).rstrip(', ') 62 | -------------------------------------------------------------------------------- /src/ssh_audit/banner.py: -------------------------------------------------------------------------------- 1 | """ 2 | The MIT License (MIT) 3 | 4 | Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu) 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | import re 25 | 26 | # pylint: disable=unused-import 27 | from typing import Dict, List, Set, Sequence, Tuple, Iterable # noqa: F401 28 | from typing import Callable, Optional, Union, Any # noqa: F401 29 | 30 | from ssh_audit.utils import Utils 31 | 32 | 33 | class Banner: 34 | _RXP, _RXR = r'SSH-\d\.\s*?\d+', r'(-\s*([^\s]*)(?:\s+(.*))?)?' 35 | RX_PROTOCOL = re.compile(re.sub(r'\\d(\+?)', r'(\\d\g<1>)', _RXP)) 36 | RX_BANNER = re.compile(r'^({0}(?:(?:-{0})*)){1}$'.format(_RXP, _RXR)) 37 | 38 | def __init__(self, protocol: Tuple[int, int], software: Optional[str], comments: Optional[str], valid_ascii: bool) -> None: 39 | self.__protocol = protocol 40 | self.__software = software 41 | self.__comments = comments 42 | self.__valid_ascii = valid_ascii 43 | 44 | @property 45 | def protocol(self) -> Tuple[int, int]: 46 | return self.__protocol 47 | 48 | @property 49 | def software(self) -> Optional[str]: 50 | return self.__software 51 | 52 | @property 53 | def comments(self) -> Optional[str]: 54 | return self.__comments 55 | 56 | @property 57 | def valid_ascii(self) -> bool: 58 | return self.__valid_ascii 59 | 60 | def __str__(self) -> str: 61 | r = 'SSH-{}.{}'.format(self.protocol[0], self.protocol[1]) 62 | if self.software is not None: 63 | r += '-{}'.format(self.software) 64 | if bool(self.comments): 65 | r += ' {}'.format(self.comments) 66 | return r 67 | 68 | def __repr__(self) -> str: 69 | p = '{}.{}'.format(self.protocol[0], self.protocol[1]) 70 | r = 'protocol={}'.format(p) 71 | if self.software is not None: 72 | r += ', software={}'.format(self.software) 73 | if bool(self.comments): 74 | r += ', comments={}'.format(self.comments) 75 | return '<{}({})>'.format(self.__class__.__name__, r) 76 | 77 | @classmethod 78 | def parse(cls, banner: str) -> Optional['Banner']: 79 | valid_ascii = Utils.is_print_ascii(banner) 80 | ascii_banner = Utils.to_print_ascii(banner) 81 | mx = cls.RX_BANNER.match(ascii_banner) 82 | if mx is None: 83 | return None 84 | protocol = min(re.findall(cls.RX_PROTOCOL, mx.group(1))) 85 | protocol = (int(protocol[0]), int(protocol[1])) 86 | software = (mx.group(3) or '').strip() or None 87 | if software is None and (mx.group(2) or '').startswith('-'): 88 | software = '' 89 | comments = (mx.group(4) or '').strip() or None 90 | if comments is not None: 91 | comments = re.sub(r'\s+', ' ', comments) 92 | return cls(protocol, software, comments, valid_ascii) 93 | -------------------------------------------------------------------------------- /src/ssh_audit/exitcodes.py: -------------------------------------------------------------------------------- 1 | # The program return values corresponding to failure(s) encountered, warning(s) encountered, connection errors, and no problems found, respectively. 2 | FAILURE = 3 3 | WARNING = 2 4 | CONNECTION_ERROR = 1 5 | GOOD = 0 6 | UNKNOWN_ERROR = -1 7 | -------------------------------------------------------------------------------- /src/ssh_audit/fingerprint.py: -------------------------------------------------------------------------------- 1 | """ 2 | The MIT License (MIT) 3 | 4 | Copyright (C) 2017-2021 Joe Testa (jtesta@positronsecurity.com) 5 | Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu) 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | """ 25 | import base64 26 | import hashlib 27 | 28 | 29 | class Fingerprint: 30 | def __init__(self, fpd: bytes) -> None: 31 | self.__fpd = fpd 32 | 33 | @property 34 | def md5(self) -> str: 35 | h = hashlib.md5(self.__fpd).hexdigest() 36 | r = ':'.join(h[i:i + 2] for i in range(0, len(h), 2)) 37 | return 'MD5:{}'.format(r) 38 | 39 | @property 40 | def sha256(self) -> str: 41 | h = base64.b64encode(hashlib.sha256(self.__fpd).digest()) 42 | r = h.decode('ascii').rstrip('=') 43 | return 'SHA256:{}'.format(r) 44 | -------------------------------------------------------------------------------- /src/ssh_audit/globals.py: -------------------------------------------------------------------------------- 1 | """ 2 | The MIT License (MIT) 3 | 4 | Copyright (C) 2017-2024 Joe Testa (jtesta@positronsecurity.com) 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | # The version to display. 25 | VERSION = 'v3.4.0-dev' 26 | 27 | # SSH software to impersonate 28 | SSH_HEADER = 'SSH-{0}-OpenSSH_8.2' 29 | 30 | # The URL to the Github issues tracker. 31 | GITHUB_ISSUES_URL = 'https://github.com/jtesta/ssh-audit/issues' 32 | 33 | # The man page. Only filled in on Docker, PyPI, Snap, and Windows builds. 34 | BUILTIN_MAN_PAGE = '' 35 | 36 | # True when installed from a Snap package, otherwise False. 37 | SNAP_PACKAGE = False 38 | 39 | # Error message when installed as a Snap package and a file access fails. 40 | SNAP_PERMISSIONS_ERROR = 'Error while accessing file. It appears that ssh-audit was installed as a Snap package. In that case, there are two options: 1.) only try to read & write files in the $HOME/snap/ssh-audit/common/ directory, or 2.) grant permissions to read & write files in $HOME using the following command: "sudo snap connect ssh-audit:home :home"' 41 | -------------------------------------------------------------------------------- /src/ssh_audit/product.py: -------------------------------------------------------------------------------- 1 | """ 2 | The MIT License (MIT) 3 | 4 | Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu) 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | 25 | 26 | class Product: # pylint: disable=too-few-public-methods 27 | OpenSSH = 'OpenSSH' 28 | DropbearSSH = 'Dropbear SSH' 29 | LibSSH = 'libssh' 30 | TinySSH = 'TinySSH' 31 | PuTTY = 'PuTTY' 32 | -------------------------------------------------------------------------------- /src/ssh_audit/protocol.py: -------------------------------------------------------------------------------- 1 | """ 2 | The MIT License (MIT) 3 | 4 | Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu) 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | 25 | 26 | class Protocol: # pylint: disable=too-few-public-methods 27 | SMSG_PUBLIC_KEY = 2 28 | MSG_DEBUG = 4 29 | MSG_KEXINIT = 20 30 | MSG_NEWKEYS = 21 31 | MSG_KEXDH_INIT = 30 32 | MSG_KEXDH_REPLY = 31 33 | MSG_KEXDH_GEX_REQUEST = 34 34 | MSG_KEXDH_GEX_GROUP = 31 35 | MSG_KEXDH_GEX_INIT = 32 36 | MSG_KEXDH_GEX_REPLY = 33 37 | -------------------------------------------------------------------------------- /src/ssh_audit/readbuf.py: -------------------------------------------------------------------------------- 1 | """ 2 | The MIT License (MIT) 3 | 4 | Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu) 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | import io 25 | import struct 26 | 27 | # pylint: disable=unused-import 28 | from typing import Dict, List, Set, Sequence, Tuple, Iterable # noqa: F401 29 | from typing import Callable, Optional, Union, Any # noqa: F401 30 | 31 | 32 | class ReadBuf: 33 | def __init__(self, data: Optional[bytes] = None) -> None: 34 | super(ReadBuf, self).__init__() 35 | self._buf = io.BytesIO(data) if data is not None else io.BytesIO() 36 | self._len = len(data) if data is not None else 0 37 | 38 | @property 39 | def unread_len(self) -> int: 40 | return self._len - self._buf.tell() 41 | 42 | def read(self, size: int) -> bytes: 43 | return self._buf.read(size) 44 | 45 | def read_byte(self) -> int: 46 | v: int = struct.unpack('B', self.read(1))[0] 47 | return v 48 | 49 | def read_bool(self) -> bool: 50 | return self.read_byte() != 0 51 | 52 | def read_int(self) -> int: 53 | v: int = struct.unpack('>I', self.read(4))[0] 54 | return v 55 | 56 | def read_list(self) -> List[str]: 57 | list_size = self.read_int() 58 | return self.read(list_size).decode('utf-8', 'replace').split(',') 59 | 60 | def read_string(self) -> bytes: 61 | n = self.read_int() 62 | return self.read(n) 63 | 64 | @classmethod 65 | def _parse_mpint(cls, v: bytes, pad: bytes, f: str) -> int: 66 | r = 0 67 | if len(v) % 4 != 0: 68 | v = pad * (4 - (len(v) % 4)) + v 69 | for i in range(0, len(v), 4): 70 | r = (r << 32) | struct.unpack(f, v[i:i + 4])[0] 71 | return r 72 | 73 | def read_mpint1(self) -> int: 74 | # NOTE: Data Type Enc @ http://www.snailbook.com/docs/protocol-1.5.txt 75 | bits = struct.unpack('>H', self.read(2))[0] 76 | n = (bits + 7) // 8 77 | return self._parse_mpint(self.read(n), b'\x00', '>I') 78 | 79 | def read_mpint2(self) -> int: 80 | # NOTE: Section 5 @ https://www.ietf.org/rfc/rfc4251.txt 81 | v = self.read_string() 82 | if len(v) == 0: 83 | return 0 84 | pad, f = (b'\xff', '>i') if ord(v[0:1]) & 0x80 != 0 else (b'\x00', '>I') 85 | return self._parse_mpint(v, pad, f) 86 | 87 | def read_line(self) -> str: 88 | return self._buf.readline().rstrip().decode('utf-8', 'replace') 89 | 90 | def reset(self) -> None: 91 | self._buf = io.BytesIO() 92 | self._len = 0 93 | -------------------------------------------------------------------------------- /src/ssh_audit/ssh1.py: -------------------------------------------------------------------------------- 1 | """ 2 | The MIT License (MIT) 3 | 4 | Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu) 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | # pylint: disable=unused-import 25 | from typing import Dict, List, Set, Sequence, Tuple, Iterable # noqa: F401 26 | from typing import Callable, Optional, Union, Any # noqa: F401 27 | 28 | from ssh_audit.ssh1_crc32 import SSH1_CRC32 29 | 30 | 31 | class SSH1: 32 | _crc32: Optional[SSH1_CRC32] = None 33 | CIPHERS = ['none', 'idea', 'des', '3des', 'tss', 'rc4', 'blowfish'] 34 | AUTHS = ['none', 'rhosts', 'rsa', 'password', 'rhosts_rsa', 'tis', 'kerberos'] 35 | 36 | @classmethod 37 | def crc32(cls, v: bytes) -> int: 38 | if cls._crc32 is None: 39 | cls._crc32 = SSH1_CRC32() 40 | return cls._crc32.calc(v) 41 | -------------------------------------------------------------------------------- /src/ssh_audit/ssh1_crc32.py: -------------------------------------------------------------------------------- 1 | """ 2 | The MIT License (MIT) 3 | 4 | Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu) 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | # pylint: disable=unused-import 25 | from typing import Dict, List, Set, Sequence, Tuple, Iterable # noqa: F401 26 | from typing import Callable, Optional, Union, Any # noqa: F401 27 | 28 | 29 | class SSH1_CRC32: 30 | def __init__(self) -> None: 31 | self._table = [0] * 256 32 | for i in range(256): 33 | crc = 0 34 | n = i 35 | for _ in range(8): 36 | x = (crc ^ n) & 1 37 | crc = (crc >> 1) ^ (x * 0xedb88320) 38 | n = n >> 1 39 | self._table[i] = crc 40 | 41 | def calc(self, v: bytes) -> int: 42 | crc, length = 0, len(v) 43 | for i in range(length): 44 | n = ord(v[i:i + 1]) 45 | n = n ^ (crc & 0xff) 46 | crc = (crc >> 8) ^ self._table[n] 47 | return crc 48 | -------------------------------------------------------------------------------- /src/ssh_audit/ssh1_kexdb.py: -------------------------------------------------------------------------------- 1 | """ 2 | The MIT License (MIT) 3 | 4 | Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu) 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | # pylint: disable=unused-import 25 | import copy 26 | import threading 27 | 28 | from typing import Dict, List, Set, Sequence, Tuple, Iterable # noqa: F401 29 | from typing import Callable, Optional, Union, Any # noqa: F401 30 | 31 | 32 | class SSH1_KexDB: # pylint: disable=too-few-public-methods 33 | 34 | FAIL_PLAINTEXT = 'no encryption/integrity' 35 | FAIL_OPENSSH37_REMOVE = 'removed since OpenSSH 3.7' 36 | FAIL_NA_BROKEN = 'not implemented in OpenSSH, broken algorithm' 37 | FAIL_NA_UNSAFE = 'not implemented in OpenSSH (server), unsafe algorithm' 38 | TEXT_CIPHER_IDEA = 'cipher used by commercial SSH' 39 | 40 | DB_PER_THREAD: Dict[int, Dict[str, Dict[str, List[List[Optional[str]]]]]] = {} 41 | 42 | MASTER_DB: Dict[str, Dict[str, List[List[Optional[str]]]]] = { 43 | 'key': { 44 | 'ssh-rsa1': [['1.2.2']], 45 | }, 46 | 'enc': { 47 | 'none': [['1.2.2'], [FAIL_PLAINTEXT]], 48 | 'idea': [[None], [], [], [TEXT_CIPHER_IDEA]], 49 | 'des': [['2.3.0C'], [FAIL_NA_UNSAFE]], 50 | '3des': [['1.2.2']], 51 | 'tss': [[''], [FAIL_NA_BROKEN]], 52 | 'rc4': [[], [FAIL_NA_BROKEN]], 53 | 'blowfish': [['1.2.2']], 54 | }, 55 | 'aut': { 56 | 'rhosts': [['1.2.2', '3.6'], [FAIL_OPENSSH37_REMOVE]], 57 | 'rsa': [['1.2.2']], 58 | 'password': [['1.2.2']], 59 | 'rhosts_rsa': [['1.2.2']], 60 | 'tis': [['1.2.2']], 61 | 'kerberos': [['1.2.2', '3.6'], [FAIL_OPENSSH37_REMOVE]], 62 | } 63 | } 64 | 65 | 66 | @staticmethod 67 | def get_db() -> Dict[str, Dict[str, List[List[Optional[str]]]]]: 68 | '''Returns a copy of the MASTER_DB that is private to the calling thread. This prevents multiple threads from polluting the results of other threads.''' 69 | calling_thread_id = threading.get_ident() 70 | 71 | if calling_thread_id not in SSH1_KexDB.DB_PER_THREAD: 72 | SSH1_KexDB.DB_PER_THREAD[calling_thread_id] = copy.deepcopy(SSH1_KexDB.MASTER_DB) 73 | 74 | return SSH1_KexDB.DB_PER_THREAD[calling_thread_id] 75 | 76 | 77 | @staticmethod 78 | def thread_exit() -> None: 79 | '''Deletes the calling thread's copy of the MASTER_DB. This is needed because, in rare circumstances, a terminated thread's ID can be re-used by new threads.''' 80 | 81 | calling_thread_id = threading.get_ident() 82 | 83 | if calling_thread_id in SSH1_KexDB.DB_PER_THREAD: 84 | del SSH1_KexDB.DB_PER_THREAD[calling_thread_id] 85 | -------------------------------------------------------------------------------- /src/ssh_audit/ssh1_publickeymessage.py: -------------------------------------------------------------------------------- 1 | """ 2 | The MIT License (MIT) 3 | 4 | Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu) 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | # pylint: disable=unused-import 25 | from typing import Dict, List, Set, Sequence, Tuple, Iterable # noqa: F401 26 | from typing import Callable, Optional, Union, Any # noqa: F401 27 | 28 | from ssh_audit.ssh1 import SSH1 29 | from ssh_audit.readbuf import ReadBuf 30 | from ssh_audit.utils import Utils 31 | from ssh_audit.writebuf import WriteBuf 32 | 33 | 34 | class SSH1_PublicKeyMessage: 35 | def __init__(self, cookie: bytes, skey: Tuple[int, int, int], hkey: Tuple[int, int, int], pflags: int, cmask: int, amask: int) -> None: 36 | if len(skey) != 3: 37 | raise ValueError('invalid server key pair: {}'.format(skey)) 38 | if len(hkey) != 3: 39 | raise ValueError('invalid host key pair: {}'.format(hkey)) 40 | self.__cookie = cookie 41 | self.__server_key = skey 42 | self.__host_key = hkey 43 | self.__protocol_flags = pflags 44 | self.__supported_ciphers_mask = cmask 45 | self.__supported_authentications_mask = amask 46 | 47 | @property 48 | def cookie(self) -> bytes: 49 | return self.__cookie 50 | 51 | @property 52 | def server_key_bits(self) -> int: 53 | return self.__server_key[0] 54 | 55 | @property 56 | def server_key_public_exponent(self) -> int: 57 | return self.__server_key[1] 58 | 59 | @property 60 | def server_key_public_modulus(self) -> int: 61 | return self.__server_key[2] 62 | 63 | @property 64 | def host_key_bits(self) -> int: 65 | return self.__host_key[0] 66 | 67 | @property 68 | def host_key_public_exponent(self) -> int: 69 | return self.__host_key[1] 70 | 71 | @property 72 | def host_key_public_modulus(self) -> int: 73 | return self.__host_key[2] 74 | 75 | @property 76 | def host_key_fingerprint_data(self) -> bytes: 77 | # pylint: disable=protected-access 78 | mod = WriteBuf._create_mpint(self.host_key_public_modulus, False) 79 | e = WriteBuf._create_mpint(self.host_key_public_exponent, False) 80 | return mod + e 81 | 82 | @property 83 | def protocol_flags(self) -> int: 84 | return self.__protocol_flags 85 | 86 | @property 87 | def supported_ciphers_mask(self) -> int: 88 | return self.__supported_ciphers_mask 89 | 90 | @property 91 | def supported_ciphers(self) -> List[str]: 92 | ciphers = [] 93 | for i in range(len(SSH1.CIPHERS)): # pylint: disable=consider-using-enumerate 94 | if self.__supported_ciphers_mask & (1 << i) != 0: 95 | ciphers.append(Utils.to_text(SSH1.CIPHERS[i])) 96 | return ciphers 97 | 98 | @property 99 | def supported_authentications_mask(self) -> int: 100 | return self.__supported_authentications_mask 101 | 102 | @property 103 | def supported_authentications(self) -> List[str]: 104 | auths = [] 105 | for i in range(1, len(SSH1.AUTHS)): 106 | if self.__supported_authentications_mask & (1 << i) != 0: 107 | auths.append(Utils.to_text(SSH1.AUTHS[i])) 108 | return auths 109 | 110 | def write(self, wbuf: 'WriteBuf') -> None: 111 | wbuf.write(self.cookie) 112 | wbuf.write_int(self.server_key_bits) 113 | wbuf.write_mpint1(self.server_key_public_exponent) 114 | wbuf.write_mpint1(self.server_key_public_modulus) 115 | wbuf.write_int(self.host_key_bits) 116 | wbuf.write_mpint1(self.host_key_public_exponent) 117 | wbuf.write_mpint1(self.host_key_public_modulus) 118 | wbuf.write_int(self.protocol_flags) 119 | wbuf.write_int(self.supported_ciphers_mask) 120 | wbuf.write_int(self.supported_authentications_mask) 121 | 122 | @property 123 | def payload(self) -> bytes: 124 | wbuf = WriteBuf() 125 | self.write(wbuf) 126 | return wbuf.write_flush() 127 | 128 | @classmethod 129 | def parse(cls, payload: bytes) -> 'SSH1_PublicKeyMessage': 130 | buf = ReadBuf(payload) 131 | cookie = buf.read(8) 132 | server_key_bits = buf.read_int() 133 | server_key_exponent = buf.read_mpint1() 134 | server_key_modulus = buf.read_mpint1() 135 | skey = (server_key_bits, server_key_exponent, server_key_modulus) 136 | host_key_bits = buf.read_int() 137 | host_key_exponent = buf.read_mpint1() 138 | host_key_modulus = buf.read_mpint1() 139 | hkey = (host_key_bits, host_key_exponent, host_key_modulus) 140 | pflags = buf.read_int() 141 | cmask = buf.read_int() 142 | amask = buf.read_int() 143 | pkm = cls(cookie, skey, hkey, pflags, cmask, amask) 144 | return pkm 145 | -------------------------------------------------------------------------------- /src/ssh_audit/ssh2_kex.py: -------------------------------------------------------------------------------- 1 | """ 2 | The MIT License (MIT) 3 | 4 | Copyright (C) 2017-2024 Joe Testa (jtesta@positronsecurity.com) 5 | Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu) 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | """ 25 | from typing import Dict, List 26 | from typing import Union 27 | 28 | from ssh_audit.outputbuffer import OutputBuffer 29 | from ssh_audit.readbuf import ReadBuf 30 | from ssh_audit.ssh2_kexparty import SSH2_KexParty 31 | from ssh_audit.writebuf import WriteBuf 32 | 33 | 34 | class SSH2_Kex: 35 | def __init__(self, outputbuffer: 'OutputBuffer', cookie: bytes, kex_algs: List[str], key_algs: List[str], cli: 'SSH2_KexParty', srv: 'SSH2_KexParty', follows: bool, unused: int = 0) -> None: # pylint: disable=too-many-arguments 36 | self.__outputbuffer = outputbuffer 37 | self.__cookie = cookie 38 | self.__kex_algs = kex_algs 39 | self.__key_algs = key_algs 40 | self.__client = cli 41 | self.__server = srv 42 | self.__follows = follows 43 | self.__unused = unused 44 | 45 | self.__dh_modulus_sizes: Dict[str, int] = {} 46 | self.__host_keys: Dict[str, Dict[str, Union[bytes, str, int]]] = {} 47 | 48 | @property 49 | def cookie(self) -> bytes: 50 | return self.__cookie 51 | 52 | @property 53 | def kex_algorithms(self) -> List[str]: 54 | return self.__kex_algs 55 | 56 | @property 57 | def key_algorithms(self) -> List[str]: 58 | return self.__key_algs 59 | 60 | # client_to_server 61 | @property 62 | def client(self) -> 'SSH2_KexParty': 63 | return self.__client 64 | 65 | # server_to_client 66 | @property 67 | def server(self) -> 'SSH2_KexParty': 68 | return self.__server 69 | 70 | @property 71 | def follows(self) -> bool: 72 | return self.__follows 73 | 74 | @property 75 | def unused(self) -> int: 76 | return self.__unused 77 | 78 | def set_dh_modulus_size(self, gex_alg: str, modulus_size: int) -> None: 79 | self.__dh_modulus_sizes[gex_alg] = modulus_size 80 | 81 | def dh_modulus_sizes(self) -> Dict[str, int]: 82 | return self.__dh_modulus_sizes 83 | 84 | def set_host_key(self, key_type: str, raw_hostkey_bytes: bytes, hostkey_size: int, ca_key_type: str, ca_key_size: int) -> None: 85 | 86 | if key_type not in self.__host_keys: 87 | self.__host_keys[key_type] = {'raw_hostkey_bytes': raw_hostkey_bytes, 'hostkey_size': hostkey_size, 'ca_key_type': ca_key_type, 'ca_key_size': ca_key_size} 88 | else: # A host key may only have one CA signature... 89 | self.__outputbuffer.d("WARNING: called SSH2_Kex.set_host_key() multiple times with the same host key type (%s)! Existing info: %r, %r, %r; Duplicate (ignored) info: %r, %r, %r" % (key_type, self.__host_keys[key_type]['hostkey_size'], self.__host_keys[key_type]['ca_key_type'], self.__host_keys[key_type]['ca_key_size'], hostkey_size, ca_key_type, ca_key_size)) 90 | 91 | def host_keys(self) -> Dict[str, Dict[str, Union[bytes, str, int]]]: 92 | return self.__host_keys 93 | 94 | def write(self, wbuf: 'WriteBuf') -> None: 95 | wbuf.write(self.cookie) 96 | wbuf.write_list(self.kex_algorithms) 97 | wbuf.write_list(self.key_algorithms) 98 | wbuf.write_list(self.client.encryption) 99 | wbuf.write_list(self.server.encryption) 100 | wbuf.write_list(self.client.mac) 101 | wbuf.write_list(self.server.mac) 102 | wbuf.write_list(self.client.compression) 103 | wbuf.write_list(self.server.compression) 104 | wbuf.write_list(self.client.languages) 105 | wbuf.write_list(self.server.languages) 106 | wbuf.write_bool(self.follows) 107 | wbuf.write_int(self.__unused) 108 | 109 | @property 110 | def payload(self) -> bytes: 111 | wbuf = WriteBuf() 112 | self.write(wbuf) 113 | return wbuf.write_flush() 114 | 115 | @classmethod 116 | def parse(cls, outputbuffer: 'OutputBuffer', payload: bytes) -> 'SSH2_Kex': 117 | buf = ReadBuf(payload) 118 | cookie = buf.read(16) 119 | kex_algs = buf.read_list() 120 | key_algs = buf.read_list() 121 | cli_enc = buf.read_list() 122 | srv_enc = buf.read_list() 123 | cli_mac = buf.read_list() 124 | srv_mac = buf.read_list() 125 | cli_compression = buf.read_list() 126 | srv_compression = buf.read_list() 127 | cli_languages = buf.read_list() 128 | srv_languages = buf.read_list() 129 | follows = buf.read_bool() 130 | unused = buf.read_int() 131 | cli = SSH2_KexParty(cli_enc, cli_mac, cli_compression, cli_languages) 132 | srv = SSH2_KexParty(srv_enc, srv_mac, srv_compression, srv_languages) 133 | kex = cls(outputbuffer, cookie, kex_algs, key_algs, cli, srv, follows, unused) 134 | return kex 135 | 136 | def __str__(self) -> str: 137 | ret = "----\nSSH2_Kex object:" 138 | ret += "\nHost keys: " 139 | ret += ", ".join(self.__key_algs) 140 | ret += "\nKey exchanges: " 141 | ret += ", ".join(self.__kex_algs) 142 | ret += "\nClient SSH2_KexParty:" 143 | ret += "\n" + str(self.__client) 144 | ret += "\nServer SSH2_KexParty:" 145 | ret += "\n" + str(self.__server) 146 | ret += "\n----" 147 | return ret 148 | -------------------------------------------------------------------------------- /src/ssh_audit/ssh2_kexparty.py: -------------------------------------------------------------------------------- 1 | """ 2 | The MIT License (MIT) 3 | 4 | Copyright (C) 2024 Joe Testa (jtesta@positronsecurity.com) 5 | Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu) 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | """ 25 | # pylint: disable=unused-import 26 | from typing import Dict, List, Set, Sequence, Tuple, Iterable # noqa: F401 27 | from typing import Callable, Optional, Union, Any # noqa: F401 28 | 29 | 30 | class SSH2_KexParty: 31 | def __init__(self, enc: List[str], mac: List[str], compression: List[str], languages: List[str]) -> None: 32 | self.__enc = enc 33 | self.__mac = mac 34 | self.__compression = compression 35 | self.__languages = languages 36 | 37 | @property 38 | def encryption(self) -> List[str]: 39 | return self.__enc 40 | 41 | @property 42 | def mac(self) -> List[str]: 43 | return self.__mac 44 | 45 | @property 46 | def compression(self) -> List[str]: 47 | return self.__compression 48 | 49 | @property 50 | def languages(self) -> List[str]: 51 | return self.__languages 52 | 53 | def __str__(self) -> str: 54 | ret = "Ciphers: " + ", ".join(self.__enc) 55 | ret += "\nMACs: " + ", ".join(self.__mac) 56 | ret += "\nCompressions: " + ", ".join(self.__compression) 57 | ret += "\nLanguages: " + ", ".join(self.__languages) 58 | return ret 59 | -------------------------------------------------------------------------------- /src/ssh_audit/timeframe.py: -------------------------------------------------------------------------------- 1 | """ 2 | The MIT License (MIT) 3 | 4 | Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu) 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | # pylint: disable=unused-import 25 | from typing import Dict, List, Set, Sequence, Tuple, Iterable # noqa: F401 26 | from typing import Callable, Optional, Union, Any # noqa: F401 27 | 28 | from ssh_audit.algorithm import Algorithm 29 | 30 | 31 | class Timeframe: 32 | def __init__(self) -> None: 33 | self.__storage: Dict[str, List[Optional[str]]] = {} 34 | 35 | def __contains__(self, product: str) -> bool: 36 | return product in self.__storage 37 | 38 | def __getitem__(self, product: str) -> Sequence[Optional[str]]: 39 | return tuple(self.__storage.get(product, [None] * 4)) 40 | 41 | def __str__(self) -> str: 42 | return self.__storage.__str__() 43 | 44 | def __repr__(self) -> str: 45 | return self.__str__() 46 | 47 | def get_from(self, product: str, for_server: bool = True) -> Optional[str]: 48 | return self[product][0 if bool(for_server) else 2] 49 | 50 | def get_till(self, product: str, for_server: bool = True) -> Optional[str]: 51 | return self[product][1 if bool(for_server) else 3] 52 | 53 | def _update(self, versions: Optional[str], pos: int) -> None: 54 | ssh_versions: Dict[str, str] = {} 55 | for_srv, for_cli = pos < 2, pos > 1 56 | for v in (versions or '').split(','): 57 | ssh_prod, ssh_ver, is_cli = Algorithm.get_ssh_version(v) 58 | if not ssh_ver or (is_cli and for_srv) or (not is_cli and for_cli and ssh_prod in ssh_versions): 59 | continue 60 | ssh_versions[ssh_prod] = ssh_ver 61 | for ssh_product, ssh_version in ssh_versions.items(): 62 | if ssh_product not in self.__storage: 63 | self.__storage[ssh_product] = [None] * 4 64 | prev = self[ssh_product][pos] 65 | if (prev is None or (prev < ssh_version and pos % 2 == 0) or (prev > ssh_version and pos % 2 == 1)): 66 | self.__storage[ssh_product][pos] = ssh_version 67 | 68 | def update(self, versions: List[Optional[str]], for_server: Optional[bool] = None) -> 'Timeframe': 69 | for_cli = for_server is None or for_server is False 70 | for_srv = for_server is None or for_server is True 71 | vlen = len(versions) 72 | for i in range(min(3, vlen)): 73 | if for_srv and i < 2: 74 | self._update(versions[i], i) 75 | if for_cli and (i % 2 == 0 or vlen == 2): 76 | self._update(versions[i], 3 - 0**i) 77 | return self 78 | -------------------------------------------------------------------------------- /src/ssh_audit/utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | The MIT License (MIT) 3 | 4 | Copyright (C) 2017-2020 Joe Testa (jtesta@positronsecurity.com) 5 | Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu) 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | """ 25 | import ipaddress 26 | import re 27 | import sys 28 | 29 | # pylint: disable=unused-import 30 | from typing import Dict, List, Set, Sequence, Tuple, Iterable # noqa: F401 31 | from typing import Callable, Optional, Union, Any # noqa: F401 32 | 33 | 34 | class Utils: 35 | @classmethod 36 | def _type_err(cls, v: Any, target: str) -> TypeError: 37 | return TypeError('cannot convert {} to {}'.format(type(v), target)) 38 | 39 | @classmethod 40 | def to_bytes(cls, v: Union[bytes, str], enc: str = 'utf-8') -> bytes: 41 | if isinstance(v, bytes): 42 | return v 43 | elif isinstance(v, str): 44 | return v.encode(enc) 45 | raise cls._type_err(v, 'bytes') 46 | 47 | @classmethod 48 | def to_text(cls, v: Union[str, bytes], enc: str = 'utf-8') -> str: 49 | if isinstance(v, str): 50 | return v 51 | elif isinstance(v, bytes): 52 | return v.decode(enc) 53 | raise cls._type_err(v, 'unicode text') 54 | 55 | @classmethod 56 | def _is_ascii(cls, v: str, char_filter: Callable[[int], bool] = lambda x: x <= 127) -> bool: 57 | r = False 58 | if isinstance(v, str): 59 | for c in v: 60 | i = cls.ctoi(c) 61 | if not char_filter(i): 62 | return r 63 | r = True 64 | return r 65 | 66 | @classmethod 67 | def _to_ascii(cls, v: str, char_filter: Callable[[int], bool] = lambda x: x <= 127, errors: str = 'replace') -> str: 68 | if isinstance(v, str): 69 | r = bytearray() 70 | for c in v: 71 | i = cls.ctoi(c) 72 | if char_filter(i): 73 | r.append(i) 74 | else: 75 | if errors == 'ignore': 76 | continue 77 | r.append(63) 78 | return cls.to_text(r.decode('ascii')) 79 | raise cls._type_err(v, 'ascii') 80 | 81 | @classmethod 82 | def is_ascii(cls, v: str) -> bool: 83 | return cls._is_ascii(v) 84 | 85 | @classmethod 86 | def to_ascii(cls, v: str, errors: str = 'replace') -> str: 87 | return cls._to_ascii(v, errors=errors) 88 | 89 | @classmethod 90 | def is_print_ascii(cls, v: str) -> bool: 91 | return cls._is_ascii(v, lambda x: 126 >= x >= 32) 92 | 93 | @classmethod 94 | def to_print_ascii(cls, v: str, errors: str = 'replace') -> str: 95 | return cls._to_ascii(v, lambda x: 126 >= x >= 32, errors) 96 | 97 | @classmethod 98 | def unique_seq(cls, seq: Sequence[Any]) -> Sequence[Any]: 99 | seen: Set[Any] = set() 100 | 101 | def _seen_add(x: Any) -> bool: 102 | seen.add(x) 103 | return False 104 | 105 | if isinstance(seq, tuple): 106 | return tuple(x for x in seq if x not in seen and not _seen_add(x)) 107 | else: 108 | return [x for x in seq if x not in seen and not _seen_add(x)] 109 | 110 | @classmethod 111 | def ctoi(cls, c: Union[str, int]) -> int: 112 | if isinstance(c, str): 113 | return ord(c[0]) 114 | else: 115 | return c 116 | 117 | @staticmethod 118 | def parse_int(v: Any) -> int: 119 | try: 120 | return int(v) 121 | except ValueError: 122 | return 0 123 | 124 | @staticmethod 125 | def parse_float(v: Any) -> float: 126 | try: 127 | return float(v) 128 | except ValueError: 129 | return -1.0 130 | 131 | @staticmethod 132 | def parse_host_and_port(host_and_port: str, default_port: int = 22) -> Tuple[str, int]: 133 | '''Parses a string into a tuple of its host and port. The port is 0 if not specified.''' 134 | host = host_and_port 135 | port = default_port 136 | 137 | mx = re.match(r'^\[([^\]]+)\](?::(\d+))?$', host_and_port) 138 | if mx is not None: 139 | host = mx.group(1) 140 | port_str = mx.group(2) 141 | if port_str is not None: 142 | port = int(port_str) 143 | else: 144 | s = host_and_port.split(':') 145 | if len(s) == 2: 146 | host = s[0] 147 | if len(s[1]) > 0: 148 | port = int(s[1]) 149 | 150 | return host, port 151 | 152 | @staticmethod 153 | def is_ipv6_address(address: str) -> bool: 154 | '''Returns True if address is an IPv6 address, otherwise False.''' 155 | is_ipv6 = True 156 | try: 157 | ipaddress.IPv6Address(address) 158 | except ipaddress.AddressValueError: 159 | is_ipv6 = False 160 | 161 | return is_ipv6 162 | 163 | @staticmethod 164 | def is_windows() -> bool: 165 | return sys.platform in ['win32', 'cygwin'] 166 | -------------------------------------------------------------------------------- /src/ssh_audit/writebuf.py: -------------------------------------------------------------------------------- 1 | """ 2 | The MIT License (MIT) 3 | 4 | Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu) 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | import io 25 | import struct 26 | 27 | # pylint: disable=unused-import 28 | from typing import Dict, List, Set, Sequence, Tuple, Iterable # noqa: F401 29 | from typing import Callable, Optional, Union, Any # noqa: F401 30 | 31 | 32 | class WriteBuf: 33 | def __init__(self, data: Optional[bytes] = None) -> None: 34 | super(WriteBuf, self).__init__() 35 | self._wbuf = io.BytesIO(data) if data is not None else io.BytesIO() 36 | 37 | def write(self, data: bytes) -> 'WriteBuf': 38 | self._wbuf.write(data) 39 | return self 40 | 41 | def write_byte(self, v: int) -> 'WriteBuf': 42 | return self.write(struct.pack('B', v)) 43 | 44 | def write_bool(self, v: bool) -> 'WriteBuf': 45 | return self.write_byte(1 if v else 0) 46 | 47 | def write_int(self, v: int) -> 'WriteBuf': 48 | return self.write(struct.pack('>I', v)) 49 | 50 | def write_string(self, v: Union[bytes, str]) -> 'WriteBuf': 51 | if not isinstance(v, bytes): 52 | v = bytes(bytearray(v, 'utf-8')) 53 | self.write_int(len(v)) 54 | return self.write(v) 55 | 56 | def write_list(self, v: List[str]) -> 'WriteBuf': 57 | return self.write_string(','.join(v)) 58 | 59 | @classmethod 60 | def _bitlength(cls, n: int) -> int: 61 | try: 62 | return n.bit_length() 63 | except AttributeError: 64 | return len(bin(n)) - (2 if n > 0 else 3) 65 | 66 | @classmethod 67 | def _create_mpint(cls, n: int, signed: bool = True, bits: Optional[int] = None) -> bytes: 68 | if bits is None: 69 | bits = cls._bitlength(n) 70 | length = bits // 8 + (1 if n != 0 else 0) 71 | ql = (length + 7) // 8 72 | fmt, v2 = '>{}Q'.format(ql), [0] * ql 73 | for i in range(ql): 74 | v2[ql - i - 1] = n & 0xffffffffffffffff 75 | n >>= 64 76 | data = bytes(struct.pack(fmt, *v2)[-length:]) 77 | if not signed: 78 | data = data.lstrip(b'\x00') 79 | elif data.startswith(b'\xff\x80'): 80 | data = data[1:] 81 | return data 82 | 83 | def write_mpint1(self, n: int) -> 'WriteBuf': 84 | # NOTE: Data Type Enc @ http://www.snailbook.com/docs/protocol-1.5.txt 85 | bits = self._bitlength(n) 86 | data = self._create_mpint(n, False, bits) 87 | self.write(struct.pack('>H', bits)) 88 | return self.write(data) 89 | 90 | def write_mpint2(self, n: int) -> 'WriteBuf': 91 | # NOTE: Section 5 @ https://www.ietf.org/rfc/rfc4251.txt 92 | data = self._create_mpint(n) 93 | return self.write_string(data) 94 | 95 | def write_line(self, v: Union[bytes, str]) -> 'WriteBuf': 96 | if not isinstance(v, bytes): 97 | v = bytes(bytearray(v, 'utf-8')) 98 | v += b'\r\n' 99 | return self.write(v) 100 | 101 | def write_flush(self) -> bytes: 102 | payload = self._wbuf.getvalue() 103 | self._wbuf.truncate(0) 104 | self._wbuf.seek(0) 105 | return payload 106 | 107 | def reset(self) -> None: 108 | self._wbuf = io.BytesIO() 109 | -------------------------------------------------------------------------------- /ssh-audit.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """src/ssh_audit/ssh_audit.py wrapper for backwards compatibility""" 3 | 4 | import multiprocessing 5 | import sys 6 | import traceback 7 | from pathlib import Path 8 | 9 | sys.path.insert(0, str(Path(__file__).resolve().parent / "src")) 10 | 11 | from ssh_audit.ssh_audit import main # noqa: E402 12 | from ssh_audit import exitcodes # noqa: E402 13 | 14 | if __name__ == "__main__": 15 | multiprocessing.freeze_support() # Needed for PyInstaller (Windows) builds. 16 | 17 | exit_code = exitcodes.GOOD 18 | try: 19 | exit_code = main() 20 | except Exception: 21 | exit_code = exitcodes.UNKNOWN_ERROR 22 | print(traceback.format_exc()) 23 | 24 | sys.exit(exit_code) 25 | -------------------------------------------------------------------------------- /test/conftest.py: -------------------------------------------------------------------------------- 1 | import io 2 | import sys 3 | import socket 4 | import pytest 5 | 6 | 7 | @pytest.fixture(scope='module') 8 | def ssh_audit(): 9 | import ssh_audit.ssh_audit 10 | return ssh_audit.ssh_audit 11 | 12 | 13 | # pylint: disable=attribute-defined-outside-init 14 | class _OutputSpy(list): 15 | def begin(self): 16 | self.__out = io.StringIO() 17 | self.__old_stdout = sys.stdout 18 | sys.stdout = self.__out 19 | 20 | def flush(self): 21 | lines = self.__out.getvalue().splitlines() 22 | sys.stdout = self.__old_stdout 23 | self.__out = None 24 | return lines 25 | 26 | 27 | @pytest.fixture(scope='module') 28 | def output_spy(): 29 | return _OutputSpy() 30 | 31 | 32 | class _VirtualGlobalSocket: 33 | def __init__(self, vsocket): 34 | self.vsocket = vsocket 35 | self.addrinfodata = {} 36 | 37 | # pylint: disable=unused-argument 38 | def create_connection(self, address, timeout=0, source_address=None): 39 | # pylint: disable=protected-access 40 | return self.vsocket._connect(address, True) 41 | 42 | # pylint: disable=unused-argument 43 | def socket(self, 44 | family=socket.AF_INET, 45 | socktype=socket.SOCK_STREAM, 46 | proto=0, 47 | fileno=None): 48 | return self.vsocket 49 | 50 | def getaddrinfo(self, host, port, family=0, socktype=0, proto=0, flags=0): 51 | key = '{}#{}'.format(host, port) 52 | if key in self.addrinfodata: 53 | data = self.addrinfodata[key] 54 | if isinstance(data, Exception): 55 | raise data 56 | return data 57 | if host == 'localhost': 58 | r = [] 59 | if family in (0, socket.AF_INET): 60 | r.append((socket.AF_INET, 1, 6, '', ('127.0.0.1', port))) 61 | if family in (0, socket.AF_INET6): 62 | r.append((socket.AF_INET6, 1, 6, '', ('::1', port))) 63 | return r 64 | return [] 65 | 66 | 67 | class _VirtualSocket: 68 | def __init__(self): 69 | self.sock_address = ('127.0.0.1', 0) 70 | self.peer_address = None 71 | self._connected = False 72 | self.timeout = -1.0 73 | self.rdata = [] 74 | self.sdata = [] 75 | self.errors = {} 76 | self.blocking = False 77 | self.gsock = _VirtualGlobalSocket(self) 78 | 79 | def _check_err(self, method): 80 | method_error = self.errors.get(method) 81 | if method_error: 82 | raise method_error 83 | 84 | def connect(self, address): 85 | return self._connect(address, False) 86 | 87 | def connect_ex(self, address): 88 | return self.connect(address) 89 | 90 | def _connect(self, address, ret=True): 91 | self.peer_address = address 92 | self._connected = True 93 | self._check_err('connect') 94 | return self if ret else None 95 | 96 | def setblocking(self, r: bool): 97 | self.blocking = r 98 | 99 | def settimeout(self, timeout): 100 | self.timeout = timeout 101 | 102 | def gettimeout(self): 103 | return self.timeout 104 | 105 | def getpeername(self): 106 | if self.peer_address is None or not self._connected: 107 | raise OSError(57, 'Socket is not connected') 108 | return self.peer_address 109 | 110 | def getsockname(self): 111 | return self.sock_address 112 | 113 | def bind(self, address): 114 | self.sock_address = address 115 | 116 | def listen(self, backlog): 117 | pass 118 | 119 | def accept(self): 120 | # pylint: disable=protected-access 121 | conn = _VirtualSocket() 122 | conn.sock_address = self.sock_address 123 | conn.peer_address = ('127.0.0.1', 0) 124 | conn._connected = True 125 | return conn, conn.peer_address 126 | 127 | def recv(self, bufsize, flags=0): 128 | # pylint: disable=unused-argument 129 | if not self._connected: 130 | raise OSError(54, 'Connection reset by peer') 131 | if not len(self.rdata) > 0: 132 | return b'' 133 | data = self.rdata.pop(0) 134 | if isinstance(data, Exception): 135 | raise data 136 | return data 137 | 138 | def send(self, data): 139 | if self.peer_address is None or not self._connected: 140 | raise OSError(32, 'Broken pipe') 141 | self._check_err('send') 142 | self.sdata.append(data) 143 | 144 | 145 | @pytest.fixture() 146 | def virtual_socket(monkeypatch): 147 | vsocket = _VirtualSocket() 148 | gsock = vsocket.gsock 149 | monkeypatch.setattr(socket, 'create_connection', gsock.create_connection) 150 | monkeypatch.setattr(socket, 'socket', gsock.socket) 151 | monkeypatch.setattr(socket, 'getaddrinfo', gsock.getaddrinfo) 152 | return vsocket 153 | -------------------------------------------------------------------------------- /test/docker/.ed25519.sk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtesta/ssh-audit/b456bb31b977a9101a8b13e5f7137561c82f57ba/test/docker/.ed25519.sk -------------------------------------------------------------------------------- /test/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:16.04 2 | 3 | COPY openssh-4.0p1/sshd /openssh/sshd-4.0p1 4 | COPY openssh-5.6p1/sshd /openssh/sshd-5.6p1 5 | COPY openssh-8.0p1/sshd /openssh/sshd-8.0p1 6 | COPY dropbear-2019.78/dropbear /dropbear/dropbear-2019.78 7 | COPY tinyssh-20190101/build/bin/tinysshd /tinysshd/tinyssh-20190101 8 | 9 | # Dropbear host keys. 10 | COPY dropbear_*_host_key* /etc/dropbear/ 11 | 12 | # OpenSSH configs. 13 | COPY sshd_config* /etc/ssh/ 14 | 15 | # OpenSSH host keys & moduli file. 16 | COPY ssh_host_* /etc/ssh/ 17 | COPY ssh1_host_* /etc/ssh/ 18 | COPY moduli_1024 /usr/local/etc/moduli 19 | 20 | # TinySSH host keys. 21 | COPY ed25519.pk /etc/tinyssh/ 22 | COPY .ed25519.sk /etc/tinyssh/ 23 | 24 | COPY debug.sh /debug.sh 25 | 26 | RUN apt update 2> /dev/null 27 | RUN apt install -y libssl-dev strace rsyslog ucspi-tcp 2> /dev/null 28 | RUN apt clean 2> /dev/null 29 | RUN useradd -s /bin/false sshd 30 | RUN mkdir /var/empty 31 | 32 | EXPOSE 22 33 | -------------------------------------------------------------------------------- /test/docker/debug.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script is run on in docker container. It will enable logging for sshd in 4 | # /var/log/auth.log. 5 | 6 | /etc/init.d/rsyslog start 7 | sleep 1 8 | /openssh/sshd-5.6p1 -o LogLevel=DEBUG3 -f /etc/ssh/sshd_config-5.6p1_test1 9 | /bin/bash 10 | -------------------------------------------------------------------------------- /test/docker/dropbear_dss_host_key: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtesta/ssh-audit/b456bb31b977a9101a8b13e5f7137561c82f57ba/test/docker/dropbear_dss_host_key -------------------------------------------------------------------------------- /test/docker/dropbear_ecdsa_host_key: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtesta/ssh-audit/b456bb31b977a9101a8b13e5f7137561c82f57ba/test/docker/dropbear_ecdsa_host_key -------------------------------------------------------------------------------- /test/docker/dropbear_rsa_host_key_1024: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtesta/ssh-audit/b456bb31b977a9101a8b13e5f7137561c82f57ba/test/docker/dropbear_rsa_host_key_1024 -------------------------------------------------------------------------------- /test/docker/dropbear_rsa_host_key_3072: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtesta/ssh-audit/b456bb31b977a9101a8b13e5f7137561c82f57ba/test/docker/dropbear_rsa_host_key_3072 -------------------------------------------------------------------------------- /test/docker/ed25519.pk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtesta/ssh-audit/b456bb31b977a9101a8b13e5f7137561c82f57ba/test/docker/ed25519.pk -------------------------------------------------------------------------------- /test/docker/expected_results/openssh_5.6p1_custom_policy_test1.json: -------------------------------------------------------------------------------- 1 | { 2 | "errors": [], 3 | "host": "localhost", 4 | "passed": true, 5 | "policy": "Docker policy: test1 (version 1)", 6 | "port": 2222, 7 | "warnings": [] 8 | } 9 | -------------------------------------------------------------------------------- /test/docker/expected_results/openssh_5.6p1_custom_policy_test1.txt: -------------------------------------------------------------------------------- 1 | Host: localhost:2222 2 | Policy: Docker policy: test1 (version 1) 3 | Result: ✔ Passed 4 | -------------------------------------------------------------------------------- /test/docker/expected_results/openssh_5.6p1_custom_policy_test10.json: -------------------------------------------------------------------------------- 1 | { 2 | "errors": [ 3 | { 4 | "actual": [ 5 | "3072" 6 | ], 7 | "expected_optional": [ 8 | "" 9 | ], 10 | "expected_required": [ 11 | "4096" 12 | ], 13 | "mismatched_field": "Host key (ssh-rsa-cert-v01@openssh.com) sizes" 14 | }, 15 | { 16 | "actual": [ 17 | "1024" 18 | ], 19 | "expected_optional": [ 20 | "" 21 | ], 22 | "expected_required": [ 23 | "4096" 24 | ], 25 | "mismatched_field": "CA signature size (ssh-rsa)" 26 | } 27 | ], 28 | "host": "localhost", 29 | "passed": false, 30 | "policy": "Docker poliicy: test10 (version 1)", 31 | "port": 2222, 32 | "warnings": [] 33 | } 34 | -------------------------------------------------------------------------------- /test/docker/expected_results/openssh_5.6p1_custom_policy_test10.txt: -------------------------------------------------------------------------------- 1 | 2 | WARNING: this policy is using deprecated features. Future versions of ssh-audit may remove support for them. Re-generating the policy file is perhaps the most straight-forward way of resolving this issue. Manually converting the 'hostkey_size_*', 'cakey_size_*', and 'dh_modulus_size_*' directives into the new format is another option. 3 | 4 | 5 | WARNING: this policy is using deprecated features. Future versions of ssh-audit may remove support for them. Re-generating the policy file is perhaps the most straight-forward way of resolving this issue. Manually converting the 'hostkey_size_*', 'cakey_size_*', and 'dh_modulus_size_*' directives into the new format is another option. 6 | 7 | 8 | WARNING: this policy is using deprecated features. Future versions of ssh-audit may remove support for them. Re-generating the policy file is perhaps the most straight-forward way of resolving this issue. Manually converting the 'hostkey_size_*', 'cakey_size_*', and 'dh_modulus_size_*' directives into the new format is another option. 9 | 10 | 11 | WARNING: this policy is using deprecated features. Future versions of ssh-audit may remove support for them. Re-generating the policy file is perhaps the most straight-forward way of resolving this issue. Manually converting the 'hostkey_size_*', 'cakey_size_*', and 'dh_modulus_size_*' directives into the new format is another option. 12 | 13 | 14 | WARNING: this policy is using deprecated features. Future versions of ssh-audit may remove support for them. Re-generating the policy file is perhaps the most straight-forward way of resolving this issue. Manually converting the 'hostkey_size_*', 'cakey_size_*', and 'dh_modulus_size_*' directives into the new format is another option. 15 | 16 | Host: localhost:2222 17 | Policy: Docker poliicy: test10 (version 1) 18 | Result: ❌ Failed! 19 |  20 | Errors: 21 | * CA signature size (ssh-rsa) did not match. 22 | - Expected: 4096 23 | - Actual: 1024 24 | 25 | * Host key (ssh-rsa-cert-v01@openssh.com) sizes did not match. 26 | - Expected: 4096 27 | - Actual: 3072 28 |  29 | -------------------------------------------------------------------------------- /test/docker/expected_results/openssh_5.6p1_custom_policy_test2.json: -------------------------------------------------------------------------------- 1 | { 2 | "errors": [ 3 | { 4 | "actual": [ 5 | "diffie-hellman-group-exchange-sha256", 6 | "diffie-hellman-group-exchange-sha1", 7 | "diffie-hellman-group14-sha1", 8 | "diffie-hellman-group1-sha1" 9 | ], 10 | "expected_optional": [ 11 | "" 12 | ], 13 | "expected_required": [ 14 | "kex_alg1", 15 | "kex_alg2" 16 | ], 17 | "mismatched_field": "Key exchanges" 18 | } 19 | ], 20 | "host": "localhost", 21 | "passed": false, 22 | "policy": "Docker policy: test2 (version 1)", 23 | "port": 2222, 24 | "warnings": [] 25 | } 26 | -------------------------------------------------------------------------------- /test/docker/expected_results/openssh_5.6p1_custom_policy_test2.txt: -------------------------------------------------------------------------------- 1 | Host: localhost:2222 2 | Policy: Docker policy: test2 (version 1) 3 | Result: ❌ Failed! 4 |  5 | Errors: 6 | * Key exchanges did not match. 7 | - Expected: kex_alg1, kex_alg2 8 | - Actual: diffie-hellman-group-exchange-sha256, diffie-hellman-group-exchange-sha1, diffie-hellman-group14-sha1, diffie-hellman-group1-sha1 9 |  10 | -------------------------------------------------------------------------------- /test/docker/expected_results/openssh_5.6p1_custom_policy_test3.json: -------------------------------------------------------------------------------- 1 | { 2 | "errors": [ 3 | { 4 | "actual": [ 5 | "ssh-rsa", 6 | "ssh-dss" 7 | ], 8 | "expected_optional": [ 9 | "" 10 | ], 11 | "expected_required": [ 12 | "ssh-rsa", 13 | "ssh-dss", 14 | "key_alg1" 15 | ], 16 | "mismatched_field": "Host keys" 17 | } 18 | ], 19 | "host": "localhost", 20 | "passed": false, 21 | "policy": "Docker policy: test3 (version 1)", 22 | "port": 2222, 23 | "warnings": [] 24 | } 25 | -------------------------------------------------------------------------------- /test/docker/expected_results/openssh_5.6p1_custom_policy_test3.txt: -------------------------------------------------------------------------------- 1 | Host: localhost:2222 2 | Policy: Docker policy: test3 (version 1) 3 | Result: ❌ Failed! 4 |  5 | Errors: 6 | * Host keys did not match. 7 | - Expected: ssh-rsa, ssh-dss, key_alg1 8 | - Actual: ssh-rsa, ssh-dss 9 |  10 | -------------------------------------------------------------------------------- /test/docker/expected_results/openssh_5.6p1_custom_policy_test4.json: -------------------------------------------------------------------------------- 1 | { 2 | "errors": [ 3 | { 4 | "actual": [ 5 | "aes128-ctr", 6 | "aes192-ctr", 7 | "aes256-ctr", 8 | "arcfour256", 9 | "arcfour128", 10 | "aes128-cbc", 11 | "3des-cbc", 12 | "blowfish-cbc", 13 | "cast128-cbc", 14 | "aes192-cbc", 15 | "aes256-cbc", 16 | "arcfour", 17 | "rijndael-cbc@lysator.liu.se" 18 | ], 19 | "expected_optional": [ 20 | "" 21 | ], 22 | "expected_required": [ 23 | "cipher_alg1", 24 | "cipher_alg2" 25 | ], 26 | "mismatched_field": "Ciphers" 27 | } 28 | ], 29 | "host": "localhost", 30 | "passed": false, 31 | "policy": "Docker policy: test4 (version 1)", 32 | "port": 2222, 33 | "warnings": [] 34 | } 35 | -------------------------------------------------------------------------------- /test/docker/expected_results/openssh_5.6p1_custom_policy_test4.txt: -------------------------------------------------------------------------------- 1 | Host: localhost:2222 2 | Policy: Docker policy: test4 (version 1) 3 | Result: ❌ Failed! 4 |  5 | Errors: 6 | * Ciphers did not match. 7 | - Expected: cipher_alg1, cipher_alg2 8 | - Actual: aes128-ctr, aes192-ctr, aes256-ctr, arcfour256, arcfour128, aes128-cbc, 3des-cbc, blowfish-cbc, cast128-cbc, aes192-cbc, aes256-cbc, arcfour, rijndael-cbc@lysator.liu.se 9 |  10 | -------------------------------------------------------------------------------- /test/docker/expected_results/openssh_5.6p1_custom_policy_test5.json: -------------------------------------------------------------------------------- 1 | { 2 | "errors": [ 3 | { 4 | "actual": [ 5 | "hmac-md5", 6 | "hmac-sha1", 7 | "umac-64@openssh.com", 8 | "hmac-ripemd160", 9 | "hmac-ripemd160@openssh.com", 10 | "hmac-sha1-96", 11 | "hmac-md5-96" 12 | ], 13 | "expected_optional": [ 14 | "" 15 | ], 16 | "expected_required": [ 17 | "hmac-md5", 18 | "hmac-sha1", 19 | "umac-64@openssh.com", 20 | "hmac-ripemd160", 21 | "hmac-ripemd160@openssh.com", 22 | "hmac_alg1", 23 | "hmac-md5-96" 24 | ], 25 | "mismatched_field": "MACs" 26 | } 27 | ], 28 | "host": "localhost", 29 | "passed": false, 30 | "policy": "Docker policy: test5 (version 1)", 31 | "port": 2222, 32 | "warnings": [] 33 | } 34 | -------------------------------------------------------------------------------- /test/docker/expected_results/openssh_5.6p1_custom_policy_test5.txt: -------------------------------------------------------------------------------- 1 | Host: localhost:2222 2 | Policy: Docker policy: test5 (version 1) 3 | Result: ❌ Failed! 4 |  5 | Errors: 6 | * MACs did not match. 7 | - Expected: hmac-md5, hmac-sha1, umac-64@openssh.com, hmac-ripemd160, hmac-ripemd160@openssh.com, hmac_alg1, hmac-md5-96 8 | - Actual: hmac-md5, hmac-sha1, umac-64@openssh.com, hmac-ripemd160, hmac-ripemd160@openssh.com, hmac-sha1-96, hmac-md5-96 9 |  10 | -------------------------------------------------------------------------------- /test/docker/expected_results/openssh_5.6p1_custom_policy_test7.json: -------------------------------------------------------------------------------- 1 | { 2 | "errors": [], 3 | "host": "localhost", 4 | "passed": true, 5 | "policy": "Docker poliicy: test7 (version 1)", 6 | "port": 2222, 7 | "warnings": [] 8 | } 9 | -------------------------------------------------------------------------------- /test/docker/expected_results/openssh_5.6p1_custom_policy_test7.txt: -------------------------------------------------------------------------------- 1 | 2 | WARNING: this policy is using deprecated features. Future versions of ssh-audit may remove support for them. Re-generating the policy file is perhaps the most straight-forward way of resolving this issue. Manually converting the 'hostkey_size_*', 'cakey_size_*', and 'dh_modulus_size_*' directives into the new format is another option. 3 | 4 | 5 | WARNING: this policy is using deprecated features. Future versions of ssh-audit may remove support for them. Re-generating the policy file is perhaps the most straight-forward way of resolving this issue. Manually converting the 'hostkey_size_*', 'cakey_size_*', and 'dh_modulus_size_*' directives into the new format is another option. 6 | 7 | 8 | WARNING: this policy is using deprecated features. Future versions of ssh-audit may remove support for them. Re-generating the policy file is perhaps the most straight-forward way of resolving this issue. Manually converting the 'hostkey_size_*', 'cakey_size_*', and 'dh_modulus_size_*' directives into the new format is another option. 9 | 10 | 11 | WARNING: this policy is using deprecated features. Future versions of ssh-audit may remove support for them. Re-generating the policy file is perhaps the most straight-forward way of resolving this issue. Manually converting the 'hostkey_size_*', 'cakey_size_*', and 'dh_modulus_size_*' directives into the new format is another option. 12 | 13 | 14 | WARNING: this policy is using deprecated features. Future versions of ssh-audit may remove support for them. Re-generating the policy file is perhaps the most straight-forward way of resolving this issue. Manually converting the 'hostkey_size_*', 'cakey_size_*', and 'dh_modulus_size_*' directives into the new format is another option. 15 | 16 | Host: localhost:2222 17 | Policy: Docker poliicy: test7 (version 1) 18 | Result: ✔ Passed 19 | -------------------------------------------------------------------------------- /test/docker/expected_results/openssh_5.6p1_custom_policy_test8.json: -------------------------------------------------------------------------------- 1 | { 2 | "errors": [ 3 | { 4 | "actual": [ 5 | "1024" 6 | ], 7 | "expected_optional": [ 8 | "" 9 | ], 10 | "expected_required": [ 11 | "2048" 12 | ], 13 | "mismatched_field": "CA signature size (ssh-rsa)" 14 | } 15 | ], 16 | "host": "localhost", 17 | "passed": false, 18 | "policy": "Docker poliicy: test8 (version 1)", 19 | "port": 2222, 20 | "warnings": [] 21 | } 22 | -------------------------------------------------------------------------------- /test/docker/expected_results/openssh_5.6p1_custom_policy_test8.txt: -------------------------------------------------------------------------------- 1 | 2 | WARNING: this policy is using deprecated features. Future versions of ssh-audit may remove support for them. Re-generating the policy file is perhaps the most straight-forward way of resolving this issue. Manually converting the 'hostkey_size_*', 'cakey_size_*', and 'dh_modulus_size_*' directives into the new format is another option. 3 | 4 | 5 | WARNING: this policy is using deprecated features. Future versions of ssh-audit may remove support for them. Re-generating the policy file is perhaps the most straight-forward way of resolving this issue. Manually converting the 'hostkey_size_*', 'cakey_size_*', and 'dh_modulus_size_*' directives into the new format is another option. 6 | 7 | 8 | WARNING: this policy is using deprecated features. Future versions of ssh-audit may remove support for them. Re-generating the policy file is perhaps the most straight-forward way of resolving this issue. Manually converting the 'hostkey_size_*', 'cakey_size_*', and 'dh_modulus_size_*' directives into the new format is another option. 9 | 10 | 11 | WARNING: this policy is using deprecated features. Future versions of ssh-audit may remove support for them. Re-generating the policy file is perhaps the most straight-forward way of resolving this issue. Manually converting the 'hostkey_size_*', 'cakey_size_*', and 'dh_modulus_size_*' directives into the new format is another option. 12 | 13 | 14 | WARNING: this policy is using deprecated features. Future versions of ssh-audit may remove support for them. Re-generating the policy file is perhaps the most straight-forward way of resolving this issue. Manually converting the 'hostkey_size_*', 'cakey_size_*', and 'dh_modulus_size_*' directives into the new format is another option. 15 | 16 | Host: localhost:2222 17 | Policy: Docker poliicy: test8 (version 1) 18 | Result: ❌ Failed! 19 |  20 | Errors: 21 | * CA signature size (ssh-rsa) did not match. 22 | - Expected: 2048 23 | - Actual: 1024 24 |  25 | -------------------------------------------------------------------------------- /test/docker/expected_results/openssh_5.6p1_custom_policy_test9.json: -------------------------------------------------------------------------------- 1 | { 2 | "errors": [ 3 | { 4 | "actual": [ 5 | "3072" 6 | ], 7 | "expected_optional": [ 8 | "" 9 | ], 10 | "expected_required": [ 11 | "4096" 12 | ], 13 | "mismatched_field": "Host key (ssh-rsa-cert-v01@openssh.com) sizes" 14 | } 15 | ], 16 | "host": "localhost", 17 | "passed": false, 18 | "policy": "Docker poliicy: test9 (version 1)", 19 | "port": 2222, 20 | "warnings": [] 21 | } 22 | -------------------------------------------------------------------------------- /test/docker/expected_results/openssh_5.6p1_custom_policy_test9.txt: -------------------------------------------------------------------------------- 1 | 2 | WARNING: this policy is using deprecated features. Future versions of ssh-audit may remove support for them. Re-generating the policy file is perhaps the most straight-forward way of resolving this issue. Manually converting the 'hostkey_size_*', 'cakey_size_*', and 'dh_modulus_size_*' directives into the new format is another option. 3 | 4 | 5 | WARNING: this policy is using deprecated features. Future versions of ssh-audit may remove support for them. Re-generating the policy file is perhaps the most straight-forward way of resolving this issue. Manually converting the 'hostkey_size_*', 'cakey_size_*', and 'dh_modulus_size_*' directives into the new format is another option. 6 | 7 | 8 | WARNING: this policy is using deprecated features. Future versions of ssh-audit may remove support for them. Re-generating the policy file is perhaps the most straight-forward way of resolving this issue. Manually converting the 'hostkey_size_*', 'cakey_size_*', and 'dh_modulus_size_*' directives into the new format is another option. 9 | 10 | 11 | WARNING: this policy is using deprecated features. Future versions of ssh-audit may remove support for them. Re-generating the policy file is perhaps the most straight-forward way of resolving this issue. Manually converting the 'hostkey_size_*', 'cakey_size_*', and 'dh_modulus_size_*' directives into the new format is another option. 12 | 13 | 14 | WARNING: this policy is using deprecated features. Future versions of ssh-audit may remove support for them. Re-generating the policy file is perhaps the most straight-forward way of resolving this issue. Manually converting the 'hostkey_size_*', 'cakey_size_*', and 'dh_modulus_size_*' directives into the new format is another option. 15 | 16 | Host: localhost:2222 17 | Policy: Docker poliicy: test9 (version 1) 18 | Result: ❌ Failed! 19 |  20 | Errors: 21 | * Host key (ssh-rsa-cert-v01@openssh.com) sizes did not match. 22 | - Expected: 4096 23 | - Actual: 3072 24 |  25 | -------------------------------------------------------------------------------- /test/docker/expected_results/openssh_8.0p1_builtin_policy_test1.json: -------------------------------------------------------------------------------- 1 | { 2 | "errors": [ 3 | { 4 | "actual": [ 5 | "3072" 6 | ], 7 | "expected_optional": [ 8 | "" 9 | ], 10 | "expected_required": [ 11 | "4096" 12 | ], 13 | "mismatched_field": "Host key (rsa-sha2-256) sizes" 14 | }, 15 | { 16 | "actual": [ 17 | "3072" 18 | ], 19 | "expected_optional": [ 20 | "" 21 | ], 22 | "expected_required": [ 23 | "4096" 24 | ], 25 | "mismatched_field": "Host key (rsa-sha2-512) sizes" 26 | }, 27 | { 28 | "actual": [ 29 | "4096" 30 | ], 31 | "expected_optional": [ 32 | "" 33 | ], 34 | "expected_required": [ 35 | "3072" 36 | ], 37 | "mismatched_field": "Group exchange (diffie-hellman-group-exchange-sha256) modulus sizes" 38 | } 39 | ], 40 | "host": "localhost", 41 | "passed": false, 42 | "policy": "Hardened OpenSSH Server v8.0 (version 4)", 43 | "port": 2222, 44 | "warnings": [] 45 | } 46 | -------------------------------------------------------------------------------- /test/docker/expected_results/openssh_8.0p1_builtin_policy_test1.txt: -------------------------------------------------------------------------------- 1 | Host: localhost:2222 2 | Policy: Hardened OpenSSH Server v8.0 (version 4) 3 | Result: ❌ Failed! 4 |  5 | Errors: 6 | * Group exchange (diffie-hellman-group-exchange-sha256) modulus sizes did not match. 7 | - Expected: 3072 8 | - Actual: 4096 9 | 10 | * Host key (rsa-sha2-256) sizes did not match. 11 | - Expected: 4096 12 | - Actual: 3072 13 | 14 | * Host key (rsa-sha2-512) sizes did not match. 15 | - Expected: 4096 16 | - Actual: 3072 17 |  18 | -------------------------------------------------------------------------------- /test/docker/expected_results/openssh_8.0p1_builtin_policy_test2.json: -------------------------------------------------------------------------------- 1 | { 2 | "errors": [ 3 | { 4 | "actual": [ 5 | "3072" 6 | ], 7 | "expected_optional": [ 8 | "" 9 | ], 10 | "expected_required": [ 11 | "4096" 12 | ], 13 | "mismatched_field": "Host key (rsa-sha2-256) sizes" 14 | }, 15 | { 16 | "actual": [ 17 | "3072" 18 | ], 19 | "expected_optional": [ 20 | "" 21 | ], 22 | "expected_required": [ 23 | "4096" 24 | ], 25 | "mismatched_field": "Host key (rsa-sha2-512) sizes" 26 | }, 27 | { 28 | "actual": [ 29 | "umac-64-etm@openssh.com", 30 | "umac-128-etm@openssh.com", 31 | "hmac-sha2-256-etm@openssh.com", 32 | "hmac-sha2-512-etm@openssh.com", 33 | "hmac-sha1-etm@openssh.com", 34 | "umac-64@openssh.com", 35 | "umac-128@openssh.com", 36 | "hmac-sha2-256", 37 | "hmac-sha2-512", 38 | "hmac-sha1" 39 | ], 40 | "expected_optional": [ 41 | "" 42 | ], 43 | "expected_required": [ 44 | "hmac-sha2-256-etm@openssh.com", 45 | "hmac-sha2-512-etm@openssh.com", 46 | "umac-128-etm@openssh.com" 47 | ], 48 | "mismatched_field": "MACs" 49 | }, 50 | { 51 | "actual": [ 52 | "4096" 53 | ], 54 | "expected_optional": [ 55 | "" 56 | ], 57 | "expected_required": [ 58 | "3072" 59 | ], 60 | "mismatched_field": "Group exchange (diffie-hellman-group-exchange-sha256) modulus sizes" 61 | } 62 | ], 63 | "host": "localhost", 64 | "passed": false, 65 | "policy": "Hardened OpenSSH Server v8.0 (version 4)", 66 | "port": 2222, 67 | "warnings": [] 68 | } 69 | -------------------------------------------------------------------------------- /test/docker/expected_results/openssh_8.0p1_builtin_policy_test2.txt: -------------------------------------------------------------------------------- 1 | Host: localhost:2222 2 | Policy: Hardened OpenSSH Server v8.0 (version 4) 3 | Result: ❌ Failed! 4 |  5 | Errors: 6 | * Group exchange (diffie-hellman-group-exchange-sha256) modulus sizes did not match. 7 | - Expected: 3072 8 | - Actual: 4096 9 | 10 | * Host key (rsa-sha2-256) sizes did not match. 11 | - Expected: 4096 12 | - Actual: 3072 13 | 14 | * Host key (rsa-sha2-512) sizes did not match. 15 | - Expected: 4096 16 | - Actual: 3072 17 | 18 | * MACs did not match. 19 | - Expected: hmac-sha2-256-etm@openssh.com, hmac-sha2-512-etm@openssh.com, umac-128-etm@openssh.com 20 | - Actual: umac-64-etm@openssh.com, umac-128-etm@openssh.com, hmac-sha2-256-etm@openssh.com, hmac-sha2-512-etm@openssh.com, hmac-sha1-etm@openssh.com, umac-64@openssh.com, umac-128@openssh.com, hmac-sha2-256, hmac-sha2-512, hmac-sha1 21 |  22 | -------------------------------------------------------------------------------- /test/docker/expected_results/openssh_8.0p1_custom_policy_test11.json: -------------------------------------------------------------------------------- 1 | { 2 | "errors": [], 3 | "host": "localhost", 4 | "passed": true, 5 | "policy": "Docker policy: test11 (version 1)", 6 | "port": 2222, 7 | "warnings": [] 8 | } 9 | -------------------------------------------------------------------------------- /test/docker/expected_results/openssh_8.0p1_custom_policy_test11.txt: -------------------------------------------------------------------------------- 1 | 2 | WARNING: this policy is using deprecated features. Future versions of ssh-audit may remove support for them. Re-generating the policy file is perhaps the most straight-forward way of resolving this issue. Manually converting the 'hostkey_size_*', 'cakey_size_*', and 'dh_modulus_size_*' directives into the new format is another option. 3 | 4 | 5 | WARNING: this policy is using deprecated features. Future versions of ssh-audit may remove support for them. Re-generating the policy file is perhaps the most straight-forward way of resolving this issue. Manually converting the 'hostkey_size_*', 'cakey_size_*', and 'dh_modulus_size_*' directives into the new format is another option. 6 | 7 | 8 | WARNING: this policy is using deprecated features. Future versions of ssh-audit may remove support for them. Re-generating the policy file is perhaps the most straight-forward way of resolving this issue. Manually converting the 'hostkey_size_*', 'cakey_size_*', and 'dh_modulus_size_*' directives into the new format is another option. 9 | 10 | Host: localhost:2222 11 | Policy: Docker policy: test11 (version 1) 12 | Result: ✔ Passed 13 | -------------------------------------------------------------------------------- /test/docker/expected_results/openssh_8.0p1_custom_policy_test12.json: -------------------------------------------------------------------------------- 1 | { 2 | "errors": [ 3 | { 4 | "actual": [ 5 | "3072" 6 | ], 7 | "expected_optional": [ 8 | "" 9 | ], 10 | "expected_required": [ 11 | "4096" 12 | ], 13 | "mismatched_field": "Host key (rsa-sha2-256) sizes" 14 | }, 15 | { 16 | "actual": [ 17 | "3072" 18 | ], 19 | "expected_optional": [ 20 | "" 21 | ], 22 | "expected_required": [ 23 | "4096" 24 | ], 25 | "mismatched_field": "Host key (rsa-sha2-512) sizes" 26 | }, 27 | { 28 | "actual": [ 29 | "3072" 30 | ], 31 | "expected_optional": [ 32 | "" 33 | ], 34 | "expected_required": [ 35 | "4096" 36 | ], 37 | "mismatched_field": "Host key (ssh-rsa) sizes" 38 | } 39 | ], 40 | "host": "localhost", 41 | "passed": false, 42 | "policy": "Docker policy: test12 (version 1)", 43 | "port": 2222, 44 | "warnings": [] 45 | } 46 | -------------------------------------------------------------------------------- /test/docker/expected_results/openssh_8.0p1_custom_policy_test12.txt: -------------------------------------------------------------------------------- 1 | 2 | WARNING: this policy is using deprecated features. Future versions of ssh-audit may remove support for them. Re-generating the policy file is perhaps the most straight-forward way of resolving this issue. Manually converting the 'hostkey_size_*', 'cakey_size_*', and 'dh_modulus_size_*' directives into the new format is another option. 3 | 4 | 5 | WARNING: this policy is using deprecated features. Future versions of ssh-audit may remove support for them. Re-generating the policy file is perhaps the most straight-forward way of resolving this issue. Manually converting the 'hostkey_size_*', 'cakey_size_*', and 'dh_modulus_size_*' directives into the new format is another option. 6 | 7 | 8 | WARNING: this policy is using deprecated features. Future versions of ssh-audit may remove support for them. Re-generating the policy file is perhaps the most straight-forward way of resolving this issue. Manually converting the 'hostkey_size_*', 'cakey_size_*', and 'dh_modulus_size_*' directives into the new format is another option. 9 | 10 | Host: localhost:2222 11 | Policy: Docker policy: test12 (version 1) 12 | Result: ❌ Failed! 13 |  14 | Errors: 15 | * Host key (rsa-sha2-256) sizes did not match. 16 | - Expected: 4096 17 | - Actual: 3072 18 | 19 | * Host key (rsa-sha2-512) sizes did not match. 20 | - Expected: 4096 21 | - Actual: 3072 22 | 23 | * Host key (ssh-rsa) sizes did not match. 24 | - Expected: 4096 25 | - Actual: 3072 26 |  27 | -------------------------------------------------------------------------------- /test/docker/expected_results/openssh_8.0p1_custom_policy_test13.json: -------------------------------------------------------------------------------- 1 | { 2 | "errors": [], 3 | "host": "localhost", 4 | "passed": true, 5 | "policy": "Docker policy: test13 (version 1)", 6 | "port": 2222, 7 | "warnings": [] 8 | } 9 | -------------------------------------------------------------------------------- /test/docker/expected_results/openssh_8.0p1_custom_policy_test13.txt: -------------------------------------------------------------------------------- 1 | 2 | WARNING: this policy is using deprecated features. Future versions of ssh-audit may remove support for them. Re-generating the policy file is perhaps the most straight-forward way of resolving this issue. Manually converting the 'hostkey_size_*', 'cakey_size_*', and 'dh_modulus_size_*' directives into the new format is another option. 3 | 4 | 5 | WARNING: this policy is using deprecated features. Future versions of ssh-audit may remove support for them. Re-generating the policy file is perhaps the most straight-forward way of resolving this issue. Manually converting the 'hostkey_size_*', 'cakey_size_*', and 'dh_modulus_size_*' directives into the new format is another option. 6 | 7 | 8 | WARNING: this policy is using deprecated features. Future versions of ssh-audit may remove support for them. Re-generating the policy file is perhaps the most straight-forward way of resolving this issue. Manually converting the 'hostkey_size_*', 'cakey_size_*', and 'dh_modulus_size_*' directives into the new format is another option. 9 | 10 | 11 | WARNING: this policy is using deprecated features. Future versions of ssh-audit may remove support for them. Re-generating the policy file is perhaps the most straight-forward way of resolving this issue. Manually converting the 'hostkey_size_*', 'cakey_size_*', and 'dh_modulus_size_*' directives into the new format is another option. 12 | 13 | Host: localhost:2222 14 | Policy: Docker policy: test13 (version 1) 15 | Result: ✔ Passed 16 | -------------------------------------------------------------------------------- /test/docker/expected_results/openssh_8.0p1_custom_policy_test14.json: -------------------------------------------------------------------------------- 1 | { 2 | "errors": [ 3 | { 4 | "actual": [ 5 | "4096" 6 | ], 7 | "expected_optional": [ 8 | "" 9 | ], 10 | "expected_required": [ 11 | "8192" 12 | ], 13 | "mismatched_field": "Group exchange (diffie-hellman-group-exchange-sha256) modulus sizes" 14 | } 15 | ], 16 | "host": "localhost", 17 | "passed": false, 18 | "policy": "Docker policy: test14 (version 1)", 19 | "port": 2222, 20 | "warnings": [] 21 | } 22 | -------------------------------------------------------------------------------- /test/docker/expected_results/openssh_8.0p1_custom_policy_test14.txt: -------------------------------------------------------------------------------- 1 | 2 | WARNING: this policy is using deprecated features. Future versions of ssh-audit may remove support for them. Re-generating the policy file is perhaps the most straight-forward way of resolving this issue. Manually converting the 'hostkey_size_*', 'cakey_size_*', and 'dh_modulus_size_*' directives into the new format is another option. 3 | 4 | 5 | WARNING: this policy is using deprecated features. Future versions of ssh-audit may remove support for them. Re-generating the policy file is perhaps the most straight-forward way of resolving this issue. Manually converting the 'hostkey_size_*', 'cakey_size_*', and 'dh_modulus_size_*' directives into the new format is another option. 6 | 7 | 8 | WARNING: this policy is using deprecated features. Future versions of ssh-audit may remove support for them. Re-generating the policy file is perhaps the most straight-forward way of resolving this issue. Manually converting the 'hostkey_size_*', 'cakey_size_*', and 'dh_modulus_size_*' directives into the new format is another option. 9 | 10 | 11 | WARNING: this policy is using deprecated features. Future versions of ssh-audit may remove support for them. Re-generating the policy file is perhaps the most straight-forward way of resolving this issue. Manually converting the 'hostkey_size_*', 'cakey_size_*', and 'dh_modulus_size_*' directives into the new format is another option. 12 | 13 | Host: localhost:2222 14 | Policy: Docker policy: test14 (version 1) 15 | Result: ❌ Failed! 16 |  17 | Errors: 18 | * Group exchange (diffie-hellman-group-exchange-sha256) modulus sizes did not match. 19 | - Expected: 8192 20 | - Actual: 4096 21 |  22 | -------------------------------------------------------------------------------- /test/docker/expected_results/openssh_8.0p1_custom_policy_test15.json: -------------------------------------------------------------------------------- 1 | { 2 | "errors": [], 3 | "host": "localhost", 4 | "passed": true, 5 | "policy": "Docker policy: test15 (version 1)", 6 | "port": 2222, 7 | "warnings": [] 8 | } 9 | -------------------------------------------------------------------------------- /test/docker/expected_results/openssh_8.0p1_custom_policy_test15.txt: -------------------------------------------------------------------------------- 1 | Host: localhost:2222 2 | Policy: Docker policy: test15 (version 1) 3 | Result: ✔ Passed 4 | -------------------------------------------------------------------------------- /test/docker/expected_results/openssh_8.0p1_custom_policy_test16.json: -------------------------------------------------------------------------------- 1 | { 2 | "errors": [ 3 | { 4 | "actual": [ 5 | "rsa-sha2-512", 6 | "rsa-sha2-256", 7 | "ssh-rsa", 8 | "ecdsa-sha2-nistp256", 9 | "ssh-ed25519" 10 | ], 11 | "expected_optional": [ 12 | "" 13 | ], 14 | "expected_required": [ 15 | "rsa-sha2-512", 16 | "extra_hostkey_alg" 17 | ], 18 | "mismatched_field": "Host keys" 19 | }, 20 | { 21 | "actual": [ 22 | "curve25519-sha256", 23 | "curve25519-sha256@libssh.org", 24 | "ecdh-sha2-nistp256", 25 | "ecdh-sha2-nistp384", 26 | "ecdh-sha2-nistp521", 27 | "diffie-hellman-group-exchange-sha256", 28 | "diffie-hellman-group16-sha512", 29 | "diffie-hellman-group18-sha512", 30 | "diffie-hellman-group14-sha256", 31 | "diffie-hellman-group14-sha1" 32 | ], 33 | "expected_optional": [ 34 | "" 35 | ], 36 | "expected_required": [ 37 | "curve25519-sha256", 38 | "extra_kex_alg" 39 | ], 40 | "mismatched_field": "Key exchanges" 41 | }, 42 | { 43 | "actual": [ 44 | "chacha20-poly1305@openssh.com", 45 | "aes128-ctr", 46 | "aes192-ctr", 47 | "aes256-ctr", 48 | "aes128-gcm@openssh.com", 49 | "aes256-gcm@openssh.com" 50 | ], 51 | "expected_optional": [ 52 | "" 53 | ], 54 | "expected_required": [ 55 | "chacha20-poly1305@openssh.com", 56 | "extra_cipher_alg" 57 | ], 58 | "mismatched_field": "Ciphers" 59 | }, 60 | { 61 | "actual": [ 62 | "umac-64-etm@openssh.com", 63 | "umac-128-etm@openssh.com", 64 | "hmac-sha2-256-etm@openssh.com", 65 | "hmac-sha2-512-etm@openssh.com", 66 | "hmac-sha1-etm@openssh.com", 67 | "umac-64@openssh.com", 68 | "umac-128@openssh.com", 69 | "hmac-sha2-256", 70 | "hmac-sha2-512", 71 | "hmac-sha1" 72 | ], 73 | "expected_optional": [ 74 | "" 75 | ], 76 | "expected_required": [ 77 | "umac-64-etm@openssh.com", 78 | "extra_mac_alg" 79 | ], 80 | "mismatched_field": "MACs" 81 | } 82 | ], 83 | "host": "localhost", 84 | "passed": false, 85 | "policy": "Docker policy: test16 (version 1)", 86 | "port": 2222, 87 | "warnings": [] 88 | } 89 | -------------------------------------------------------------------------------- /test/docker/expected_results/openssh_8.0p1_custom_policy_test16.txt: -------------------------------------------------------------------------------- 1 | Host: localhost:2222 2 | Policy: Docker policy: test16 (version 1) 3 | Result: ❌ Failed! 4 |  5 | Errors: 6 | * Ciphers did not match. 7 | - Expected (subset and/or reordering allowed): chacha20-poly1305@openssh.com, extra_cipher_alg 8 | - Actual: chacha20-poly1305@openssh.com, aes128-ctr, aes192-ctr, aes256-ctr, aes128-gcm@openssh.com, aes256-gcm@openssh.com 9 | 10 | * Host keys did not match. 11 | - Expected (subset and/or reordering allowed): rsa-sha2-512, extra_hostkey_alg 12 | - Actual: rsa-sha2-512, rsa-sha2-256, ssh-rsa, ecdsa-sha2-nistp256, ssh-ed25519 13 | 14 | * Key exchanges did not match. 15 | - Expected (subset and/or reordering allowed): curve25519-sha256, extra_kex_alg 16 | - Actual: curve25519-sha256, curve25519-sha256@libssh.org, ecdh-sha2-nistp256, ecdh-sha2-nistp384, ecdh-sha2-nistp521, diffie-hellman-group-exchange-sha256, diffie-hellman-group16-sha512, diffie-hellman-group18-sha512, diffie-hellman-group14-sha256, diffie-hellman-group14-sha1 17 | 18 | * MACs did not match. 19 | - Expected (subset and/or reordering allowed): umac-64-etm@openssh.com, extra_mac_alg 20 | - Actual: umac-64-etm@openssh.com, umac-128-etm@openssh.com, hmac-sha2-256-etm@openssh.com, hmac-sha2-512-etm@openssh.com, hmac-sha1-etm@openssh.com, umac-64@openssh.com, umac-128@openssh.com, hmac-sha2-256, hmac-sha2-512, hmac-sha1 21 |  22 | -------------------------------------------------------------------------------- /test/docker/expected_results/openssh_8.0p1_custom_policy_test17.json: -------------------------------------------------------------------------------- 1 | { 2 | "errors": [], 3 | "host": "localhost", 4 | "passed": true, 5 | "policy": "Docker policy: test17 (version 1)", 6 | "port": 2222, 7 | "warnings": [] 8 | } 9 | -------------------------------------------------------------------------------- /test/docker/expected_results/openssh_8.0p1_custom_policy_test17.txt: -------------------------------------------------------------------------------- 1 | Host: localhost:2222 2 | Policy: Docker policy: test17 (version 1) 3 | Result: ✔ Passed 4 | -------------------------------------------------------------------------------- /test/docker/expected_results/openssh_8.0p1_custom_policy_test6.json: -------------------------------------------------------------------------------- 1 | { 2 | "errors": [], 3 | "host": "localhost", 4 | "passed": true, 5 | "policy": "Docker policy: test6 (version 1)", 6 | "port": 2222, 7 | "warnings": [] 8 | } 9 | -------------------------------------------------------------------------------- /test/docker/expected_results/openssh_8.0p1_custom_policy_test6.txt: -------------------------------------------------------------------------------- 1 | Host: localhost:2222 2 | Policy: Docker policy: test6 (version 1) 3 | Result: ✔ Passed 4 | -------------------------------------------------------------------------------- /test/docker/expected_results/openssh_8.0p1_test3.txt: -------------------------------------------------------------------------------- 1 | # general 2 | (gen) banner: SSH-2.0-OpenSSH_8.0 3 | (gen) software: OpenSSH 8.0 4 | (gen) compatibility: OpenSSH 7.4+, Dropbear SSH 2020.79+ 5 | (gen) compression: enabled (zlib@openssh.com) 6 | 7 | # key exchange algorithms 8 | (kex) curve25519-sha256 -- [warn] does not provide protection against post-quantum attacks 9 | `- [info] available since OpenSSH 7.4, Dropbear SSH 2018.76 10 | `- [info] default key exchange from OpenSSH 7.4 to 8.9 11 | (kex) curve25519-sha256@libssh.org -- [warn] does not provide protection against post-quantum attacks 12 | `- [info] available since OpenSSH 6.4, Dropbear SSH 2013.62 13 | `- [info] default key exchange from OpenSSH 6.5 to 7.3 14 | (kex) diffie-hellman-group-exchange-sha256 (4096-bit) -- [warn] does not provide protection against post-quantum attacks 15 | `- [info] available since OpenSSH 4.4 16 | `- [info] OpenSSH's GEX fallback mechanism was triggered during testing. Very old SSH clients will still be able to create connections using a 2048-bit modulus, though modern clients will use 4096. This can only be disabled by recompiling the code (see https://github.com/openssh/openssh-portable/blob/V_9_4/dh.c#L477). 17 | 18 | # host-key algorithms 19 | (key) ssh-ed25519 -- [info] available since OpenSSH 6.5, Dropbear SSH 2020.79 20 | 21 | # encryption algorithms (ciphers) 22 | (enc) chacha20-poly1305@openssh.com -- [warn] vulnerable to the Terrapin attack (CVE-2023-48795), allowing message prefix truncation 23 | `- [info] available since OpenSSH 6.5, Dropbear SSH 2020.79 24 | `- [info] default cipher since OpenSSH 6.9 25 | (enc) aes256-gcm@openssh.com -- [info] available since OpenSSH 6.2 26 | (enc) aes128-gcm@openssh.com -- [info] available since OpenSSH 6.2 27 | (enc) aes256-ctr -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52 28 | (enc) aes192-ctr -- [info] available since OpenSSH 3.7 29 | (enc) aes128-ctr -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52 30 | 31 | # message authentication code algorithms 32 | (mac) hmac-sha2-256-etm@openssh.com -- [info] available since OpenSSH 6.2 33 | (mac) hmac-sha2-512-etm@openssh.com -- [info] available since OpenSSH 6.2 34 | (mac) umac-128-etm@openssh.com -- [info] available since OpenSSH 6.2 35 | 36 | # fingerprints 37 | (fin) ssh-ed25519: SHA256:UrnXIVH+7dlw8UqYocl48yUEcKrthGDQG2CPCgp7MxU 38 | 39 | # algorithm recommendations (for OpenSSH 8.0) 40 | (rec) +rsa-sha2-256 -- key algorithm to append  41 | (rec) +rsa-sha2-512 -- key algorithm to append  42 | (rec) !diffie-hellman-group-exchange-sha256 -- kex algorithm to change (increase modulus size to 3072 bits or larger)  43 | (rec) -chacha20-poly1305@openssh.com -- enc algorithm to remove  44 | (rec) -curve25519-sha256 -- kex algorithm to remove  45 | (rec) -curve25519-sha256@libssh.org -- kex algorithm to remove  46 | 47 | # additional info 48 | (nfo) For hardening guides on common OSes, please see:  49 | 50 | -------------------------------------------------------------------------------- /test/docker/expected_results/tinyssh_20190101_test1.json: -------------------------------------------------------------------------------- 1 | { 2 | "additional_notes": [], 3 | "banner": { 4 | "comments": "", 5 | "protocol": "2.0", 6 | "raw": "", 7 | "software": "tinyssh_noversion" 8 | }, 9 | "compression": [ 10 | "none" 11 | ], 12 | "cves": [], 13 | "enc": [ 14 | { 15 | "algorithm": "chacha20-poly1305@openssh.com", 16 | "notes": { 17 | "info": [ 18 | "default cipher since OpenSSH 6.9", 19 | "available since OpenSSH 6.5, Dropbear SSH 2020.79" 20 | ], 21 | "warn": [ 22 | "vulnerable to the Terrapin attack (CVE-2023-48795), allowing message prefix truncation" 23 | ] 24 | } 25 | } 26 | ], 27 | "fingerprints": [ 28 | { 29 | "hash": "89ocln1x7KNqnMgWffGoYtD70ksJ4FrH7BMJHa7SrwU", 30 | "hash_alg": "SHA256", 31 | "hostkey": "ssh-ed25519" 32 | }, 33 | { 34 | "hash": "dd:9c:6d:f9:b0:8c:af:fa:c2:65:81:5d:5d:56:f8:21", 35 | "hash_alg": "MD5", 36 | "hostkey": "ssh-ed25519" 37 | } 38 | ], 39 | "kex": [ 40 | { 41 | "algorithm": "curve25519-sha256", 42 | "notes": { 43 | "info": [ 44 | "default key exchange from OpenSSH 7.4 to 8.9", 45 | "available since OpenSSH 7.4, Dropbear SSH 2018.76" 46 | ], 47 | "warn": [ 48 | "does not provide protection against post-quantum attacks" 49 | ] 50 | } 51 | }, 52 | { 53 | "algorithm": "curve25519-sha256@libssh.org", 54 | "notes": { 55 | "info": [ 56 | "default key exchange from OpenSSH 6.5 to 7.3", 57 | "available since OpenSSH 6.4, Dropbear SSH 2013.62" 58 | ], 59 | "warn": [ 60 | "does not provide protection against post-quantum attacks" 61 | ] 62 | } 63 | }, 64 | { 65 | "algorithm": "sntrup4591761x25519-sha512@tinyssh.org", 66 | "notes": { 67 | "info": [ 68 | "the sntrup4591761 algorithm was withdrawn, as it may not provide strong post-quantum security", 69 | "available since OpenSSH 8.0" 70 | ], 71 | "warn": [ 72 | "using experimental algorithm" 73 | ] 74 | } 75 | } 76 | ], 77 | "key": [ 78 | { 79 | "algorithm": "ssh-ed25519", 80 | "notes": { 81 | "info": [ 82 | "available since OpenSSH 6.5, Dropbear SSH 2020.79" 83 | ] 84 | } 85 | } 86 | ], 87 | "mac": [ 88 | { 89 | "algorithm": "hmac-sha2-256", 90 | "notes": { 91 | "info": [ 92 | "available since OpenSSH 5.9, Dropbear SSH 2013.56" 93 | ], 94 | "warn": [ 95 | "using encrypt-and-MAC mode" 96 | ] 97 | } 98 | } 99 | ], 100 | "recommendations": {}, 101 | "target": "localhost:2222" 102 | } 103 | -------------------------------------------------------------------------------- /test/docker/expected_results/tinyssh_20190101_test1.txt: -------------------------------------------------------------------------------- 1 | # general 2 | (gen) software: TinySSH noversion 3 | (gen) compatibility: OpenSSH 8.0-8.4, Dropbear SSH 2020.79+ 4 | (gen) compression: disabled 5 | 6 | # key exchange algorithms 7 | (kex) curve25519-sha256 -- [warn] does not provide protection against post-quantum attacks 8 | `- [info] available since OpenSSH 7.4, Dropbear SSH 2018.76 9 | `- [info] default key exchange from OpenSSH 7.4 to 8.9 10 | (kex) curve25519-sha256@libssh.org -- [warn] does not provide protection against post-quantum attacks 11 | `- [info] available since OpenSSH 6.4, Dropbear SSH 2013.62 12 | `- [info] default key exchange from OpenSSH 6.5 to 7.3 13 | (kex) sntrup4591761x25519-sha512@tinyssh.org -- [warn] using experimental algorithm 14 | `- [info] available since OpenSSH 8.0 15 | `- [info] the sntrup4591761 algorithm was withdrawn, as it may not provide strong post-quantum security 16 | 17 | # host-key algorithms 18 | (key) ssh-ed25519 -- [info] available since OpenSSH 6.5, Dropbear SSH 2020.79 19 | 20 | # encryption algorithms (ciphers) 21 | (enc) chacha20-poly1305@openssh.com -- [warn] vulnerable to the Terrapin attack (CVE-2023-48795), allowing message prefix truncation 22 | `- [info] available since OpenSSH 6.5, Dropbear SSH 2020.79 23 | `- [info] default cipher since OpenSSH 6.9 24 | 25 | # message authentication code algorithms 26 | (mac) hmac-sha2-256 -- [warn] using encrypt-and-MAC mode 27 | `- [info] available since OpenSSH 5.9, Dropbear SSH 2013.56 28 | 29 | # fingerprints 30 | (fin) ssh-ed25519: SHA256:89ocln1x7KNqnMgWffGoYtD70ksJ4FrH7BMJHa7SrwU 31 | 32 | -------------------------------------------------------------------------------- /test/docker/host_ca_ed25519: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW 3 | QyNTUxOQAAACAbM9Wp3ZPcC8Ifhu6GjNDJaoMg7KxO0el2+r9J35TltQAAAKAa0zr8GtM6 4 | /AAAAAtzc2gtZWQyNTUxOQAAACAbM9Wp3ZPcC8Ifhu6GjNDJaoMg7KxO0el2+r9J35TltQ 5 | AAAEC/j/BpfmgaZqNMTkJXO4cKZBr31N5z33IRFjh5m6IDDhsz1andk9wLwh+G7oaM0Mlq 6 | gyDsrE7R6Xb6v0nflOW1AAAAHWpkb2dAbG9jYWxob3N0LndvbmRlcmxhbmQubG9s 7 | -----END OPENSSH PRIVATE KEY----- 8 | -------------------------------------------------------------------------------- /test/docker/host_ca_ed25519.pub: -------------------------------------------------------------------------------- 1 | ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBsz1andk9wLwh+G7oaM0MlqgyDsrE7R6Xb6v0nflOW1 jdog@localhost.wonderland.lol 2 | -------------------------------------------------------------------------------- /test/docker/host_ca_rsa_1024: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXgIBAAKBgQDnRlN3AFnUe2lFf5XG9UhXLr/9POruNTFbMt0zrjOUSjmAS7hS 3 | 6pDv5VEToT6DaR1EQUYaqSMpHYzZhuCK52vrydOm5XFbJ7712r9MyZQUhoVZx8Su 4 | dBHzVDIVO3jcMMWIlrfWBMnUaUHEqpmy88Y7gKDa2TWxJg1+hg51KqHrUQIDAQAB 5 | AoGBANALOUXRcP1tTtOP4+In/709dsONKyDBhPavGMFGsWtyIavBcbxU+bBzrq1j 6 | 3WJFCmi99xxAjjqMNInxhMgvSaoJtsiY0/FFxqRy6l/ZnRjI6hrVKR8whrPKVgBF 7 | pvbjeQIn9txeCYA8kwl/Si762u7byq+qvupE53xMP94J02KBAkEA/Q4+Hn1Rjblw 8 | VXynF+oXIq6iZy+8PW+Y/FIL8d31ehzfcssCMdFV6S3/wBoQkWby30oGC/xGmHGR 9 | 6ffXGilByQJBAOn3NMrBPXNkaPeQtgV3tk4s1dRDQYhbqGNz6tcgThyyPdhJCmCy 10 | jgUEhLwAetsDI8/+3avWbo6/csOV+BvpYUkCQQDQyEp6L1z0+FV1QqY99dZmt/yn 11 | 89t0OLnZG/xc7osU1/OHq3TBE3y1KU2D+j1HKdAiZ9l7VAYOykzf46qmG/n5AkEA 12 | 2kWjfcjcIIw7lULvXZh6fuI7NwTr3V/Nb8MUA1EDLqhnJCG4SdAqyKmXf6Fe/HYo 13 | cgKPIaIykIAxfCCsULXg6QJAOxB0CKYJlopVBdjGMlGqOEneWTmb1A2INQDE2Una 14 | LkSd0Rr8OiEzDeemV7j3Ec4BH0HxGMnHDxMybZwoZRnRPw== 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /test/docker/host_ca_rsa_1024.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDnRlN3AFnUe2lFf5XG9UhXLr/9POruNTFbMt0zrjOUSjmAS7hS6pDv5VEToT6DaR1EQUYaqSMpHYzZhuCK52vrydOm5XFbJ7712r9MyZQUhoVZx8SudBHzVDIVO3jcMMWIlrfWBMnUaUHEqpmy88Y7gKDa2TWxJg1+hg51KqHrUQ== jdog@localhost.wonderland.lol 2 | -------------------------------------------------------------------------------- /test/docker/host_ca_rsa_3072: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIG4wIBAAKCAYEAqxQEIbj8w0TrBY1fDO81curijQrdLOUr8Vl8XECWc5QGd1Lk 3 | AG80NgdcCBPvjWxZSmYrKeqA78GUdN+KgycE0ztpxYSXKHZMaIM5Xe94BB+BocH9 4 | 1vd/2iBzGeed1nV/zfAdq2AEHQj1TpII+a+z25yxv2PuwVTTwwo9I/6JgNq3evH4 5 | Hbwgr3SRfEEYZQ+YL8cOpBuNg1YZOR0k1yk23ZqAd92JybxZ4iCtOt7rcj2sFHzN 6 | u1U544wWBwIL5yZZKTgBhY4dqfT2Ep7IzR5HdsdrvQV9qC92GM1zDE+U3AwrVKjH 7 | s0YZq3jzcq/yvFDCcMMRz4/0pGFFU26oWma+n3vbAxKJoL+rhG8QM9+l2qFlLGsn 8 | M0kUXAJXsPKbygpaP8Z3U4eKgTuJ2GuS9eLIFnB7mrwD75V6GgN9q5mY89DfkVSk 9 | HaoqpY8pPdRkz9QAmMEuLtHmv29CVOpfX5v/rsm7wASAZqtUlmFu4rFGBLwvZbUl 10 | Wu02HmgBT47g6EIfAgMBAAECggGAKVCdKtO03yd+pomcodAHFWiaK7uq7FOwCAo3 11 | WUQT0Xe3FAwFmgFBF6cxV5YQ7RN0gN4poGbMmpoiUxNFLSU4KhcYFSZPJutiyn6e 12 | VQwm7L/7G2hw+AAvdSsPAPuJh6g6pC5Py/pVI/ns2/uyhTIkem3eEz18BF6LAXgw 13 | icfHx0GKu/tBk1TCg/zfwaUq0gUxGKC27XTl+QjK8JsUMY33fQ755Xiv9PMytcR0 14 | cVoyfBVewFffi1UqtMQ48ZpR65G743RxrP4/wcwsfD7n5LJLdyxQkh3gIMTJ8dd/ 15 | R5V4FlueorRgjTbLTjGDxNrCAJ+locezhEEPXsPh2q0KiIXGyz2AMxaOqFmhU8oK 16 | aVVt8pWJ+YsrKIgc/A3s18ezO8uO5ZdtjQ+CWguduUGY7YgWezGLO1LPxhJC4d7b 17 | Q/xpeKveTRlcScAqOUzKgSuEhcvPgj8paUcRUoiXm4qiJBY5sXJks+YGp8BGksH0 18 | O94no+Ns2G58MlL+RyXk3JWrc6zRAoHBANdPplY2sIuIiiEBu95f1Qar1nCBHhB2 19 | i+HpnsUOdSlbxwMxoF8ffeN9N+DQqaqPu1RhFa5xbB2EUSujvOnL7b/RWqe1X9Po 20 | UIt5UjXctNP/HYcQDyjXY+rV5SZhHDyv6TBYurNZlvlBivliDz82THPRtqVxed3B 21 | w2MeaSkKAQ8rA7PE+0j3TG+YtIij0mHOhNPJgEZ/XZ9MIQOGMycRJhwOlclBI5NP 22 | Ak6p30ArnU2fX4qMkU3i+wqUfXS1hhDihwKBwQDLaHWPIWPVbWdcCbYQTcUmFC3i 23 | xkxd0UuLcfS9csk61nvdFj7m8tMExX+3fIo/fHEtzDd98Alc1i6/f6ePl0CX6NDu 24 | QIWLryI1QQRQidHCdw0wQ3N3VD4ZXJHDeqBxogVAkA7A/1QeXwcXE/Xj2ZgyDwhL 25 | 3+myjmvWtw9zJsXL0F3tpPzn+Mrf0KRkWOaluOw7hMMjVjrgu6g24HMWbHHVLRTx 26 | dlAI7tgxCAPe2SEi+1mzaVUZ8cfgqYqC3X66UakCgcEAopxtK7+yJi/A4pzEnnYS 27 | FS/CjMV3R0fA7aXbW0hIBCxkaW0Zib3m/eCcSxZMjZxwBpIsJctTtBcylprbGlgB 28 | /1TF+tNoxEo4Sp4eEL/XciTC0Da4vEewFrPklM/S26KfovvgRYPsGeP+aco9aahA 29 | pVhFcT36pBiq0DkvgucjValO6n5iqgDboYzbDDdttKCcgLc2Qgf/VUfRxy+bgm3Z 30 | MmdxiMXBcIfDXlW9XmGSNAWhyqnPM9uxbZQoC/Tsg+QRAoHANHMcFSsz9f2+8DGk 31 | 27FiC76aUmZ1nJ9yTmO1CwDFOMHDsK+iyqSEmy9eDm8zqsko2flVuciicWjdJw4A 32 | o/sJceJbtYO3q9weAwNf3HCdQPq30OEjrfpwBNQk1fYR1xtDJXHADC4Kf8ZbKq0/ 33 | 81/Rad8McZwsQ5mL3xLXDgdKa5KwFa48dIhnr6y6JxHxb3wule5W7w62Ierhpjzc 34 | EEUoWSLFyrmKS7Ni1cnOTbFJZR7Q831Or2Dz/E9bYwFAQ0T5AoHAM4/zU+8rsbdD 35 | FvvhWsj7Ivfh6pxx1Tl1Wccaauea9AJayHht0FOzkycpJrH1E+6F5MzhkFFU1SUY 36 | 60NZxzSZgbU0HBrJRcRFyo510iMcnctdTdyh8p7nweGoD0oqXzf6cHqrUep8Y8rQ 37 | gkSVhPE31+NGlPbwz+NOflcaaAWYiDC6wjVt1asaZq292SJD4DF1fAUkbQ2hxgyQ 38 | +G/6y5ovrcGnh7q63RLhW1TRf8dD2D2Av9UgXDmWZAZ5n838FS+X 39 | -----END RSA PRIVATE KEY----- 40 | -------------------------------------------------------------------------------- /test/docker/host_ca_rsa_3072.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCrFAQhuPzDROsFjV8M7zVy6uKNCt0s5SvxWXxcQJZzlAZ3UuQAbzQ2B1wIE++NbFlKZisp6oDvwZR034qDJwTTO2nFhJcodkxogzld73gEH4Ghwf3W93/aIHMZ553WdX/N8B2rYAQdCPVOkgj5r7PbnLG/Y+7BVNPDCj0j/omA2rd68fgdvCCvdJF8QRhlD5gvxw6kG42DVhk5HSTXKTbdmoB33YnJvFniIK063utyPawUfM27VTnjjBYHAgvnJlkpOAGFjh2p9PYSnsjNHkd2x2u9BX2oL3YYzXMMT5TcDCtUqMezRhmrePNyr/K8UMJwwxHPj/SkYUVTbqhaZr6fe9sDEomgv6uEbxAz36XaoWUsayczSRRcAlew8pvKClo/xndTh4qBO4nYa5L14sgWcHuavAPvlXoaA32rmZjz0N+RVKQdqiqljyk91GTP1ACYwS4u0ea/b0JU6l9fm/+uybvABIBmq1SWYW7isUYEvC9ltSVa7TYeaAFPjuDoQh8= jdog@localhost.wonderland.lol 2 | -------------------------------------------------------------------------------- /test/docker/policies/policy_test1.txt: -------------------------------------------------------------------------------- 1 | # 2 | # Docker policy: test1 3 | # 4 | 5 | name = "Docker policy: test1" 6 | version = 1 7 | host keys = ssh-rsa, ssh-dss 8 | key exchanges = diffie-hellman-group-exchange-sha256, diffie-hellman-group-exchange-sha1, diffie-hellman-group14-sha1, diffie-hellman-group1-sha1 9 | ciphers = aes128-ctr, aes192-ctr, aes256-ctr, arcfour256, arcfour128, aes128-cbc, 3des-cbc, blowfish-cbc, cast128-cbc, aes192-cbc, aes256-cbc, arcfour, rijndael-cbc@lysator.liu.se 10 | macs = hmac-md5, hmac-sha1, umac-64@openssh.com, hmac-ripemd160, hmac-ripemd160@openssh.com, hmac-sha1-96, hmac-md5-96 11 | -------------------------------------------------------------------------------- /test/docker/policies/policy_test10.txt: -------------------------------------------------------------------------------- 1 | # 2 | # Docker policy: test10 3 | # 4 | 5 | # The name of this policy (displayed in the output during scans). Must be in quotes. 6 | name = "Docker poliicy: test10" 7 | 8 | # The version of this policy (displayed in the output during scans). Not parsed, and may be any value, including strings. 9 | version = 1 10 | 11 | # The banner that must match exactly. Commented out to ignore banners, since minor variability in the banner is sometimes normal. 12 | # banner = "SSH-2.0-OpenSSH_5.6" 13 | 14 | # The header that must match exactly. Commented out to ignore headers, since variability in the header is sometimes normal. 15 | # header = "[]" 16 | 17 | # The compression options that must match exactly (order matters). Commented out to ignore by default. 18 | # compressions = none, zlib@openssh.com 19 | 20 | # RSA host key sizes. 21 | hostkey_size_rsa-sha2-256 = 3072 22 | hostkey_size_rsa-sha2-512 = 3072 23 | hostkey_size_ssh-rsa = 3072 24 | hostkey_size_ssh-rsa-cert-v01@openssh.com = 4096 25 | 26 | # RSA CA key sizes. 27 | cakey_size_ssh-rsa-cert-v01@openssh.com = 4096 28 | 29 | # The host key types that must match exactly (order matters). 30 | host keys = ssh-rsa, ssh-rsa-cert-v01@openssh.com 31 | 32 | # The key exchange algorithms that must match exactly (order matters). 33 | key exchanges = diffie-hellman-group-exchange-sha256, diffie-hellman-group-exchange-sha1, diffie-hellman-group14-sha1, diffie-hellman-group1-sha1 34 | 35 | # The ciphers that must match exactly (order matters). 36 | ciphers = aes128-ctr, aes192-ctr, aes256-ctr, arcfour256, arcfour128, aes128-cbc, 3des-cbc, blowfish-cbc, cast128-cbc, aes192-cbc, aes256-cbc, arcfour, rijndael-cbc@lysator.liu.se 37 | 38 | # The MACs that must match exactly (order matters). 39 | macs = hmac-md5, hmac-sha1, umac-64@openssh.com, hmac-ripemd160, hmac-ripemd160@openssh.com, hmac-sha1-96, hmac-md5-96 40 | -------------------------------------------------------------------------------- /test/docker/policies/policy_test11.txt: -------------------------------------------------------------------------------- 1 | # 2 | # Docker policy: test11 3 | # 4 | 5 | # The name of this policy (displayed in the output during scans). Must be in quotes. 6 | name = "Docker policy: test11" 7 | 8 | # The version of this policy (displayed in the output during scans). Not parsed, and may be any value, including strings. 9 | version = 1 10 | 11 | # The banner that must match exactly. Commented out to ignore banners, since minor variability in the banner is sometimes normal. 12 | # banner = "SSH-2.0-OpenSSH_8.0" 13 | 14 | # The header that must match exactly. Commented out to ignore headers, since variability in the header is sometimes normal. 15 | # header = "[]" 16 | 17 | # The compression options that must match exactly (order matters). Commented out to ignore by default. 18 | # compressions = none, zlib@openssh.com 19 | 20 | # RSA host key sizes. 21 | hostkey_size_rsa-sha2-256 = 3072 22 | hostkey_size_rsa-sha2-512 = 3072 23 | hostkey_size_ssh-rsa = 3072 24 | 25 | # The host key types that must match exactly (order matters). 26 | host keys = rsa-sha2-512, rsa-sha2-256, ssh-rsa, ecdsa-sha2-nistp256, ssh-ed25519 27 | 28 | # The key exchange algorithms that must match exactly (order matters). 29 | key exchanges = curve25519-sha256, curve25519-sha256@libssh.org, ecdh-sha2-nistp256, ecdh-sha2-nistp384, ecdh-sha2-nistp521, diffie-hellman-group-exchange-sha256, diffie-hellman-group16-sha512, diffie-hellman-group18-sha512, diffie-hellman-group14-sha256, diffie-hellman-group14-sha1 30 | 31 | # The ciphers that must match exactly (order matters). 32 | ciphers = chacha20-poly1305@openssh.com, aes128-ctr, aes192-ctr, aes256-ctr, aes128-gcm@openssh.com, aes256-gcm@openssh.com 33 | 34 | # The MACs that must match exactly (order matters). 35 | macs = umac-64-etm@openssh.com, umac-128-etm@openssh.com, hmac-sha2-256-etm@openssh.com, hmac-sha2-512-etm@openssh.com, hmac-sha1-etm@openssh.com, umac-64@openssh.com, umac-128@openssh.com, hmac-sha2-256, hmac-sha2-512, hmac-sha1 36 | -------------------------------------------------------------------------------- /test/docker/policies/policy_test12.txt: -------------------------------------------------------------------------------- 1 | # 2 | # Docker policy: test12 3 | # 4 | 5 | # The name of this policy (displayed in the output during scans). Must be in quotes. 6 | name = "Docker policy: test12" 7 | 8 | # The version of this policy (displayed in the output during scans). Not parsed, and may be any value, including strings. 9 | version = 1 10 | 11 | # The banner that must match exactly. Commented out to ignore banners, since minor variability in the banner is sometimes normal. 12 | # banner = "SSH-2.0-OpenSSH_8.0" 13 | 14 | # The header that must match exactly. Commented out to ignore headers, since variability in the header is sometimes normal. 15 | # header = "[]" 16 | 17 | # The compression options that must match exactly (order matters). Commented out to ignore by default. 18 | # compressions = none, zlib@openssh.com 19 | 20 | # RSA host key sizes. 21 | hostkey_size_rsa-sha2-256 = 4096 22 | hostkey_size_rsa-sha2-512 = 4096 23 | hostkey_size_ssh-rsa = 4096 24 | 25 | # The host key types that must match exactly (order matters). 26 | host keys = rsa-sha2-512, rsa-sha2-256, ssh-rsa, ecdsa-sha2-nistp256, ssh-ed25519 27 | 28 | # The key exchange algorithms that must match exactly (order matters). 29 | key exchanges = curve25519-sha256, curve25519-sha256@libssh.org, ecdh-sha2-nistp256, ecdh-sha2-nistp384, ecdh-sha2-nistp521, diffie-hellman-group-exchange-sha256, diffie-hellman-group16-sha512, diffie-hellman-group18-sha512, diffie-hellman-group14-sha256, diffie-hellman-group14-sha1 30 | 31 | # The ciphers that must match exactly (order matters). 32 | ciphers = chacha20-poly1305@openssh.com, aes128-ctr, aes192-ctr, aes256-ctr, aes128-gcm@openssh.com, aes256-gcm@openssh.com 33 | 34 | # The MACs that must match exactly (order matters). 35 | macs = umac-64-etm@openssh.com, umac-128-etm@openssh.com, hmac-sha2-256-etm@openssh.com, hmac-sha2-512-etm@openssh.com, hmac-sha1-etm@openssh.com, umac-64@openssh.com, umac-128@openssh.com, hmac-sha2-256, hmac-sha2-512, hmac-sha1 36 | -------------------------------------------------------------------------------- /test/docker/policies/policy_test13.txt: -------------------------------------------------------------------------------- 1 | # 2 | # Docker policy: test13 3 | # 4 | 5 | # The name of this policy (displayed in the output during scans). Must be in quotes. 6 | name = "Docker policy: test13" 7 | 8 | # The version of this policy (displayed in the output during scans). Not parsed, and may be any value, including strings. 9 | version = 1 10 | 11 | # The banner that must match exactly. Commented out to ignore banners, since minor variability in the banner is sometimes normal. 12 | # banner = "SSH-2.0-OpenSSH_8.0" 13 | 14 | # The header that must match exactly. Commented out to ignore headers, since variability in the header is sometimes normal. 15 | # header = "[]" 16 | 17 | # The compression options that must match exactly (order matters). Commented out to ignore by default. 18 | # compressions = none, zlib@openssh.com 19 | 20 | # RSA host key sizes. 21 | hostkey_size_rsa-sha2-256 = 3072 22 | hostkey_size_rsa-sha2-512 = 3072 23 | hostkey_size_ssh-rsa = 3072 24 | 25 | # Group exchange DH modulus sizes. 26 | dh_modulus_size_diffie-hellman-group-exchange-sha256 = 4096 27 | 28 | # The host key types that must match exactly (order matters). 29 | host keys = rsa-sha2-512, rsa-sha2-256, ssh-rsa, ecdsa-sha2-nistp256, ssh-ed25519 30 | 31 | # The key exchange algorithms that must match exactly (order matters). 32 | key exchanges = curve25519-sha256, curve25519-sha256@libssh.org, ecdh-sha2-nistp256, ecdh-sha2-nistp384, ecdh-sha2-nistp521, diffie-hellman-group-exchange-sha256, diffie-hellman-group16-sha512, diffie-hellman-group18-sha512, diffie-hellman-group14-sha256, diffie-hellman-group14-sha1 33 | 34 | # The ciphers that must match exactly (order matters). 35 | ciphers = chacha20-poly1305@openssh.com, aes128-ctr, aes192-ctr, aes256-ctr, aes128-gcm@openssh.com, aes256-gcm@openssh.com 36 | 37 | # The MACs that must match exactly (order matters). 38 | macs = umac-64-etm@openssh.com, umac-128-etm@openssh.com, hmac-sha2-256-etm@openssh.com, hmac-sha2-512-etm@openssh.com, hmac-sha1-etm@openssh.com, umac-64@openssh.com, umac-128@openssh.com, hmac-sha2-256, hmac-sha2-512, hmac-sha1 39 | -------------------------------------------------------------------------------- /test/docker/policies/policy_test14.txt: -------------------------------------------------------------------------------- 1 | # 2 | # Docker policy: test14 3 | # 4 | 5 | # The name of this policy (displayed in the output during scans). Must be in quotes. 6 | name = "Docker policy: test14" 7 | 8 | # The version of this policy (displayed in the output during scans). Not parsed, and may be any value, including strings. 9 | version = 1 10 | 11 | # The banner that must match exactly. Commented out to ignore banners, since minor variability in the banner is sometimes normal. 12 | # banner = "SSH-2.0-OpenSSH_8.0" 13 | 14 | # The header that must match exactly. Commented out to ignore headers, since variability in the header is sometimes normal. 15 | # header = "[]" 16 | 17 | # The compression options that must match exactly (order matters). Commented out to ignore by default. 18 | # compressions = none, zlib@openssh.com 19 | 20 | # RSA host key sizes. 21 | hostkey_size_rsa-sha2-256 = 3072 22 | hostkey_size_rsa-sha2-512 = 3072 23 | hostkey_size_ssh-rsa = 3072 24 | 25 | # Group exchange DH modulus sizes. 26 | dh_modulus_size_diffie-hellman-group-exchange-sha256 = 8192 27 | 28 | # The host key types that must match exactly (order matters). 29 | host keys = rsa-sha2-512, rsa-sha2-256, ssh-rsa, ecdsa-sha2-nistp256, ssh-ed25519 30 | 31 | # The key exchange algorithms that must match exactly (order matters). 32 | key exchanges = curve25519-sha256, curve25519-sha256@libssh.org, ecdh-sha2-nistp256, ecdh-sha2-nistp384, ecdh-sha2-nistp521, diffie-hellman-group-exchange-sha256, diffie-hellman-group16-sha512, diffie-hellman-group18-sha512, diffie-hellman-group14-sha256, diffie-hellman-group14-sha1 33 | 34 | # The ciphers that must match exactly (order matters). 35 | ciphers = chacha20-poly1305@openssh.com, aes128-ctr, aes192-ctr, aes256-ctr, aes128-gcm@openssh.com, aes256-gcm@openssh.com 36 | 37 | # The MACs that must match exactly (order matters). 38 | macs = umac-64-etm@openssh.com, umac-128-etm@openssh.com, hmac-sha2-256-etm@openssh.com, hmac-sha2-512-etm@openssh.com, hmac-sha1-etm@openssh.com, umac-64@openssh.com, umac-128@openssh.com, hmac-sha2-256, hmac-sha2-512, hmac-sha1 39 | -------------------------------------------------------------------------------- /test/docker/policies/policy_test15.txt: -------------------------------------------------------------------------------- 1 | # 2 | # Docker policy: test15 3 | # 4 | 5 | name = "Docker policy: test15" 6 | version = 1 7 | allow_algorithm_subset_and_reordering = true 8 | banner = "SSH-2.0-OpenSSH_8.0" 9 | compressions = none, zlib@openssh.com 10 | host keys = rsa-sha2-512, rsa-sha2-256, ssh-rsa, ecdsa-sha2-nistp256, ssh-ed25519, extra_hostkey_alg 11 | key exchanges = curve25519-sha256, curve25519-sha256@libssh.org, ecdh-sha2-nistp256, ecdh-sha2-nistp384, ecdh-sha2-nistp521, diffie-hellman-group-exchange-sha256, diffie-hellman-group16-sha512, diffie-hellman-group18-sha512, diffie-hellman-group14-sha256, diffie-hellman-group14-sha1, extra_kex_alg 12 | ciphers = chacha20-poly1305@openssh.com, aes128-ctr, aes192-ctr, aes256-ctr, aes128-gcm@openssh.com, aes256-gcm@openssh.com, extra_cipher_alg 13 | macs = umac-64-etm@openssh.com, umac-128-etm@openssh.com, hmac-sha2-256-etm@openssh.com, hmac-sha2-512-etm@openssh.com, hmac-sha1-etm@openssh.com, umac-64@openssh.com, umac-128@openssh.com, hmac-sha2-256, hmac-sha2-512, hmac-sha1, extra_mac_alg 14 | -------------------------------------------------------------------------------- /test/docker/policies/policy_test16.txt: -------------------------------------------------------------------------------- 1 | # 2 | # Docker policy: test16 3 | # 4 | 5 | name = "Docker policy: test16" 6 | version = 1 7 | allow_algorithm_subset_and_reordering = true 8 | banner = "SSH-2.0-OpenSSH_8.0" 9 | compressions = none, zlib@openssh.com 10 | host keys = rsa-sha2-512, extra_hostkey_alg 11 | key exchanges = curve25519-sha256, extra_kex_alg 12 | ciphers = chacha20-poly1305@openssh.com, extra_cipher_alg 13 | macs = umac-64-etm@openssh.com, extra_mac_alg 14 | -------------------------------------------------------------------------------- /test/docker/policies/policy_test17.txt: -------------------------------------------------------------------------------- 1 | # 2 | # Docker policy: test17 3 | # 4 | 5 | name = "Docker policy: test17" 6 | version = 1 7 | allow_larger_keys = true 8 | banner = "SSH-2.0-OpenSSH_8.0" 9 | compressions = none, zlib@openssh.com 10 | host keys = rsa-sha2-512, rsa-sha2-256, ssh-rsa, ecdsa-sha2-nistp256, ssh-ed25519 11 | key exchanges = curve25519-sha256, curve25519-sha256@libssh.org, ecdh-sha2-nistp256, ecdh-sha2-nistp384, ecdh-sha2-nistp521, diffie-hellman-group-exchange-sha256, diffie-hellman-group16-sha512, diffie-hellman-group18-sha512, diffie-hellman-group14-sha256, diffie-hellman-group14-sha1 12 | ciphers = chacha20-poly1305@openssh.com, aes128-ctr, aes192-ctr, aes256-ctr, aes128-gcm@openssh.com, aes256-gcm@openssh.com 13 | macs = umac-64-etm@openssh.com, umac-128-etm@openssh.com, hmac-sha2-256-etm@openssh.com, hmac-sha2-512-etm@openssh.com, hmac-sha1-etm@openssh.com, umac-64@openssh.com, umac-128@openssh.com, hmac-sha2-256, hmac-sha2-512, hmac-sha1 14 | host_key_sizes = {"ssh-rsa": {"hostkey_size": 2048}, "rsa-sha2-256": {"hostkey_size": 2048}, "rsa-sha2-512": {"hostkey_size": 2048}, "ssh-ed25519": {"hostkey_size": 256}} 15 | dh_modulus_sizes = {"diffie-hellman-group-exchange-sha256": 2048} 16 | -------------------------------------------------------------------------------- /test/docker/policies/policy_test2.txt: -------------------------------------------------------------------------------- 1 | # 2 | # Docker policy: test2 3 | # 4 | 5 | name = "Docker policy: test2" 6 | version = 1 7 | host keys = ssh-rsa, ssh-dss 8 | key exchanges = kex_alg1, kex_alg2 9 | ciphers = aes128-ctr, aes192-ctr, aes256-ctr, arcfour256, arcfour128, aes128-cbc, 3des-cbc, blowfish-cbc, cast128-cbc, aes192-cbc, aes256-cbc, arcfour, rijndael-cbc@lysator.liu.se 10 | macs = hmac-md5, hmac-sha1, umac-64@openssh.com, hmac-ripemd160, hmac-ripemd160@openssh.com, hmac-sha1-96, hmac-md5-96 11 | -------------------------------------------------------------------------------- /test/docker/policies/policy_test3.txt: -------------------------------------------------------------------------------- 1 | # 2 | # Docker policy: test3 3 | # 4 | 5 | name = "Docker policy: test3" 6 | version = 1 7 | host keys = ssh-rsa, ssh-dss, key_alg1 8 | key exchanges = diffie-hellman-group-exchange-sha256, diffie-hellman-group-exchange-sha1, diffie-hellman-group14-sha1, diffie-hellman-group1-sha1 9 | ciphers = aes128-ctr, aes192-ctr, aes256-ctr, arcfour256, arcfour128, aes128-cbc, 3des-cbc, blowfish-cbc, cast128-cbc, aes192-cbc, aes256-cbc, arcfour, rijndael-cbc@lysator.liu.se 10 | macs = hmac-md5, hmac-sha1, umac-64@openssh.com, hmac-ripemd160, hmac-ripemd160@openssh.com, hmac-sha1-96, hmac-md5-96 11 | -------------------------------------------------------------------------------- /test/docker/policies/policy_test4.txt: -------------------------------------------------------------------------------- 1 | # 2 | # Docker policy: test4 3 | # 4 | 5 | name = "Docker policy: test4" 6 | version = 1 7 | host keys = ssh-rsa, ssh-dss 8 | key exchanges = diffie-hellman-group-exchange-sha256, diffie-hellman-group-exchange-sha1, diffie-hellman-group14-sha1, diffie-hellman-group1-sha1 9 | ciphers = cipher_alg1, cipher_alg2 10 | macs = hmac-md5, hmac-sha1, umac-64@openssh.com, hmac-ripemd160, hmac-ripemd160@openssh.com, hmac-sha1-96, hmac-md5-96 11 | -------------------------------------------------------------------------------- /test/docker/policies/policy_test5.txt: -------------------------------------------------------------------------------- 1 | # 2 | # Docker policy: test5 3 | # 4 | 5 | name = "Docker policy: test5" 6 | version = 1 7 | host keys = ssh-rsa, ssh-dss 8 | key exchanges = diffie-hellman-group-exchange-sha256, diffie-hellman-group-exchange-sha1, diffie-hellman-group14-sha1, diffie-hellman-group1-sha1 9 | ciphers = aes128-ctr, aes192-ctr, aes256-ctr, arcfour256, arcfour128, aes128-cbc, 3des-cbc, blowfish-cbc, cast128-cbc, aes192-cbc, aes256-cbc, arcfour, rijndael-cbc@lysator.liu.se 10 | macs = hmac-md5, hmac-sha1, umac-64@openssh.com, hmac-ripemd160, hmac-ripemd160@openssh.com, hmac_alg1, hmac-md5-96 11 | -------------------------------------------------------------------------------- /test/docker/policies/policy_test6.txt: -------------------------------------------------------------------------------- 1 | # 2 | # Docker policy: test6 3 | # 4 | 5 | name = "Docker policy: test6" 6 | version = 1 7 | banner = "SSH-2.0-OpenSSH_8.0" 8 | compressions = none, zlib@openssh.com 9 | host keys = rsa-sha2-512, rsa-sha2-256, ssh-rsa, ecdsa-sha2-nistp256, ssh-ed25519 10 | key exchanges = curve25519-sha256, curve25519-sha256@libssh.org, ecdh-sha2-nistp256, ecdh-sha2-nistp384, ecdh-sha2-nistp521, diffie-hellman-group-exchange-sha256, diffie-hellman-group16-sha512, diffie-hellman-group18-sha512, diffie-hellman-group14-sha256, diffie-hellman-group14-sha1 11 | ciphers = chacha20-poly1305@openssh.com, aes128-ctr, aes192-ctr, aes256-ctr, aes128-gcm@openssh.com, aes256-gcm@openssh.com 12 | macs = umac-64-etm@openssh.com, umac-128-etm@openssh.com, hmac-sha2-256-etm@openssh.com, hmac-sha2-512-etm@openssh.com, hmac-sha1-etm@openssh.com, umac-64@openssh.com, umac-128@openssh.com, hmac-sha2-256, hmac-sha2-512, hmac-sha1 13 | -------------------------------------------------------------------------------- /test/docker/policies/policy_test7.txt: -------------------------------------------------------------------------------- 1 | # 2 | # Docker policy: test7 3 | # 4 | 5 | # The name of this policy (displayed in the output during scans). Must be in quotes. 6 | name = "Docker poliicy: test7" 7 | 8 | # The version of this policy (displayed in the output during scans). Not parsed, and may be any value, including strings. 9 | version = 1 10 | 11 | # The banner that must match exactly. Commented out to ignore banners, since minor variability in the banner is sometimes normal. 12 | # banner = "SSH-2.0-OpenSSH_5.6" 13 | 14 | # The header that must match exactly. Commented out to ignore headers, since variability in the header is sometimes normal. 15 | # header = "[]" 16 | 17 | # The compression options that must match exactly (order matters). Commented out to ignore by default. 18 | # compressions = none, zlib@openssh.com 19 | 20 | # RSA host key sizes. 21 | hostkey_size_rsa-sha2-256 = 3072 22 | hostkey_size_rsa-sha2-512 = 3072 23 | hostkey_size_ssh-rsa = 3072 24 | hostkey_size_ssh-rsa-cert-v01@openssh.com = 3072 25 | 26 | # RSA CA key sizes. 27 | cakey_size_ssh-rsa-cert-v01@openssh.com = 1024 28 | 29 | # The host key types that must match exactly (order matters). 30 | host keys = ssh-rsa, ssh-rsa-cert-v01@openssh.com 31 | 32 | # The key exchange algorithms that must match exactly (order matters). 33 | key exchanges = diffie-hellman-group-exchange-sha256, diffie-hellman-group-exchange-sha1, diffie-hellman-group14-sha1, diffie-hellman-group1-sha1 34 | 35 | # The ciphers that must match exactly (order matters). 36 | ciphers = aes128-ctr, aes192-ctr, aes256-ctr, arcfour256, arcfour128, aes128-cbc, 3des-cbc, blowfish-cbc, cast128-cbc, aes192-cbc, aes256-cbc, arcfour, rijndael-cbc@lysator.liu.se 37 | 38 | # The MACs that must match exactly (order matters). 39 | macs = hmac-md5, hmac-sha1, umac-64@openssh.com, hmac-ripemd160, hmac-ripemd160@openssh.com, hmac-sha1-96, hmac-md5-96 40 | -------------------------------------------------------------------------------- /test/docker/policies/policy_test8.txt: -------------------------------------------------------------------------------- 1 | # 2 | # Docker policy: test8 3 | # 4 | 5 | # The name of this policy (displayed in the output during scans). Must be in quotes. 6 | name = "Docker poliicy: test8" 7 | 8 | # The version of this policy (displayed in the output during scans). Not parsed, and may be any value, including strings. 9 | version = 1 10 | 11 | # The banner that must match exactly. Commented out to ignore banners, since minor variability in the banner is sometimes normal. 12 | # banner = "SSH-2.0-OpenSSH_5.6" 13 | 14 | # The header that must match exactly. Commented out to ignore headers, since variability in the header is sometimes normal. 15 | # header = "[]" 16 | 17 | # The compression options that must match exactly (order matters). Commented out to ignore by default. 18 | # compressions = none, zlib@openssh.com 19 | 20 | # RSA host key sizes. 21 | hostkey_size_rsa-sha2-256 = 3072 22 | hostkey_size_rsa-sha2-512 = 3072 23 | hostkey_size_ssh-rsa = 3072 24 | hostkey_size_ssh-rsa-cert-v01@openssh.com = 3072 25 | 26 | # RSA CA key sizes. 27 | cakey_size_ssh-rsa-cert-v01@openssh.com = 2048 28 | 29 | # The host key types that must match exactly (order matters). 30 | host keys = ssh-rsa, ssh-rsa-cert-v01@openssh.com 31 | 32 | # The key exchange algorithms that must match exactly (order matters). 33 | key exchanges = diffie-hellman-group-exchange-sha256, diffie-hellman-group-exchange-sha1, diffie-hellman-group14-sha1, diffie-hellman-group1-sha1 34 | 35 | # The ciphers that must match exactly (order matters). 36 | ciphers = aes128-ctr, aes192-ctr, aes256-ctr, arcfour256, arcfour128, aes128-cbc, 3des-cbc, blowfish-cbc, cast128-cbc, aes192-cbc, aes256-cbc, arcfour, rijndael-cbc@lysator.liu.se 37 | 38 | # The MACs that must match exactly (order matters). 39 | macs = hmac-md5, hmac-sha1, umac-64@openssh.com, hmac-ripemd160, hmac-ripemd160@openssh.com, hmac-sha1-96, hmac-md5-96 40 | -------------------------------------------------------------------------------- /test/docker/policies/policy_test9.txt: -------------------------------------------------------------------------------- 1 | # 2 | # Docker policy: test9 3 | # 4 | 5 | # The name of this policy (displayed in the output during scans). Must be in quotes. 6 | name = "Docker poliicy: test9" 7 | 8 | # The version of this policy (displayed in the output during scans). Not parsed, and may be any value, including strings. 9 | version = 1 10 | 11 | # The banner that must match exactly. Commented out to ignore banners, since minor variability in the banner is sometimes normal. 12 | # banner = "SSH-2.0-OpenSSH_5.6" 13 | 14 | # The header that must match exactly. Commented out to ignore headers, since variability in the header is sometimes normal. 15 | # header = "[]" 16 | 17 | # The compression options that must match exactly (order matters). Commented out to ignore by default. 18 | # compressions = none, zlib@openssh.com 19 | 20 | # RSA host key sizes. 21 | hostkey_size_rsa-sha2-256 = 3072 22 | hostkey_size_rsa-sha2-512 = 3072 23 | hostkey_size_ssh-rsa = 3072 24 | hostkey_size_ssh-rsa-cert-v01@openssh.com = 4096 25 | 26 | # RSA CA key sizes. 27 | cakey_size_ssh-rsa-cert-v01@openssh.com = 1024 28 | 29 | # The host key types that must match exactly (order matters). 30 | host keys = ssh-rsa, ssh-rsa-cert-v01@openssh.com 31 | 32 | # The key exchange algorithms that must match exactly (order matters). 33 | key exchanges = diffie-hellman-group-exchange-sha256, diffie-hellman-group-exchange-sha1, diffie-hellman-group14-sha1, diffie-hellman-group1-sha1 34 | 35 | # The ciphers that must match exactly (order matters). 36 | ciphers = aes128-ctr, aes192-ctr, aes256-ctr, arcfour256, arcfour128, aes128-cbc, 3des-cbc, blowfish-cbc, cast128-cbc, aes192-cbc, aes256-cbc, arcfour, rijndael-cbc@lysator.liu.se 37 | 38 | # The MACs that must match exactly (order matters). 39 | macs = hmac-md5, hmac-sha1, umac-64@openssh.com, hmac-ripemd160, hmac-ripemd160@openssh.com, hmac-sha1-96, hmac-md5-96 40 | -------------------------------------------------------------------------------- /test/docker/ssh1_host_key: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtesta/ssh-audit/b456bb31b977a9101a8b13e5f7137561c82f57ba/test/docker/ssh1_host_key -------------------------------------------------------------------------------- /test/docker/ssh1_host_key.pub: -------------------------------------------------------------------------------- 1 | 1024 35 150823875409720459951648542224727752099073441604930026287525797402159071426070997897033651155038337251362080634963146983947007228274330777134724953282680928153520263171933106732090266742784258910450489054624715996015082463159338507115031336180486071622718809324273851629938883104520608180885444242395900180011 root@ubuntu1604server 2 | -------------------------------------------------------------------------------- /test/docker/ssh_host_dsa_key: -------------------------------------------------------------------------------- 1 | -----BEGIN DSA PRIVATE KEY----- 2 | MIIBugIBAAKBgQDth1eV+A8j191R0ey0dVXL2LGNGYM+a+PomSa7suK8xNCeVLKC 3 | YpQ6VSWpAf6FbRWev1UVo8IpbglwFZPcyFPK2G1H7p45ows2SN4CleszDD56e6W0 4 | 3Plc+qMqSJ6LTjr4M5+HqTDOM3CS72d7MXUkfHQiagyrWQhXyc0kFsNJLwIVAKg7 5 | b5+NiIZzpg5IEH0tlYFQpuhBAoGAGcbq79QqNNZRuPCE/F05sCoTRGCmFnDjCuCg 6 | WN7wNRotjMz/S3pHtCCeuTT1jT6Hy0ZFHftv0t/GF8GBRgeokUbS4ytHpOkFWcTz 7 | 8oFguDL44nq8eNfSY6bzEl84qsgEe4HP93mB4FR1ZUUgI4b7gCBOYEFl3yPiH7H1 8 | p7Z9E1oCgYAl1UPQkeRhElz+AgEbNsnMKu1+6O3/z95D1Wvv4OEwAImbytlBaC7p 9 | kwJElJNsMMfGqCC8OHdJ0e4VQQUwk/GOhD0MFhVQHBtVZYbiWmVkpfHf1ouUQg3f 10 | 1IZmz2SSt6cPPEu+BEQ/Sn3mFRJ5XSTHLtnI0HJeDND5u1+6p1nXawIURv3Maige 11 | oxmfqC24VoROJEq+sew= 12 | -----END DSA PRIVATE KEY----- 13 | -------------------------------------------------------------------------------- /test/docker/ssh_host_dsa_key.pub: -------------------------------------------------------------------------------- 1 | ssh-dss AAAAB3NzaC1kc3MAAACBAO2HV5X4DyPX3VHR7LR1VcvYsY0Zgz5r4+iZJruy4rzE0J5UsoJilDpVJakB/oVtFZ6/VRWjwiluCXAVk9zIU8rYbUfunjmjCzZI3gKV6zMMPnp7pbTc+Vz6oypInotOOvgzn4epMM4zcJLvZ3sxdSR8dCJqDKtZCFfJzSQWw0kvAAAAFQCoO2+fjYiGc6YOSBB9LZWBUKboQQAAAIAZxurv1Co01lG48IT8XTmwKhNEYKYWcOMK4KBY3vA1Gi2MzP9Leke0IJ65NPWNPofLRkUd+2/S38YXwYFGB6iRRtLjK0ek6QVZxPPygWC4Mvjierx419JjpvMSXziqyAR7gc/3eYHgVHVlRSAjhvuAIE5gQWXfI+IfsfWntn0TWgAAAIAl1UPQkeRhElz+AgEbNsnMKu1+6O3/z95D1Wvv4OEwAImbytlBaC7pkwJElJNsMMfGqCC8OHdJ0e4VQQUwk/GOhD0MFhVQHBtVZYbiWmVkpfHf1ouUQg3f1IZmz2SSt6cPPEu+BEQ/Sn3mFRJ5XSTHLtnI0HJeDND5u1+6p1nXaw== 2 | -------------------------------------------------------------------------------- /test/docker/ssh_host_ecdsa_key: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PRIVATE KEY----- 2 | MHcCAQEEICq/YV5QenL0uW5g5tCjY3EWs+UBFmskY+Jjt2vd2aEmoAoGCCqGSM49 3 | AwEHoUQDQgAEdYSxDVUjOpW479L/nRDiAdxRB5Kuy2bgkP/LA2pnWPcGIWmFa4QU 4 | YN2U3JsFKcLIcx5cvTehQfgrHDnaSKVdKA== 5 | -----END EC PRIVATE KEY----- 6 | -------------------------------------------------------------------------------- /test/docker/ssh_host_ecdsa_key.pub: -------------------------------------------------------------------------------- 1 | ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBHWEsQ1VIzqVuO/S/50Q4gHcUQeSrstm4JD/ywNqZ1j3BiFphWuEFGDdlNybBSnCyHMeXL03oUH4Kxw52kilXSg= 2 | -------------------------------------------------------------------------------- /test/docker/ssh_host_ed25519_key: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW 3 | QyNTUxOQAAACC/9RD2Ao95ODDIH8i11ekTALut8AUNqWoQx0jHlP4xygAAAKDiqVOs4qlT 4 | rAAAAAtzc2gtZWQyNTUxOQAAACC/9RD2Ao95ODDIH8i11ekTALut8AUNqWoQx0jHlP4xyg 5 | AAAECTmHGkq0Qea0QqTJYMXL0bpxVU7mhgwYninfVWxrA017/1EPYCj3k4MMgfyLXV6RMA 6 | u63wBQ2pahDHSMeU/jHKAAAAHWpkb2dAbG9jYWxob3N0LndvbmRlcmxhbmQubG9s 7 | -----END OPENSSH PRIVATE KEY----- 8 | -------------------------------------------------------------------------------- /test/docker/ssh_host_ed25519_key-cert.pub: -------------------------------------------------------------------------------- 1 | ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIO1W0I8tD0c4LypvHY1XNch3BQCw9Yy28/4KmAYql80DAAAAIL/1EPYCj3k4MMgfyLXV6RMAu63wBQ2pahDHSMeU/jHKAAAAAAAAAAAAAAACAAAABHRlc3QAAAAIAAAABHRlc3QAAAAAXV7hvAAAAACBa2YhAAAAAAAAAAAAAAAAAAAAMwAAAAtzc2gtZWQyNTUxOQAAACAbM9Wp3ZPcC8Ifhu6GjNDJaoMg7KxO0el2+r9J35TltQAAAFMAAAALc3NoLWVkMjU1MTkAAABAW60bCSeIG4Ta+57zgkSbW4LIGCxtOuJJ+pP3i3S0xJJfHGnOtXbg0NQm7pulNl/wd01kgJO9A7RjbhTh7TV1AA== ssh_host_ed25519_key.pub 2 | -------------------------------------------------------------------------------- /test/docker/ssh_host_ed25519_key.pub: -------------------------------------------------------------------------------- 1 | ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL/1EPYCj3k4MMgfyLXV6RMAu63wBQ2pahDHSMeU/jHK 2 | -------------------------------------------------------------------------------- /test/docker/ssh_host_rsa_key_1024: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXgIBAAKBgQDeCC1U7VqVg9AfrfWrXACiW6pzYOuP8tim68z+YN/dUU7JhFZ4 3 | 0toteQkLcJBAD2miQ6ZJYkjVfhQ4FRFeOW5vcN0UYHn8ttb2mKdGJdt24ZYY5Z6J 4 | WHQhPOpSgtWyUv6RnxU2ligEeaoPaiepUUOhoyLf4WcF7voVCAKZNqeTtQIDAQAB 5 | AoGATGZ16s5NqDsWJ4B9k3xx/2wZZ+BGzl6a7D0habq97XLn8HGoK6UqTBFk6lnO 6 | WSy0hZBPrNq0AzqCDJY7RrfuZqgVAu/+HEFuXencgt8Z//ueYBaGK8yAC+OrMnDG 7 | LbSoIGRq8saaFtCzt47c+uSVsrhJ4TvK5gbceZuD/2uw10ECQQD79T0j+YWsLISK 8 | PKvYHqEXSMPN6b+lK9hRPLoF9NMksNLSjuxxhkYHz+hJPVNT+wPtRMAYmMdPXfKa 9 | FjuErXVFAkEA4ZgJIOeJ7OHfqGEgd29m36yFy0UaUJ+cmYuJzHAYWgW3TOanqpZm 10 | A8EENuXvH0DtYRVytv4m/cIRVVPxWtXzsQJBALXlQUOEc0VuSi1GScVXr3KQ3JL+ 11 | ipWixqM3VRDRw9D8Ouc5uWbnygz/wrGFLXA2ioozlP7s5Q7eQzOMk2FgnIUCQQCz 12 | j5QUgLcjuVWQbF6vMhisCGImPUaIzcKT5KE1/DMl1E7mAuGJwlRIwKVeHP6L3d4T 13 | 3EKGrRzT9lhdlocRSiLBAkEAi3xI0MOZp4xGviPc1C1TKuqdJSr8fHwbtLozwNQO 14 | nnF6m5S72JzZEThDBZS9zcdBp9EFpTvUGzx/O0GI454eoA== 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /test/docker/ssh_host_rsa_key_1024-cert_1024.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgFqxXSa9HTqCw5YW3DdIwVREPGxI+i56w32RnHWRg0NoAAAADAQABAAAAgQDeCC1U7VqVg9AfrfWrXACiW6pzYOuP8tim68z+YN/dUU7JhFZ40toteQkLcJBAD2miQ6ZJYkjVfhQ4FRFeOW5vcN0UYHn8ttb2mKdGJdt24ZYY5Z6JWHQhPOpSgtWyUv6RnxU2ligEeaoPaiepUUOhoyLf4WcF7voVCAKZNqeTtQAAAAAAAAAAAAAAAgAAAAR0ZXN0AAAACAAAAAR0ZXN0AAAAAF1evHgAAAAAgWtAtQAAAAAAAAAAAAAAAAAAAJcAAAAHc3NoLXJzYQAAAAMBAAEAAACBAOdGU3cAWdR7aUV/lcb1SFcuv/086u41MVsy3TOuM5RKOYBLuFLqkO/lUROhPoNpHURBRhqpIykdjNmG4Irna+vJ06blcVsnvvXav0zJlBSGhVnHxK50EfNUMhU7eNwwxYiWt9YEydRpQcSqmbLzxjuAoNrZNbEmDX6GDnUqoetRAAAAjwAAAAdzc2gtcnNhAAAAgFRc2g0eWXGmqSa6Z8rcMPHf4rNMornEHSnTzZ8Rdh4YBhDa9xMRRy6puaWPDzXxOfZh7eSjCwrzUMEXTgCIO4oX62xm/6kUnAmKhXle0+inR/hdPg03daE0SBJ4spBT49lJ4WIW38RKFNzjmYg70rTPAnP8oM/F3CC1GV117/Vv ssh_host_rsa_key_1024.pub 2 | -------------------------------------------------------------------------------- /test/docker/ssh_host_rsa_key_1024-cert_3072.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgrJGcfyW8V6VWGT7lD1ardj2RtTP8TOjmLRNbuoGkyZQAAAADAQABAAAAgQDeCC1U7VqVg9AfrfWrXACiW6pzYOuP8tim68z+YN/dUU7JhFZ40toteQkLcJBAD2miQ6ZJYkjVfhQ4FRFeOW5vcN0UYHn8ttb2mKdGJdt24ZYY5Z6JWHQhPOpSgtWyUv6RnxU2ligEeaoPaiepUUOhoyLf4WcF7voVCAKZNqeTtQAAAAAAAAAAAAAAAgAAAAR0ZXN0AAAACAAAAAR0ZXN0AAAAAF1evHgAAAAAgWtA7gAAAAAAAAAAAAAAAAAAAZcAAAAHc3NoLXJzYQAAAAMBAAEAAAGBAKsUBCG4/MNE6wWNXwzvNXLq4o0K3SzlK/FZfFxAlnOUBndS5ABvNDYHXAgT741sWUpmKynqgO/BlHTfioMnBNM7acWElyh2TGiDOV3veAQfgaHB/db3f9ogcxnnndZ1f83wHatgBB0I9U6SCPmvs9ucsb9j7sFU08MKPSP+iYDat3rx+B28IK90kXxBGGUPmC/HDqQbjYNWGTkdJNcpNt2agHfdicm8WeIgrTre63I9rBR8zbtVOeOMFgcCC+cmWSk4AYWOHan09hKeyM0eR3bHa70FfagvdhjNcwxPlNwMK1Sox7NGGat483Kv8rxQwnDDEc+P9KRhRVNuqFpmvp972wMSiaC/q4RvEDPfpdqhZSxrJzNJFFwCV7Dym8oKWj/Gd1OHioE7idhrkvXiyBZwe5q8A++VehoDfauZmPPQ35FUpB2qKqWPKT3UZM/UAJjBLi7R5r9vQlTqX1+b/67Ju8AEgGarVJZhbuKxRgS8L2W1JVrtNh5oAU+O4OhCHwAAAY8AAAAHc3NoLXJzYQAAAYCO78ONpHp+EGWDqDLb/GPFDH32o6oaRRrIH/Bhvg1FOi5XngnHTdU7xWdnJqNE2Sl6VOrg0sTCMYcX9fZ8tVREnCo3aF7Iwow5Br67QYKayRzHANQqHaVK46lpI1gz81V00u54tX1F8oEUqm6sRmFKFuklt6CjfbR+tnpj7DrfeOTKEBOGJP2uU0jMsJr2DrBeXrzONjIJtIJ1AxWjXd2LeIWO2C6yTkcN5ggThMMaeu6QuuBPpC2PN2COfu+Mgto9g103+/SS4Wa8CzinZZn2Xe1isUxI8QRNrShy4Hl/bIZQL7mi/0rxkfw+fA7IzMk462V99gPVSp+/jK0sbUJoC3QeeglS5hWodjW+VGZfgweGQ+AE/OxkNSv+kDPMYEkjfOf4qhxS5QFvButLt6zp2UNbE5+OWvYpdjO9/DOa0ro+wCw07+dVKIcDpU2csiCcJvQ/HmKAmhch7jOHa0WaxSX0tt0xTPJWTvr6E4WZOgEnk9AvWmrKjF5tEzGYwTU= ssh_host_rsa_key_1024.pub 2 | -------------------------------------------------------------------------------- /test/docker/ssh_host_rsa_key_1024.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDeCC1U7VqVg9AfrfWrXACiW6pzYOuP8tim68z+YN/dUU7JhFZ40toteQkLcJBAD2miQ6ZJYkjVfhQ4FRFeOW5vcN0UYHn8ttb2mKdGJdt24ZYY5Z6JWHQhPOpSgtWyUv6RnxU2ligEeaoPaiepUUOhoyLf4WcF7voVCAKZNqeTtQ== 2 | -------------------------------------------------------------------------------- /test/docker/ssh_host_rsa_key_3072: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIG5AIBAAKCAYEAzhHp7eFnrQAJqOd7aihyQyIDKgxCF7H51Q3Ft3+8af+lX3Ol 3 | Ie77Gi5GNNM+eRB4OzG+CBslxN5I3pM//sZ+gyylA1VuWZZkOlgtbOHutIkO2ldk 4 | XtoGidla0VAxLcUcUK6cCmqwBTT31Hp4Qimp2zyeg/l5q0DhWKguY13lrm5b3YZY 5 | rj7CW3Ktzxf8SbYz6du8KF0dHCWilzq+FLeGzXr7Yul5njVF5njkGvZ9duQ0qiVR 6 | zqZkrkLEWgQlCM0T+PyUbvedL1MfDZPHGh7ZhU0snOvJRsxAr31tlknq+WwauZYd 7 | DzJf1g1URcM65UwEsPlfgOW3ZoZogR1v57Im+KdsKhq2B3snEtJgdQh06JyO0ur4 8 | uUXo1mMtvBFhiptUtwP4g9v/IN4neeK+wBRom46m2Q1bMUBPneBOa8r2SY/3ynrz 9 | XuVIWFOQtF60aJ+BNqvgUVCKOmz1KzoJwTqGm+EFaKM5z+UQWjIbSE3Ge4X5hXtk 10 | Ou52v+tyDUk6boZLAgMBAAECggGAdrhxWmA7N7tG1W2Pd6iXs7+brRTk2vvpYGqP 11 | 11kbNsJXBzf8EiG5vuqb/gEaA+uOKSRORCNHzjT2LG0POHwpFO+aneIRMkHnuolk 12 | mk9ME+zGhtpEdDUOAUsc/GxD+QePeZgvQ/0VLdrHUT3BnPSd7DXvaT9IbnZxnX8/ 13 | QnYtRiJEgMrOuoxjswXNxvsdmWYEYJ38uBB1Hes80f3A1vSpECbjP6gdLh2pCM/r 14 | MvGBdQaipMfdar4IUTEcKHQs1fY3mlAxnWRjYCqJPmq10d3NrdUrHb2zBE1HCC4h 15 | aj2ycTxFhDJqGV6Y2AboHqh2c7lPJ+R2UjI9mIpALZSviHB1POcpWCAGA3NKjri9 16 | 8jgxl3bj03ikJNfCuvlqRTa8at63W2zZTMRsxamoiO023uUOEMNBPwWXP/rVhQ8g 17 | ufih0SY44j0EMPIuu2PoQV4ZSOtDw8xdPrchVCa078/pP5cRa4uV0bl2K4as+cYC 18 | BhjEq2Org3ulDW2n6Mz5ZS7NbAkxAoHBAP/bgPGKX7rfrHa5MRHIgbLSmZtUoF51 19 | YGelc8ytRx6UT6wriJ1jQRXiI5mZlIXyVxMpIz9s4+h59kF+LpZuNLc3vTYpPOQn 20 | RUDBVY6+SPC5MancL7bfBoHahpWEJuJB/WUE7eWvQM03/LsBtU6Nq+R632t5KdqF 21 | A4y86qgD1vIjcBWvySLFJZGOCoNbj7ZinoBUO3ueYK6SUj8xH6TAqOJsTPvquRT3 22 | AFBpFBmrVc24wW7wTiLkQOhkIQs1J/ZhYwKBwQDOL07qF8wsoQBBTTXkZ59BCauz 23 | R8kfqe5oUBwsmGJdiIHX6gutBA07sSwzVekIvCCkJFXk3TxLoBSMHEZEIdnS+HVt 24 | gMIacYuhbh+XztdY0kadH/SMbVQD/2LZcL99vcZPq1QF3cHb0Buip5+fyAYjoEc7 25 | oVgvewD/TwdNcMjos/kMNh6l04kLi6vQG3WhoSBPWaoB669ppBNXSrWKe43nXVi6 26 | EvjGEiL+HCCnmD6LiD6p797Owu9AChP6fXInD/kCgcEAiLP3SRbt3yLzOtvn4+CF 27 | q83qVJv6s31zbO1x2cIbZbNIfm0kKTOG6vJQoxjzyj2ZWJt6QcEkZGoFsSiCK83m 28 | TJ5zciTGbACvd9HUrNfukO/iISeMNuEi0O65Sdm6DNnFUdw4X6grr3pihmh7PuVj 29 | GkisZvft7Nt08hVeKzch+W4FzRCHHxTG5eZGp7icKI64sUhQH9SXQ67aUvkkNxrZ 30 | IWFMIK1hBlqSyGPcYXqx9aDpeSTcGrhqFcCqBxr3pySRAoHAfJNO3delEC3yxoHN 31 | FwSYzyX1rOuplE0K89G7RCKKBDNPKFKL3Wx+Rluk9htpIlLwcdxWXWJiZNsCrykC 32 | N3YwcuyVnqTWIj4KfG3Z/tIFgPADpDnDevkvcv7iDbi2qlV4NXix2p2C3LnfiKY4 33 | psSnGO1lPJ0eeAmcr6VjJyIG8bqTthIY8F5gBi7Mj3+X0iFVMTxeoKxzHqP435wP 34 | Fe3S7kCTNFH0J1Cb/eamwDwXRhz6p5h7iXd0MMAmFAmpZ/qZAoHBAPDSIvk2ocf1 35 | FVW8pKtKOJFIs8iQVIaOLKwPJVP8/JsB1+7mQx5KMoROb5pNpX2edN4vvG0CgqpJ 36 | KekleqpH6nQCqYGFZ1BDhORElNILxeJHcNl0eAG++IJ2PfIpTZV30edDqMm0x7EI 37 | 8POZWAx809VzcYbE2jsgpN/EuiaG30EAI5yNvyzmZRCyQykH+eltHlCx17MWBxRQ 38 | bb2UUfpdInTMS2vyrvkeUACkC1DGYdBVVBqqPTkHZg+Kcbs8ntQqEQ== 39 | -----END RSA PRIVATE KEY----- 40 | -------------------------------------------------------------------------------- /test/docker/ssh_host_rsa_key_3072-cert_1024.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgGHz1JwF/1IcxW3pdQtpqbUjIHaFuk0cR/+l50vG+9hIAAAADAQABAAABgQDOEent4WetAAmo53tqKHJDIgMqDEIXsfnVDcW3f7xp/6Vfc6Uh7vsaLkY00z55EHg7Mb4IGyXE3kjekz/+xn6DLKUDVW5ZlmQ6WC1s4e60iQ7aV2Re2gaJ2VrRUDEtxRxQrpwKarAFNPfUenhCKanbPJ6D+XmrQOFYqC5jXeWublvdhliuPsJbcq3PF/xJtjPp27woXR0cJaKXOr4Ut4bNevti6XmeNUXmeOQa9n125DSqJVHOpmSuQsRaBCUIzRP4/JRu950vUx8Nk8caHtmFTSyc68lGzECvfW2WSer5bBq5lh0PMl/WDVRFwzrlTASw+V+A5bdmhmiBHW/nsib4p2wqGrYHeycS0mB1CHTonI7S6vi5RejWYy28EWGKm1S3A/iD2/8g3id54r7AFGibjqbZDVsxQE+d4E5ryvZJj/fKevNe5UhYU5C0XrRon4E2q+BRUIo6bPUrOgnBOoab4QVooznP5RBaMhtITcZ7hfmFe2Q67na/63INSTpuhksAAAAAAAAAAAAAAAIAAAAEdGVzdAAAAAgAAAAEdGVzdAAAAABdXry0AAAAAIFrQR8AAAAAAAAAAAAAAAAAAACXAAAAB3NzaC1yc2EAAAADAQABAAAAgQDnRlN3AFnUe2lFf5XG9UhXLr/9POruNTFbMt0zrjOUSjmAS7hS6pDv5VEToT6DaR1EQUYaqSMpHYzZhuCK52vrydOm5XFbJ7712r9MyZQUhoVZx8SudBHzVDIVO3jcMMWIlrfWBMnUaUHEqpmy88Y7gKDa2TWxJg1+hg51KqHrUQAAAI8AAAAHc3NoLXJzYQAAAIB4HaEexgQ9T6rScEbiHZx+suCaYXI7ywLYyoSEO48K8o+MmO83UTLtpPa3DXlT8hSYL8Aq6Bb5AMkDawsgsC484owPqObT/5ndLG/fctNBFcCTSL0ftte+A8xH0pZaGRoKbdxxgMqX4ubrCXpbMLGF9aAeh7MRa756XzqGlsCiSA== ssh_host_rsa_key_3072.pub 2 | -------------------------------------------------------------------------------- /test/docker/ssh_host_rsa_key_3072-cert_3072.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAg9MVX4OlkEy3p9eC+JJp8h7j76EmI46EY/RXxCGSWTC0AAAADAQABAAABgQDOEent4WetAAmo53tqKHJDIgMqDEIXsfnVDcW3f7xp/6Vfc6Uh7vsaLkY00z55EHg7Mb4IGyXE3kjekz/+xn6DLKUDVW5ZlmQ6WC1s4e60iQ7aV2Re2gaJ2VrRUDEtxRxQrpwKarAFNPfUenhCKanbPJ6D+XmrQOFYqC5jXeWublvdhliuPsJbcq3PF/xJtjPp27woXR0cJaKXOr4Ut4bNevti6XmeNUXmeOQa9n125DSqJVHOpmSuQsRaBCUIzRP4/JRu950vUx8Nk8caHtmFTSyc68lGzECvfW2WSer5bBq5lh0PMl/WDVRFwzrlTASw+V+A5bdmhmiBHW/nsib4p2wqGrYHeycS0mB1CHTonI7S6vi5RejWYy28EWGKm1S3A/iD2/8g3id54r7AFGibjqbZDVsxQE+d4E5ryvZJj/fKevNe5UhYU5C0XrRon4E2q+BRUIo6bPUrOgnBOoab4QVooznP5RBaMhtITcZ7hfmFe2Q67na/63INSTpuhksAAAAAAAAAAAAAAAIAAAAEdGVzdAAAAAgAAAAEdGVzdAAAAABdXr0sAAAAAIFrQWwAAAAAAAAAAAAAAAAAAAGXAAAAB3NzaC1yc2EAAAADAQABAAABgQCrFAQhuPzDROsFjV8M7zVy6uKNCt0s5SvxWXxcQJZzlAZ3UuQAbzQ2B1wIE++NbFlKZisp6oDvwZR034qDJwTTO2nFhJcodkxogzld73gEH4Ghwf3W93/aIHMZ553WdX/N8B2rYAQdCPVOkgj5r7PbnLG/Y+7BVNPDCj0j/omA2rd68fgdvCCvdJF8QRhlD5gvxw6kG42DVhk5HSTXKTbdmoB33YnJvFniIK063utyPawUfM27VTnjjBYHAgvnJlkpOAGFjh2p9PYSnsjNHkd2x2u9BX2oL3YYzXMMT5TcDCtUqMezRhmrePNyr/K8UMJwwxHPj/SkYUVTbqhaZr6fe9sDEomgv6uEbxAz36XaoWUsayczSRRcAlew8pvKClo/xndTh4qBO4nYa5L14sgWcHuavAPvlXoaA32rmZjz0N+RVKQdqiqljyk91GTP1ACYwS4u0ea/b0JU6l9fm/+uybvABIBmq1SWYW7isUYEvC9ltSVa7TYeaAFPjuDoQh8AAAGPAAAAB3NzaC1yc2EAAAGAG8tCiBMSq3Of3Gmcrid2IfPmaaemYivgEEuK8ubq1rznF0vtR07/NUQ7WVzfJhUSeG0gtJ3A1ey60NjcBn0DHao4Q3ATIXnkSOIKjNolZ2urqYv9fT1LAC4I5XWGzK2aKK0NEqAYr06YPtcGOBQk5+3GPAWSJ4eQycKRz5BSuMYbKaVxU0kGSvbavG07ZntMQhia/lILyq84PjXh/JlRVpIqY+LAS0qwqkUR3gWMTmvYvYI7fXU84ReVB1ut75bY7Xx0DXHPl1Zc2MNDLGcKsByZtoO9ueZRyOlZMUJcVP5fK+OUuZKjMbCaaJnV55BQ78/rftIPYsTEEO2Sf9WT86ADa3k4S0pyWqlTxBzZcDWNt+fZFNm9wcqcYS32nDKtfixcDN8E/IJIWY7aoabPqoYnKUVQBOcIEnZf1HqsKUVmF44Dp9mKhefUs3BtcdK63j/lNXzzMrPwZQAreJqH/uV3TgYBLjMPl++ctX6tCe6Hv5zFKNhnOCSBSzcsCgIU ssh_host_rsa_key_3072.pub 2 | -------------------------------------------------------------------------------- /test/docker/ssh_host_rsa_key_3072.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDOEent4WetAAmo53tqKHJDIgMqDEIXsfnVDcW3f7xp/6Vfc6Uh7vsaLkY00z55EHg7Mb4IGyXE3kjekz/+xn6DLKUDVW5ZlmQ6WC1s4e60iQ7aV2Re2gaJ2VrRUDEtxRxQrpwKarAFNPfUenhCKanbPJ6D+XmrQOFYqC5jXeWublvdhliuPsJbcq3PF/xJtjPp27woXR0cJaKXOr4Ut4bNevti6XmeNUXmeOQa9n125DSqJVHOpmSuQsRaBCUIzRP4/JRu950vUx8Nk8caHtmFTSyc68lGzECvfW2WSer5bBq5lh0PMl/WDVRFwzrlTASw+V+A5bdmhmiBHW/nsib4p2wqGrYHeycS0mB1CHTonI7S6vi5RejWYy28EWGKm1S3A/iD2/8g3id54r7AFGibjqbZDVsxQE+d4E5ryvZJj/fKevNe5UhYU5C0XrRon4E2q+BRUIo6bPUrOgnBOoab4QVooznP5RBaMhtITcZ7hfmFe2Q67na/63INSTpuhks= 2 | -------------------------------------------------------------------------------- /test/stubs/colorama.pyi: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | def init(autoreset: bool = False, convert: Optional[bool] = None, strip: Optional[bool] = None, wrap: bool = True) -> None: ... 4 | 5 | -------------------------------------------------------------------------------- /test/test_banner.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from ssh_audit.banner import Banner 4 | 5 | 6 | # pylint: disable=line-too-long,attribute-defined-outside-init 7 | class TestBanner: 8 | @pytest.fixture(autouse=True) 9 | def init(self, ssh_audit): 10 | self.banner = Banner 11 | 12 | def test_simple_banners(self): 13 | banner = lambda x: self.banner.parse(x) # noqa 14 | b = banner('SSH-2.0-OpenSSH_7.3') 15 | assert b.protocol == (2, 0) 16 | assert b.software == 'OpenSSH_7.3' 17 | assert b.comments is None 18 | assert str(b) == 'SSH-2.0-OpenSSH_7.3' 19 | b = banner('SSH-1.99-Sun_SSH_1.1.3') 20 | assert b.protocol == (1, 99) 21 | assert b.software == 'Sun_SSH_1.1.3' 22 | assert b.comments is None 23 | assert str(b) == 'SSH-1.99-Sun_SSH_1.1.3' 24 | b = banner('SSH-1.5-Cisco-1.25') 25 | assert b.protocol == (1, 5) 26 | assert b.software == 'Cisco-1.25' 27 | assert b.comments is None 28 | assert str(b) == 'SSH-1.5-Cisco-1.25' 29 | 30 | def test_invalid_banners(self): 31 | b = lambda x: self.banner.parse(x) # noqa 32 | assert b('Something') is None 33 | assert b('SSH-XXX-OpenSSH_7.3') is None 34 | 35 | def test_banners_with_spaces(self): 36 | b = lambda x: self.banner.parse(x) # noqa 37 | s = 'SSH-2.0-OpenSSH_4.3p2' 38 | assert str(b('SSH-2.0-OpenSSH_4.3p2 ')) == s 39 | assert str(b('SSH-2.0- OpenSSH_4.3p2')) == s 40 | assert str(b('SSH-2.0- OpenSSH_4.3p2 ')) == s 41 | s = 'SSH-2.0-OpenSSH_4.3p2 Debian-9etch3 on i686-pc-linux-gnu' 42 | assert str(b('SSH-2.0- OpenSSH_4.3p2 Debian-9etch3 on i686-pc-linux-gnu')) == s 43 | assert str(b('SSH-2.0-OpenSSH_4.3p2 Debian-9etch3 on i686-pc-linux-gnu ')) == s 44 | assert str(b('SSH-2.0- OpenSSH_4.3p2 Debian-9etch3 on i686-pc-linux-gnu ')) == s 45 | 46 | def test_banners_without_software(self): 47 | b = lambda x: self.banner.parse(x) # noqa 48 | assert b('SSH-2.0').protocol == (2, 0) 49 | assert b('SSH-2.0').software is None 50 | assert b('SSH-2.0').comments is None 51 | assert str(b('SSH-2.0')) == 'SSH-2.0' 52 | assert b('SSH-2.0-').protocol == (2, 0) 53 | assert b('SSH-2.0-').software == '' 54 | assert b('SSH-2.0-').comments is None 55 | assert str(b('SSH-2.0-')) == 'SSH-2.0-' 56 | 57 | def test_banners_with_comments(self): 58 | b = lambda x: self.banner.parse(x) # noqa 59 | assert repr(b('SSH-2.0-OpenSSH_7.2p2 Ubuntu-1')) == '' 60 | assert repr(b('SSH-1.99-OpenSSH_3.4p1 Debian 1:3.4p1-1.woody.3')) == '' 61 | assert repr(b('SSH-1.5-1.3.7 F-SECURE SSH')) == '' 62 | 63 | def test_banners_with_multiple_protocols(self): 64 | b = lambda x: self.banner.parse(x) # noqa 65 | assert str(b('SSH-1.99-SSH-1.99-OpenSSH_3.6.1p2')) == 'SSH-1.99-OpenSSH_3.6.1p2' 66 | assert str(b('SSH-2.0-SSH-2.0-OpenSSH_4.3p2 Debian-9')) == 'SSH-2.0-OpenSSH_4.3p2 Debian-9' 67 | assert str(b('SSH-1.99-SSH-2.0-dropbear_0.5')) == 'SSH-1.99-dropbear_0.5' 68 | assert str(b('SSH-2.0-SSH-1.99-OpenSSH_4.2p1 SSH Secure Shell (non-commercial)')) == 'SSH-1.99-OpenSSH_4.2p1 SSH Secure Shell (non-commercial)' 69 | assert str(b('SSH-1.99-SSH-1.99-SSH-1.99-OpenSSH_3.9p1')) == 'SSH-1.99-OpenSSH_3.9p1' 70 | -------------------------------------------------------------------------------- /test/test_buffer.py: -------------------------------------------------------------------------------- 1 | import re 2 | import pytest 3 | 4 | from ssh_audit.readbuf import ReadBuf 5 | from ssh_audit.writebuf import WriteBuf 6 | 7 | 8 | # pylint: disable=attribute-defined-outside-init,bad-whitespace 9 | class TestBuffer: 10 | @pytest.fixture(autouse=True) 11 | def init(self, ssh_audit): 12 | self.rbuf = ReadBuf 13 | self.wbuf = WriteBuf 14 | self.utf8rchar = b'\xef\xbf\xbd' 15 | 16 | @classmethod 17 | def _b(cls, v): 18 | v = re.sub(r'\s', '', v) 19 | data = [int(v[i * 2:i * 2 + 2], 16) for i in range(len(v) // 2)] 20 | return bytes(bytearray(data)) 21 | 22 | def test_unread(self): 23 | w = self.wbuf().write_byte(1).write_int(2).write_flush() 24 | r = self.rbuf(w) 25 | assert r.unread_len == 5 26 | r.read_byte() 27 | assert r.unread_len == 4 28 | r.read_int() 29 | assert r.unread_len == 0 30 | 31 | def test_byte(self): 32 | w = lambda x: self.wbuf().write_byte(x).write_flush() # noqa 33 | r = lambda x: self.rbuf(x).read_byte() # noqa 34 | tc = [(0x00, '00'), 35 | (0x01, '01'), 36 | (0x10, '10'), 37 | (0xff, 'ff')] 38 | for p in tc: 39 | assert w(p[0]) == self._b(p[1]) 40 | assert r(self._b(p[1])) == p[0] 41 | 42 | def test_bool(self): 43 | w = lambda x: self.wbuf().write_bool(x).write_flush() # noqa 44 | r = lambda x: self.rbuf(x).read_bool() # noqa 45 | tc = [(True, '01'), 46 | (False, '00')] 47 | for p in tc: 48 | assert w(p[0]) == self._b(p[1]) 49 | assert r(self._b(p[1])) == p[0] 50 | 51 | def test_int(self): 52 | w = lambda x: self.wbuf().write_int(x).write_flush() # noqa 53 | r = lambda x: self.rbuf(x).read_int() # noqa 54 | tc = [(0x00, '00 00 00 00'), 55 | (0x01, '00 00 00 01'), 56 | (0xabcd, '00 00 ab cd'), 57 | (0xffffffff, 'ff ff ff ff')] 58 | for p in tc: 59 | assert w(p[0]) == self._b(p[1]) 60 | assert r(self._b(p[1])) == p[0] 61 | 62 | def test_string(self): 63 | w = lambda x: self.wbuf().write_string(x).write_flush() # noqa 64 | r = lambda x: self.rbuf(x).read_string() # noqa 65 | tc = [('abc1', '00 00 00 04 61 62 63 31'), 66 | (b'abc2', '00 00 00 04 61 62 63 32')] 67 | for p in tc: 68 | v = p[0] 69 | assert w(v) == self._b(p[1]) 70 | if not isinstance(v, bytes): 71 | v = bytes(bytearray(v, 'utf-8')) 72 | assert r(self._b(p[1])) == v 73 | 74 | def test_list(self): 75 | w = lambda x: self.wbuf().write_list(x).write_flush() # noqa 76 | r = lambda x: self.rbuf(x).read_list() # noqa 77 | tc = [(['d', 'ef', 'ault'], '00 00 00 09 64 2c 65 66 2c 61 75 6c 74')] 78 | for p in tc: 79 | assert w(p[0]) == self._b(p[1]) 80 | assert r(self._b(p[1])) == p[0] 81 | 82 | def test_list_nonutf8(self): 83 | r = lambda x: self.rbuf(x).read_list() # noqa 84 | src = self._b('00 00 00 04 de ad be ef') 85 | dst = [(b'\xde\xad' + self.utf8rchar + self.utf8rchar).decode('utf-8')] 86 | assert r(src) == dst 87 | 88 | def test_line(self): 89 | w = lambda x: self.wbuf().write_line(x).write_flush() # noqa 90 | r = lambda x: self.rbuf(x).read_line() # noqa 91 | tc = [('example line', '65 78 61 6d 70 6c 65 20 6c 69 6e 65 0d 0a')] 92 | for p in tc: 93 | assert w(p[0]) == self._b(p[1]) 94 | assert r(self._b(p[1])) == p[0] 95 | 96 | def test_line_nonutf8(self): 97 | r = lambda x: self.rbuf(x).read_line() # noqa 98 | src = self._b('de ad be af') 99 | dst = (b'\xde\xad' + self.utf8rchar + self.utf8rchar).decode('utf-8') 100 | assert r(src) == dst 101 | 102 | def test_bitlen(self): 103 | # pylint: disable=protected-access 104 | class Py26Int(int): 105 | def bit_length(self): 106 | raise AttributeError 107 | assert self.wbuf._bitlength(42) == 6 108 | assert self.wbuf._bitlength(Py26Int(42)) == 6 109 | 110 | def test_mpint1(self): 111 | mpint1w = lambda x: self.wbuf().write_mpint1(x).write_flush() # noqa 112 | mpint1r = lambda x: self.rbuf(x).read_mpint1() # noqa 113 | tc = [(0x0, '00 00'), 114 | (0x1234, '00 0d 12 34'), 115 | (0x12345, '00 11 01 23 45'), 116 | (0xdeadbeef, '00 20 de ad be ef')] 117 | for p in tc: 118 | assert mpint1w(p[0]) == self._b(p[1]) 119 | assert mpint1r(self._b(p[1])) == p[0] 120 | 121 | def test_mpint2(self): 122 | mpint2w = lambda x: self.wbuf().write_mpint2(x).write_flush() # noqa 123 | mpint2r = lambda x: self.rbuf(x).read_mpint2() # noqa 124 | tc = [(0x0, '00 00 00 00'), 125 | (0x80, '00 00 00 02 00 80'), 126 | (0x9a378f9b2e332a7, '00 00 00 08 09 a3 78 f9 b2 e3 32 a7'), 127 | (-0x1234, '00 00 00 02 ed cc'), 128 | (-0xdeadbeef, '00 00 00 05 ff 21 52 41 11'), 129 | (-0x8000, '00 00 00 02 80 00'), 130 | (-0x80, '00 00 00 01 80')] 131 | for p in tc: 132 | assert mpint2w(p[0]) == self._b(p[1]) 133 | assert mpint2r(self._b(p[1])) == p[0] 134 | assert mpint2r(self._b('00 00 00 02 ff 80')) == -0x80 135 | 136 | def test_reset(self): 137 | w = self.wbuf() 138 | w.write_int(7) 139 | w.write_int(13) 140 | assert len(w.write_flush()) == 8 141 | 142 | w.write_int(7) 143 | w.write_int(13) 144 | w.reset() 145 | assert len(w.write_flush()) == 0 146 | -------------------------------------------------------------------------------- /test/test_build_struct.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pytest 3 | 4 | from ssh_audit.outputbuffer import OutputBuffer 5 | from ssh_audit.ssh2_kex import SSH2_Kex 6 | from ssh_audit.ssh2_kexparty import SSH2_KexParty 7 | 8 | 9 | @pytest.fixture 10 | def kex(ssh_audit): 11 | kex_algs, key_algs = [], [] 12 | enc, mac, compression, languages = [], [], ['none'], [] 13 | cli = SSH2_KexParty(enc, mac, compression, languages) 14 | enc, mac, compression, languages = [], [], ['none'], [] 15 | srv = SSH2_KexParty(enc, mac, compression, languages) 16 | cookie = os.urandom(16) 17 | kex = SSH2_Kex(OutputBuffer, cookie, kex_algs, key_algs, cli, srv, 0) 18 | return kex 19 | 20 | 21 | def test_prevent_runtime_error_regression(ssh_audit, kex): 22 | """Prevent a regression of https://github.com/jtesta/ssh-audit/issues/41 23 | 24 | The following test setup does not contain any sensible data. 25 | It was made up to reproduce a situation when there are several host 26 | keys, and an error occurred when iterating and modifying them at the 27 | same time. 28 | """ 29 | kex.set_host_key("ssh-rsa", b"\x00\x00\x00\x07ssh-rsa\x00\x00\x00", 1024, '', 0) 30 | kex.set_host_key("ssh-rsa1", b"\x00\x00\x00\x07ssh-rsa\x00\x00\x00", 1024, '', 0) 31 | kex.set_host_key("ssh-rsa2", b"\x00\x00\x00\x07ssh-rsa\x00\x00\x00", 1024, '', 0) 32 | kex.set_host_key("ssh-rsa3", b"\x00\x00\x00\x07ssh-rsa\x00\x00\x00", 1024, '', 0) 33 | kex.set_host_key("ssh-rsa4", b"\x00\x00\x00\x07ssh-rsa\x00\x00\x00", 1024, '', 0) 34 | kex.set_host_key("ssh-rsa5", b"\x00\x00\x00\x07ssh-rsa\x00\x00\x00", 1024, '', 0) 35 | kex.set_host_key("ssh-rsa6", b"\x00\x00\x00\x07ssh-rsa\x00\x00\x00", 1024, '', 0) 36 | kex.set_host_key("ssh-rsa7", b"\x00\x00\x00\x07ssh-rsa\x00\x00\x00", 1024, '', 0) 37 | kex.set_host_key("ssh-rsa8", b"\x00\x00\x00\x07ssh-rsa\x00\x00\x00", 1024, '', 0) 38 | 39 | rv = ssh_audit.build_struct('localhost', None, kex=kex) 40 | 41 | assert len(rv["fingerprints"]) == (9 * 2) # Each host key generates two hash fingerprints: one using SHA256, and one using MD5. 42 | 43 | for key in ['banner', 'compression', 'enc', 'fingerprints', 'kex', 'key', 'mac']: 44 | assert key in rv 45 | -------------------------------------------------------------------------------- /test/test_dheater.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from ssh_audit.ssh2_kexdb import SSH2_KexDB 4 | from ssh_audit.dheat import DHEat 5 | 6 | 7 | class TestDHEat: 8 | 9 | @pytest.fixture(autouse=True) 10 | def init(self): 11 | self.SSH2_KexDB = SSH2_KexDB 12 | self.DHEat = DHEat 13 | 14 | def test_kex_definition_completeness(self): 15 | alg_db = self.SSH2_KexDB.get_db() 16 | kex_db = alg_db['kex'] 17 | 18 | # Get all Diffie-Hellman algorithms defined in our database. 19 | dh_algs = [] 20 | for kex in kex_db: 21 | if kex.startswith('diffie-hellman-'): 22 | dh_algs.append(kex) 23 | 24 | # Ensure that each DH algorithm in our database is in either DHEat's alg_priority or gex_algs list. Also ensure that all non-group exchange algorithms are accounted for in the alg_modulus_sizes dictionary. 25 | for dh_alg in dh_algs: 26 | assert (dh_alg in self.DHEat.alg_priority) or (dh_alg in self.DHEat.gex_algs) 27 | 28 | if dh_alg.find("group-exchange") == -1: 29 | assert dh_alg in self.DHEat.alg_modulus_sizes 30 | -------------------------------------------------------------------------------- /test/test_outputbuffer.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | # pylint: disable=attribute-defined-outside-init 5 | class TestOutputBuffer: 6 | @pytest.fixture(autouse=True) 7 | def init(self, ssh_audit): 8 | self.OutputBuffer = ssh_audit.OutputBuffer 9 | 10 | def test_outputbuffer_no_lines(self, output_spy): 11 | output_spy.begin() 12 | obuf = self.OutputBuffer() 13 | obuf.write() 14 | assert output_spy.flush() == [''] 15 | output_spy.begin() 16 | 17 | def test_outputbuffer_defaults(self): 18 | obuf = self.OutputBuffer() 19 | # default: on 20 | assert obuf.batch is False 21 | assert obuf.use_colors is True 22 | assert obuf.level == 'info' 23 | 24 | def test_outputbuffer_colors(self, output_spy): 25 | out = self.OutputBuffer() 26 | 27 | # Test without colors. 28 | out.use_colors = False 29 | 30 | output_spy.begin() 31 | out.info('info color') 32 | out.write() 33 | assert output_spy.flush() == ['info color'] 34 | 35 | output_spy.begin() 36 | out.head('head color') 37 | out.write() 38 | assert output_spy.flush() == ['head color'] 39 | 40 | output_spy.begin() 41 | out.good('good color') 42 | out.write() 43 | assert output_spy.flush() == ['good color'] 44 | 45 | output_spy.begin() 46 | out.warn('warn color') 47 | out.write() 48 | assert output_spy.flush() == ['warn color'] 49 | 50 | output_spy.begin() 51 | out.fail('fail color') 52 | out.write() 53 | assert output_spy.flush() == ['fail color'] 54 | 55 | # If colors aren't supported by this system, skip the color tests. 56 | if not out.colors_supported: 57 | return 58 | 59 | # Test with colors. 60 | out.use_colors = True 61 | 62 | output_spy.begin() 63 | out.info('info color') 64 | out.write() 65 | assert output_spy.flush() == ['info color'] 66 | 67 | output_spy.begin() 68 | out.head('head color') 69 | out.write() 70 | assert output_spy.flush() in [['\x1b[0;36mhead color\x1b[0m'], ['\x1b[0;96mhead color\x1b[0m']] 71 | 72 | output_spy.begin() 73 | out.good('good color') 74 | out.write() 75 | assert output_spy.flush() in [['\x1b[0;32mgood color\x1b[0m'], ['\x1b[0;92mgood color\x1b[0m']] 76 | 77 | output_spy.begin() 78 | out.warn('warn color') 79 | out.write() 80 | assert output_spy.flush() in [['\x1b[0;33mwarn color\x1b[0m'], ['\x1b[0;93mwarn color\x1b[0m']] 81 | 82 | output_spy.begin() 83 | out.fail('fail color') 84 | out.write() 85 | assert output_spy.flush() in [['\x1b[0;31mfail color\x1b[0m'], ['\x1b[0;91mfail color\x1b[0m']] 86 | 87 | def test_outputbuffer_sep(self, output_spy): 88 | out = self.OutputBuffer() 89 | output_spy.begin() 90 | out.sep() 91 | out.sep() 92 | out.sep() 93 | out.write() 94 | assert output_spy.flush() == ['', '', ''] 95 | 96 | def test_outputbuffer_levels(self): 97 | out = self.OutputBuffer() 98 | assert out.get_level('info') == 0 99 | assert out.get_level('good') == 0 100 | assert out.get_level('warn') == 1 101 | assert out.get_level('fail') == 2 102 | assert out.get_level('unknown') > 2 103 | 104 | def test_outputbuffer_level_property(self): 105 | out = self.OutputBuffer() 106 | out.level = 'info' 107 | assert out.level == 'info' 108 | out.level = 'good' 109 | assert out.level == 'info' 110 | out.level = 'warn' 111 | assert out.level == 'warn' 112 | out.level = 'fail' 113 | assert out.level == 'fail' 114 | out.level = 'invalid level' 115 | assert out.level == 'unknown' 116 | 117 | def test_outputbuffer_level(self, output_spy): 118 | out = self.OutputBuffer() 119 | # visible: all 120 | out.level = 'info' 121 | output_spy.begin() 122 | out.info('info color') 123 | out.head('head color') 124 | out.good('good color') 125 | out.warn('warn color') 126 | out.fail('fail color') 127 | out.write() 128 | assert len(output_spy.flush()) == 5 129 | # visible: head, warn, fail 130 | out.level = 'warn' 131 | output_spy.begin() 132 | out.info('info color') 133 | out.head('head color') 134 | out.good('good color') 135 | out.warn('warn color') 136 | out.fail('fail color') 137 | out.write() 138 | assert len(output_spy.flush()) == 3 139 | # visible: head, fail 140 | out.level = 'fail' 141 | output_spy.begin() 142 | out.info('info color') 143 | out.head('head color') 144 | out.good('good color') 145 | out.warn('warn color') 146 | out.fail('fail color') 147 | out.write() 148 | assert len(output_spy.flush()) == 2 149 | # visible: head 150 | out.level = 'invalid level' 151 | output_spy.begin() 152 | out.info('info color') 153 | out.head('head color') 154 | out.good('good color') 155 | out.warn('warn color') 156 | out.fail('fail color') 157 | out.write() 158 | assert len(output_spy.flush()) == 1 159 | 160 | def test_outputbuffer_batch(self, output_spy): 161 | out = self.OutputBuffer() 162 | # visible: all 163 | output_spy.begin() 164 | out.level = 'info' 165 | out.batch = False 166 | out.info('info color') 167 | out.head('head color') 168 | out.good('good color') 169 | out.warn('warn color') 170 | out.fail('fail color') 171 | out.write() 172 | assert len(output_spy.flush()) == 5 173 | # visible: all except head 174 | output_spy.begin() 175 | out.level = 'info' 176 | out.batch = True 177 | out.info('info color') 178 | out.head('head color') 179 | out.good('good color') 180 | out.warn('warn color') 181 | out.fail('fail color') 182 | out.write() 183 | assert len(output_spy.flush()) == 4 184 | -------------------------------------------------------------------------------- /test/test_resolve.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import pytest 3 | 4 | 5 | # pylint: disable=attribute-defined-outside-init,protected-access 6 | class TestResolve: 7 | @pytest.fixture(autouse=True) 8 | def init(self, ssh_audit): 9 | self.AuditConf = ssh_audit.AuditConf 10 | self.audit = ssh_audit.audit 11 | self.OutputBuffer = ssh_audit.OutputBuffer 12 | self.ssh_socket = ssh_audit.SSH_Socket 13 | 14 | def _conf(self): 15 | conf = self.AuditConf('localhost', 22) 16 | conf.colors = False 17 | conf.batch = True 18 | return conf 19 | 20 | def test_resolve_error(self, output_spy, virtual_socket): 21 | vsocket = virtual_socket 22 | vsocket.gsock.addrinfodata['localhost#22'] = socket.gaierror(8, 'hostname nor servname provided, or not known') 23 | conf = self._conf() 24 | s = self.ssh_socket(self.OutputBuffer(), 'localhost', 22, conf.ip_version_preference) 25 | # output_spy.begin() 26 | with pytest.raises(socket.gaierror): 27 | list(s._resolve()) 28 | # lines = output_spy.flush() 29 | # assert len(lines) == 1 30 | # assert 'hostname nor servname provided' in lines[-1] 31 | 32 | def test_resolve_hostname_without_records(self, output_spy, virtual_socket): 33 | vsocket = virtual_socket 34 | vsocket.gsock.addrinfodata['localhost#22'] = [] 35 | conf = self._conf() 36 | s = self.ssh_socket(self.OutputBuffer(), 'localhost', 22, conf.ip_version_preference) 37 | output_spy.begin() 38 | r = list(s._resolve()) 39 | assert len(r) == 0 40 | 41 | def test_resolve_ipv4(self, virtual_socket): 42 | conf = self._conf() 43 | conf.ipv4 = True 44 | s = self.ssh_socket(self.OutputBuffer(), 'localhost', 22, conf.ip_version_preference) 45 | r = list(s._resolve()) 46 | assert len(r) == 1 47 | assert r[0] == (socket.AF_INET, ('127.0.0.1', 22)) 48 | 49 | def test_resolve_ipv6(self, virtual_socket): 50 | conf = self._conf() 51 | conf.ipv6 = True 52 | s = self.ssh_socket(self.OutputBuffer(), 'localhost', 22, conf.ip_version_preference) 53 | r = list(s._resolve()) 54 | assert len(r) == 1 55 | assert r[0] == (socket.AF_INET6, ('::1', 22)) 56 | 57 | def test_resolve_ipv46_both(self, virtual_socket): 58 | conf = self._conf() 59 | s = self.ssh_socket(self.OutputBuffer(), 'localhost', 22, conf.ip_version_preference) 60 | r = list(s._resolve()) 61 | assert len(r) == 2 62 | assert r[0] == (socket.AF_INET, ('127.0.0.1', 22)) 63 | assert r[1] == (socket.AF_INET6, ('::1', 22)) 64 | 65 | def test_resolve_ipv46_order(self, virtual_socket): 66 | conf = self._conf() 67 | conf.ipv4 = True 68 | conf.ipv6 = True 69 | s = self.ssh_socket(self.OutputBuffer(), 'localhost', 22, conf.ip_version_preference) 70 | r = list(s._resolve()) 71 | assert len(r) == 2 72 | assert r[0] == (socket.AF_INET, ('127.0.0.1', 22)) 73 | assert r[1] == (socket.AF_INET6, ('::1', 22)) 74 | conf = self._conf() 75 | conf.ipv6 = True 76 | conf.ipv4 = True 77 | s = self.ssh_socket(self.OutputBuffer(), 'localhost', 22, conf.ip_version_preference) 78 | r = list(s._resolve()) 79 | assert len(r) == 2 80 | assert r[0] == (socket.AF_INET6, ('::1', 22)) 81 | assert r[1] == (socket.AF_INET, ('127.0.0.1', 22)) 82 | -------------------------------------------------------------------------------- /test/test_socket.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from ssh_audit.outputbuffer import OutputBuffer 4 | from ssh_audit.ssh_socket import SSH_Socket 5 | 6 | 7 | # pylint: disable=attribute-defined-outside-init 8 | class TestSocket: 9 | @pytest.fixture(autouse=True) 10 | def init(self, ssh_audit): 11 | self.OutputBuffer = OutputBuffer 12 | self.ssh_socket = SSH_Socket 13 | 14 | def test_invalid_host(self, virtual_socket): 15 | with pytest.raises(ValueError): 16 | self.ssh_socket(self.OutputBuffer(), None, 22) 17 | 18 | def test_invalid_port(self, virtual_socket): 19 | with pytest.raises(ValueError): 20 | self.ssh_socket(self.OutputBuffer(), 'localhost', 'abc') 21 | with pytest.raises(ValueError): 22 | self.ssh_socket(self.OutputBuffer(), 'localhost', -1) 23 | with pytest.raises(ValueError): 24 | self.ssh_socket(self.OutputBuffer(), 'localhost', 0) 25 | with pytest.raises(ValueError): 26 | self.ssh_socket(self.OutputBuffer(), 'localhost', 65536) 27 | 28 | def test_not_connected_socket(self, virtual_socket): 29 | sock = self.ssh_socket(self.OutputBuffer(), 'localhost', 22) 30 | banner, header, err = sock.get_banner() 31 | assert banner is None 32 | assert len(header) == 0 33 | assert err == 'not connected' 34 | s, e = sock.recv() 35 | assert s == -1 36 | assert e == 'not connected' 37 | s, e = sock.send('nothing') 38 | assert s == -1 39 | assert e == 'not connected' 40 | s, e = sock.send_packet() 41 | assert s == -1 42 | assert e == 'not connected' 43 | -------------------------------------------------------------------------------- /test/test_ssh2_kexdb.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from ssh_audit.ssh2_kexdb import SSH2_KexDB 4 | 5 | 6 | class Test_SSH2_KexDB: 7 | 8 | @pytest.fixture(autouse=True) 9 | def init(self): 10 | self.db = SSH2_KexDB.get_db() 11 | self.pq_warning = SSH2_KexDB.WARN_NOT_PQ_SAFE 12 | 13 | def test_ssh2_kexdb(self): 14 | '''Ensures that the SSH2_KexDB.ALGORITHMS dictionary is in the right format.''' 15 | 16 | db_keys = list(self.db.keys()) 17 | db_keys.sort() 18 | 19 | # Ensure only these keys exist in the database. 20 | assert db_keys == ['enc', 'kex', 'key', 'mac'] 21 | 22 | # For 'enc', 'kex', etc... 23 | for alg_type in self.db: 24 | 25 | # Iterate over algorithms within this type (i.e.: all 'enc' algorithms, all 'kex' algorithms, etc). 26 | for alg_name in self.db[alg_type]: 27 | 28 | # Get the list of failures, warnings, etc., for this algorithm. 29 | alg_data = self.db[alg_type][alg_name] 30 | 31 | # This list must be between 1 and 4 entries long. 32 | assert 1 <= len(alg_data) <= 4 33 | 34 | # The first entry denotes the versions when this algorithm was added to OpenSSH, Dropbear, and/or libssh, followed by when it was deprecated, and finally when it was removed. Hence it must have between 0 and 3 entries. 35 | added_entry = alg_data[0] 36 | assert 0 <= len(added_entry) <= 3 37 | 38 | 39 | def test_kex_pq_unsafe(self): 40 | '''Ensures that all key exchange algorithms are marked as post-quantum unsafe, unless they appear in a whitelist.''' 41 | 42 | # These algorithms include protections against quantum attacks. 43 | kex_pq_safe = [ 44 | "ecdh-nistp256-kyber-512r3-sha256-d00@openquantumsafe.org", 45 | "ecdh-nistp384-kyber-768r3-sha384-d00@openquantumsafe.org", 46 | "ecdh-nistp521-kyber-1024r3-sha512-d00@openquantumsafe.org", 47 | "ext-info-c", 48 | "ext-info-s", 49 | "kex-strict-c-v00@openssh.com", 50 | "kex-strict-s-v00@openssh.com", 51 | "mlkem768x25519-sha256", 52 | "sntrup4591761x25519-sha512@tinyssh.org", 53 | "sntrup761x25519-sha512@openssh.com", 54 | "sntrup761x25519-sha512", 55 | "x25519-kyber-512r3-sha256-d00@amazon.com", 56 | "x25519-kyber512-sha512@aws.amazon.com", 57 | "mlkem768nistp256-sha256", # PQ safe, but has a conventional back-door. 58 | "mlkem1024nistp384-sha384" # PQ safe, but has a conventional back-door. 59 | ] 60 | 61 | failures = [] 62 | for kex_name in self.db['kex']: 63 | 64 | # Skip key exchanges that are PQ safe. 65 | if kex_name in kex_pq_safe: 66 | continue 67 | 68 | # Ensure all other kex exchanges have the proper PQ unsafe flag set in their warnings list. 69 | alg_data = self.db['kex'][kex_name] 70 | if len(alg_data) < 3 or self.pq_warning not in alg_data[2]: 71 | failures.append(kex_name) 72 | 73 | assert failures == [] 74 | -------------------------------------------------------------------------------- /test/test_utils.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | # pylint: disable=attribute-defined-outside-init 5 | class TestUtils: 6 | @pytest.fixture(autouse=True) 7 | def init(self, ssh_audit): 8 | self.utils = ssh_audit.Utils 9 | 10 | def test_to_bytes(self): 11 | assert self.utils.to_bytes(b'fran\xc3\xa7ais') == b'fran\xc3\xa7ais' 12 | assert self.utils.to_bytes('fran\xe7ais') == b'fran\xc3\xa7ais' 13 | # other 14 | with pytest.raises(TypeError): 15 | self.utils.to_bytes(123) 16 | 17 | def test_to_text(self): 18 | assert self.utils.to_text(b'fran\xc3\xa7ais') == 'fran\xe7ais' 19 | assert self.utils.to_text('fran\xe7ais') == 'fran\xe7ais' 20 | # other 21 | with pytest.raises(TypeError): 22 | self.utils.to_text(123) 23 | 24 | def test_is_ascii(self): 25 | assert self.utils.is_ascii('francais') is True 26 | assert self.utils.is_ascii('fran\xe7ais') is False 27 | # other 28 | assert self.utils.is_ascii(123) is False 29 | 30 | def test_to_ascii(self): 31 | assert self.utils.to_ascii('francais') == 'francais' 32 | assert self.utils.to_ascii('fran\xe7ais') == 'fran?ais' 33 | assert self.utils.to_ascii('fran\xe7ais', 'ignore') == 'franais' 34 | with pytest.raises(TypeError): 35 | self.utils.to_ascii(123) 36 | 37 | def test_is_print_ascii(self): 38 | assert self.utils.is_print_ascii('francais') is True 39 | assert self.utils.is_print_ascii('francais\n') is False 40 | assert self.utils.is_print_ascii('fran\xe7ais') is False 41 | # other 42 | assert self.utils.is_print_ascii(123) is False 43 | 44 | def test_to_print_ascii(self): 45 | assert self.utils.to_print_ascii('francais') == 'francais' 46 | assert self.utils.to_print_ascii('francais\n') == 'francais?' 47 | assert self.utils.to_print_ascii('fran\xe7ais') == 'fran?ais' 48 | assert self.utils.to_print_ascii('fran\xe7ais\n') == 'fran?ais?' 49 | assert self.utils.to_print_ascii('fran\xe7ais', 'ignore') == 'franais' 50 | assert self.utils.to_print_ascii('fran\xe7ais\n', 'ignore') == 'franais' 51 | with pytest.raises(TypeError): 52 | self.utils.to_print_ascii(123) 53 | 54 | def test_ctoi(self): 55 | assert self.utils.ctoi(123) == 123 56 | assert self.utils.ctoi('ABC') == 65 57 | 58 | def test_parse_int(self): 59 | assert self.utils.parse_int(123) == 123 60 | assert self.utils.parse_int('123') == 123 61 | assert self.utils.parse_int(-123) == -123 62 | assert self.utils.parse_int('-123') == -123 63 | assert self.utils.parse_int('abc') == 0 64 | 65 | def test_unique_seq(self): 66 | assert self.utils.unique_seq((1, 2, 2, 3, 3, 3)) == (1, 2, 3) 67 | assert self.utils.unique_seq((3, 3, 3, 2, 2, 1)) == (3, 2, 1) 68 | assert self.utils.unique_seq([1, 2, 2, 3, 3, 3]) == [1, 2, 3] 69 | assert self.utils.unique_seq([3, 3, 3, 2, 2, 1]) == [3, 2, 1] 70 | 71 | def test_parse_float(self): 72 | assert self.utils.parse_float('5.x') == -1.0 73 | 74 | def test_ipv6address(self): 75 | assert self.utils.is_ipv6_address('1.2.3.4') is False 76 | assert self.utils.is_ipv6_address('2600:1f18:420a:b500:bc4:c9c6:1d6:e3e4') is True 77 | -------------------------------------------------------------------------------- /test/tools/ci-win.cmd: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | IF "%PYTHON%" == "" ( 4 | ECHO PYTHON environment variable not set 5 | EXIT 1 6 | ) 7 | SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" 8 | FOR /F %%i IN ('python -c "import platform; print(platform.python_version());"') DO ( 9 | SET PYTHON_VERSION=%%i 10 | ) 11 | SET PYTHON_VERSION_MAJOR=%PYTHON_VERSION:~0,1% 12 | IF "%PYTHON_VERSION:~3,1%" == "." ( 13 | SET PYTHON_VERSION_MINOR=%PYTHON_VERSION:~2,1% 14 | ) ELSE ( 15 | SET PYTHON_VERSION_MINOR=%PYTHON_VERSION:~2,2% 16 | ) 17 | FOR /F %%i IN ('python -c "import struct; print(struct.calcsize(\"P\")*8)"') DO ( 18 | SET PYTHON_ARCH=%%i 19 | ) 20 | CALL :devenv 21 | 22 | IF /I "%1"=="" ( 23 | SET target=test 24 | ) ELSE ( 25 | SET target=%1 26 | ) 27 | 28 | echo [CI] TARGET=%target% 29 | GOTO %target% 30 | 31 | :devenv 32 | SET WIN_SDK_ROOT=C:\Program Files\Microsoft SDKs\Windows 33 | SET VS2015_ROOT=C:\Program Files (x86)\Microsoft Visual Studio 14.0 34 | IF %PYTHON_VERSION_MAJOR% == 2 ( 35 | SET WINDOWS_SDK_VERSION="v7.0" 36 | ) ELSE IF %PYTHON_VERSION_MAJOR% == 3 ( 37 | IF %PYTHON_VERSION_MAJOR% LEQ 4 ( 38 | SET WINDOWS_SDK_VERSION="v7.1" 39 | ) ELSE ( 40 | SET WINDOWS_SDK_VERSION="2015" 41 | ) 42 | ) ELSE ( 43 | ECHO Unsupported Python version: "%PYTHON_VERSION%" 44 | EXIT 1 45 | ) 46 | SETLOCAL ENABLEDELAYEDEXPANSION 47 | IF %PYTHON_ARCH% == 32 (SET PYTHON_ARCHX=x86) ELSE (SET PYTHON_ARCHX=x64) 48 | IF %WINDOWS_SDK_VERSION% == "2015" ( 49 | "%VS2015_ROOT%\VC\vcvarsall.bat" %PYTHON_ARCHX% 50 | ) ELSE ( 51 | SET DISTUTILS_USE_SDK=1 52 | SET MSSdk=1 53 | "%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Setup\WindowsSdkVer.exe" -q -version:%WINDOWS_SDK_VERSION% 54 | "%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Bin\SetEnv.cmd" /%PYTHON_ARCHX% /release 55 | ) 56 | GOTO :eof 57 | 58 | :install 59 | pip install --user --upgrade pip virtualenv 60 | SET VENV_DIR=.venv\%PYTHON_VERSION% 61 | rmdir /s /q %VENV_DIR% > nul 2>nul 62 | mkdir .venv > nul 2>nul 63 | IF "%PYTHON_VERSION_MAJOR%%PYTHON_VERSION_MINOR%" == "26" ( 64 | python -c "import virtualenv; virtualenv.main();" %VENV_DIR% 65 | ) ELSE ( 66 | python -m virtualenv %VENV_DIR% 67 | ) 68 | CALL %VENV_DIR%\Scripts\activate 69 | python -V 70 | pip install tox 71 | deactivate 72 | GOTO :eof 73 | 74 | :install_deps 75 | SET LXML_FILE= 76 | SET LXML_URL= 77 | IF %PYTHON_VERSION_MAJOR% == 3 ( 78 | IF %PYTHON_VERSION_MINOR% == 3 ( 79 | IF %PYTHON_ARCH% == 32 ( 80 | SET LXML_FILE=lxml-3.7.3.win32-py3.3.exe 81 | SET LXML_URL=https://pypi.python.org/packages/66/fd/b82a54e7a15e91184efeef4b659379d0581a73cf78239d70feb0f0877841/lxml-3.7.3.win32-py3.3.exe 82 | ) ELSE ( 83 | SET LXML_FILE=lxml-3.7.3.win-amd64-py3.3.exe 84 | SET LXML_URL=https://pypi.python.org/packages/dc/bc/4742b84793fa1fd991b5d2c6f2e5d32695659d6cfedf5c66aef9274a8723/lxml-3.7.3.win-amd64-py3.3.exe 85 | ) 86 | ) ELSE IF %PYTHON_VERSION_MINOR% == 4 ( 87 | IF %PYTHON_ARCH% == 32 ( 88 | SET LXML_FILE=lxml-3.7.3.win32-py3.4.exe 89 | SET LXML_URL=https://pypi.python.org/packages/88/33/265459d68d465ddc707621e6471989f5c2cb0d43f230f516800ffd629af7/lxml-3.7.3.win32-py3.4.exe 90 | ) ELSE ( 91 | SET LXML_FILE=lxml-3.7.3.win-amd64-py3.4.exe 92 | SET LXML_URL=https://pypi.python.org/packages/2d/65/e47db7f36a69a1b59b4f661e42d699d6c43e663b8fd91035e6f7681d017e/lxml-3.7.3.win-amd64-py3.4.exe 93 | ) 94 | ) 95 | ) 96 | IF NOT "%LXML_FILE%" == "" ( 97 | CALL :download %LXML_URL% .downloads\%LXML_FILE% 98 | easy_install --user .downloads\%LXML_FILE% 99 | ) 100 | GOTO :eof 101 | 102 | :test 103 | SET VENV_DIR=.venv\%PYTHON_VERSION% 104 | CALL %VENV_DIR%\Scripts\activate 105 | IF "%TOXENV%" == "" ( 106 | SET TOXENV=py%PYTHON_VERSION_MAJOR%%PYTHON_VERSION_MINOR% 107 | ) 108 | IF "%PYTHON_VERSION_MAJOR%%PYTHON_VERSION_MINOR%" == "26" ( 109 | SET TOX=python -c "from tox import cmdline; cmdline()" 110 | ) ELSE ( 111 | SET TOX=python -m tox 112 | ) 113 | IF %PYTHON_VERSION_MAJOR% == 3 ( 114 | IF %PYTHON_VERSION_MINOR% LEQ 4 ( 115 | :: Python 3.3 and 3.4 does not support typed-ast (mypy dependency) 116 | %TOX% --sitepackages -e %TOXENV%-test,%TOXENV%-lint,cov || EXIT 1 117 | ) ELSE ( 118 | %TOX% --sitepackages -e %TOXENV%-test,%TOXENV%-type,%TOXENV%-lint,cov || EXIT 1 119 | ) 120 | ) ELSE ( 121 | %TOX% --sitepackages -e %TOXENV%-test,%TOXENV%-lint,cov || EXIT 1 122 | ) 123 | GOTO :eof 124 | 125 | :download 126 | IF NOT EXIST %2 ( 127 | IF NOT EXIST .downloads\ mkdir .downloads 128 | powershell -command "(new-object net.webclient).DownloadFile('%1', '%2')" || EXIT 1 129 | 130 | ) 131 | GOTO :eof 132 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py{py3}-{test,pylint,flake8} 4 | py{38,39,310,311,312,313}-{test,mypy,pylint,flake8} 5 | cov 6 | skip_missing_interpreters = true 7 | 8 | [testenv] 9 | deps = 10 | test: pytest 11 | test,cov: {[testenv:cov]deps} 12 | test,py{38,39,310,311,312,313}-{type,mypy}: colorama 13 | py{38,39,310,311,312,313}-{type,mypy}: {[testenv:mypy]deps} 14 | py{py3,38,39,310,311,312,313}-{lint,pylint},lint: {[testenv:pylint]deps} 15 | py{py3,38,39,310,311,312,313}-{lint,flake8},lint: {[testenv:flake8]deps} 16 | setenv = 17 | SSHAUDIT = {toxinidir}/src 18 | test: COVERAGE_FILE = {toxinidir}/.coverage.{envname} 19 | type,mypy: MYPYPATH = {toxinidir}/test/stubs 20 | type,mypy: MYPYHTML = {toxinidir}/reports/html/mypy 21 | commands = 22 | test: coverage run --source ssh_audit -m -p -- \ 23 | test: pytest -v --junitxml={toxinidir}/reports/junit.{envname}.xml {posargs:test} 24 | test: coverage combine 25 | test: coverage report --show-missing 26 | test: coverage html -d {toxinidir}/reports/html/coverage.{envname} 27 | py{38,39,310,311,312,313}-{type,mypy}: {[testenv:mypy]commands} 28 | py{py3,38,39,310,311,312,313}-{lint,pylint},lint: {[testenv:pylint]commands} 29 | py{py3,38,39,310,311,312,313}-{lint,flake8},lint: {[testenv:flake8]commands} 30 | 31 | #ignore_outcome = 32 | # type: true 33 | # lint: true 34 | 35 | [testenv:cov] 36 | deps = 37 | coverage 38 | setenv = 39 | COVERAGE_FILE = {toxinidir}/.coverage 40 | commands = 41 | coverage erase 42 | coverage combine 43 | coverage report --show-missing 44 | coverage xml -i -o {toxinidir}/reports/coverage.xml 45 | coverage html -d {toxinidir}/reports/html/coverage 46 | 47 | [testenv:mypy] 48 | deps = 49 | colorama 50 | lxml 51 | mypy 52 | commands = 53 | mypy \ 54 | --strict \ 55 | --show-error-context \ 56 | --html-report {env:MYPYHTML}.py3.{envname} \ 57 | {posargs:{env:SSHAUDIT}} 58 | 59 | [testenv:pylint] 60 | deps = 61 | mccabe 62 | pylint 63 | commands = 64 | pylint \ 65 | --rcfile tox.ini \ 66 | --load-plugins=pylint.extensions.bad_builtin \ 67 | --load-plugins=pylint.extensions.check_elif \ 68 | --load-plugins=pylint.extensions.mccabe \ 69 | {posargs:{env:SSHAUDIT}} 70 | 71 | [testenv:flake8] 72 | deps = 73 | flake8 74 | commands = 75 | flake8 {posargs:{env:SSHAUDIT} {toxinidir}/setup.py {toxinidir}/test {toxinidir}/ssh-audit.py} --statistics 76 | 77 | [pylint] 78 | reports = no 79 | #output-format = colorized 80 | indent-string = " " 81 | disable = 82 | broad-except, 83 | duplicate-code, 84 | fixme, 85 | invalid-name, 86 | line-too-long, 87 | missing-docstring, 88 | no-else-raise, 89 | no-else-return, 90 | super-with-arguments, # Can be re-factored, at some point. 91 | too-complex, 92 | too-many-arguments, 93 | too-many-boolean-expressions, 94 | too-many-branches, 95 | too-many-instance-attributes, 96 | too-many-lines, 97 | too-many-locals, 98 | too-many-nested-blocks, 99 | too-many-positional-arguments, 100 | too-many-return-statements, 101 | too-many-statements, 102 | consider-using-f-string 103 | max-complexity = 15 104 | max-args = 8 105 | max-locals = 20 106 | max-returns = 6 107 | max-branches = 15 108 | max-statements = 60 109 | max-parents = 7 110 | max-attributes = 8 111 | min-public-methods = 1 112 | max-public-methods = 20 113 | max-bool-expr = 5 114 | max-nested-blocks = 6 115 | max-line-length = 80 116 | ignore-long-lines = ^\s*(#\s+type:\s+.*|[A-Z0-9_]+\s+=\s+.*|('.*':\s+)?\[.*\],?|assert\s+.*)$ 117 | max-module-lines = 2500 118 | 119 | [flake8] 120 | # E241 = multiple spaces after operator; should be kept for tabular data 121 | # E303 = too many blank lines 122 | # E501 = line too long 123 | ignore = E241, E303, E501 124 | 125 | [pytest] 126 | junit_family = xunit1 127 | 128 | [coverage:paths] 129 | source = 130 | src 131 | */site-packages 132 | -------------------------------------------------------------------------------- /windows_icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtesta/ssh-audit/b456bb31b977a9101a8b13e5f7137561c82f57ba/windows_icon.ico --------------------------------------------------------------------------------