Flake8 fixes (#35)

* Apply Flake8 also on `setup.py`

modified:   tox.ini

* Fix W605 - invalid escape syntax

modified:   packages/setup.py
modified:   tox.ini

* Update comment about Flake8: W504

W503 and W504 are mutual exclusive - so we have to keep one of them.

modified:   tox.ini

* Fix F841 - variable assigned but never used

modified:   ssh-audit.py
modified:   tox.ini

* Fix E741 - ambiguous variable name 'l'

modified:   ssh-audit.py
modified:   tox.ini

* Fix E712 - comparison to False should be 'if cond is False'

... and not 'if conf == False'.

modified:   ssh-audit.py
modified:   tox.ini

* Fix E711 - comparison to None should be 'if cond is not None'

... and not 'if cond != None'.

modified:   ssh-audit.py
modified:   tox.ini

* Fix E305 - expected 2 blank lines

... after class or function definition, found 1.

modified:   ssh-audit.py
modified:   tox.ini

* Fix E303 - too many blank lines

modified:   ssh-audit.py
modified:   tox.ini

* Fix E303 - too many blank lines

modified:   ssh-audit.py
modified:   tox.ini

* Fix E301 - expected 1 blank line, found 0

No code change necessary, probably fixed by another commit.

modified:   tox.ini

* Fix E265 - block comment should start with '# '

There is lots of commented out code, which usually should be just
deleted.

I will keep it for now, as I am not yet very familiar with the code
base.

modified:   ssh-audit.py
modified:   tox.ini

* Fix E261 - at least two spaces before inline comment

modified:   ssh-audit.py
modified:   tox.ini

* Fix E251 - unexpected spaces around keyword / parameter equals

modified:   packages/setup.py
modified:   tox.ini

* Fix E231 - missing whitespace after ','

No code change necessary, probably fixed by previous commit.

modified:   tox.ini

* Fix E226 - missing whitespace around arithmetic operator

modified:   ssh-audit.py
modified:   tox.ini

* Fix W293 - blank line contains whitespace

modified:   ssh-audit.py
modified:   tox.ini

* Fix E221 - multiple spaces before operator

modified:   ssh-audit.py
modified:   tox.ini

* Update comment about Flake 8 E241

Lots of data is formatted as tables, so this warning is disabled for a
good reason.

modified:   tox.ini

* Fix E401 - multiple imports on one line

modified:   ssh-audit.py
modified:   tox.ini

* Do not ignore Flake8 warning F401

... as there were no errors in source code anyway.

modified:   tox.ini

* Fix F821 - undefined name

modified:   ssh-audit.py
modified:   tox.ini

* Reformat ignore section for Flake8

modified:   tox.ini

* Flake8 test suite

modified:   test/conftest.py
modified:   test/test_auditconf.py
modified:   test/test_banner.py
modified:   test/test_buffer.py
modified:   test/test_errors.py
modified:   test/test_output.py
modified:   test/test_resolve.py
modified:   test/test_socket.py
modified:   test/test_software.py
modified:   test/test_ssh1.py
modified:   test/test_ssh2.py
modified:   test/test_ssh_algorithm.py
modified:   test/test_utils.py
modified:   test/test_version_compare.py
modified:   tox.ini
This commit is contained in:
Jürgen Gmach 2020-06-09 23:54:07 +02:00 committed by GitHub
parent 29d874b450
commit 246a41d46f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 459 additions and 485 deletions

View File

@ -5,7 +5,7 @@ import re
from setuptools import setup
version = re.search('^VERSION\s*=\s*\'v(\d\.\d\.\d)\'', open('sshaudit/sshaudit.py').read(), re.M).group(1)
version = re.search(r'^VERSION\s*=\s*\'v(\d\.\d\.\d)\'', open('sshaudit/sshaudit.py').read(), re.M).group(1)
print("\n\nPackaging ssh-audit v%s...\n\n" % version)
with open("sshaudit/README.md", "rb") as f:
@ -13,20 +13,20 @@ with open("sshaudit/README.md", "rb") as f:
setup(
name = "ssh-audit",
packages = ["sshaudit"],
license = 'MIT',
entry_points = {
name="ssh-audit",
packages=["sshaudit"],
license='MIT',
entry_points={
"console_scripts": ['ssh-audit = sshaudit.sshaudit:main']
},
version = version,
description = "An SSH server & client configuration security auditing tool",
long_description = long_descr,
long_description_content_type = "text/markdown",
author = "Joe Testa",
author_email = "jtesta@positronsecurity.com",
url = "https://github.com/jtesta/ssh-audit",
classifiers = [
version=version,
description="An SSH server & client configuration security auditing tool",
long_description=long_descr,
long_description_content_type="text/markdown",
author="Joe Testa",
author_email="jtesta@positronsecurity.com",
url="https://github.com/jtesta/ssh-audit",
classifiers=[
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Information Technology",
"Intended Audience :: System Administrators",

View File

@ -25,11 +25,24 @@
THE SOFTWARE.
"""
from __future__ import print_function
import base64, binascii, errno, hashlib, getopt, io, os, random, re, select, socket, struct, sys, json
import base64
import binascii
import errno
import getopt
import hashlib
import io
import json
import os
import random
import re
import select
import socket
import struct
import sys
VERSION = 'v2.2.1-dev'
SSH_HEADER = 'SSH-{0}-OpenSSH_8.0' # SSH software to impersonate
SSH_HEADER = 'SSH-{0}-OpenSSH_8.0' # SSH software to impersonate
if sys.version_info.major < 3:
print("\n!!!! NOTE: Python 2 is being considered for deprecation. If you have a good reason to need continued Python 2 support, please e-mail jtesta@positronsecurity.com with your rationale.\n\n")
@ -41,7 +54,7 @@ if sys.version_info >= (3,): # pragma: nocover
else: # pragma: nocover
import StringIO as _StringIO # pylint: disable=import-error
StringIO = BytesIO = _StringIO.StringIO
text_type = unicode # pylint: disable=undefined-variable
text_type = unicode # pylint: disable=undefined-variable # noqa: F821
binary_type = str
try: # pragma: nocover
# pylint: disable=unused-import
@ -99,7 +112,7 @@ class AuditConf(object):
self.ipv4 = False
self.ipv6 = False
self.timeout = 5.0
self.timeout_set = False # Set to True when the user explicitly sets it.
self.timeout_set = False # Set to True when the user explicitly sets it.
def __setattr__(self, name, value):
# type: (str, Union[str, int, bool, Sequence[int]]) -> None
@ -190,9 +203,9 @@ class AuditConf(object):
elif o in ('-t', '--timeout'):
aconf.timeout = float(a)
aconf.timeout_set = True
if len(args) == 0 and aconf.client_audit == False:
if len(args) == 0 and aconf.client_audit is False:
usage_cb()
if aconf.client_audit == False:
if aconf.client_audit is False:
if oport is not None:
host = args[0]
else:
@ -310,7 +323,7 @@ class SSH2(object): # pylint: disable=too-few-public-methods
WARN_OPENSSH74_UNSAFE = 'disabled (in client) since OpenSSH 7.4, unsafe algorithm'
WARN_OPENSSH72_LEGACY = 'disabled (in client) since OpenSSH 7.2, legacy algorithm'
FAIL_OPENSSH70_LEGACY = 'removed since OpenSSH 7.0, legacy algorithm'
FAIL_OPENSSH70_WEAK = 'removed (in server) and disabled (in client) since OpenSSH 7.0, weak algorithm'
FAIL_OPENSSH70_WEAK = 'removed (in server) and disabled (in client) since OpenSSH 7.0, weak algorithm'
FAIL_OPENSSH70_LOGJAM = 'disabled (in client) since OpenSSH 7.0, logjam attack'
INFO_OPENSSH69_CHACHA = 'default cipher since OpenSSH 6.9.'
FAIL_OPENSSH67_UNSAFE = 'removed (in server) since OpenSSH 6.7, unsafe algorithm'
@ -319,21 +332,21 @@ class SSH2(object): # pylint: disable=too-few-public-methods
FAIL_DBEAR67_DISABLED = 'disabled since Dropbear SSH 2015.67'
FAIL_DBEAR53_DISABLED = 'disabled since Dropbear SSH 0.53'
FAIL_DEPRECATED_CIPHER = 'deprecated cipher'
FAIL_WEAK_CIPHER = 'using weak cipher'
FAIL_WEAK_ALGORITHM = 'using weak/obsolete algorithm'
FAIL_PLAINTEXT = 'no encryption/integrity'
FAIL_DEPRECATED_MAC = 'deprecated MAC'
WARN_CURVES_WEAK = 'using weak elliptic curves'
WARN_RNDSIG_KEY = 'using weak random number generator could reveal the key'
WARN_MODULUS_SIZE = 'using small 1024-bit modulus'
WARN_HASH_WEAK = 'using weak hashing algorithm'
WARN_CIPHER_MODE = 'using weak cipher mode'
WARN_BLOCK_SIZE = 'using small 64-bit block size'
WARN_CIPHER_WEAK = 'using weak cipher'
WARN_ENCRYPT_AND_MAC = 'using encrypt-and-MAC mode'
WARN_TAG_SIZE = 'using small 64-bit tag size'
WARN_TAG_SIZE_96 = 'using small 96-bit tag size'
WARN_EXPERIMENTAL = 'using experimental algorithm'
FAIL_WEAK_CIPHER = 'using weak cipher'
FAIL_WEAK_ALGORITHM = 'using weak/obsolete algorithm'
FAIL_PLAINTEXT = 'no encryption/integrity'
FAIL_DEPRECATED_MAC = 'deprecated MAC'
WARN_CURVES_WEAK = 'using weak elliptic curves'
WARN_RNDSIG_KEY = 'using weak random number generator could reveal the key'
WARN_MODULUS_SIZE = 'using small 1024-bit modulus'
WARN_HASH_WEAK = 'using weak hashing algorithm'
WARN_CIPHER_MODE = 'using weak cipher mode'
WARN_BLOCK_SIZE = 'using small 64-bit block size'
WARN_CIPHER_WEAK = 'using weak cipher'
WARN_ENCRYPT_AND_MAC = 'using encrypt-and-MAC mode'
WARN_TAG_SIZE = 'using small 64-bit tag size'
WARN_TAG_SIZE_96 = 'using small 96-bit tag size'
WARN_EXPERIMENTAL = 'using experimental algorithm'
ALGORITHMS = {
# Format: 'algorithm_name': [['version_first_appeared_in'], [reason_for_failure1, reason_for_failure2, ...], [warning1, warning2, ...]]
@ -378,7 +391,7 @@ class SSH2(object): # pylint: disable=too-few-public-methods
'ecdh-sha2-nistp384': [['5.7,d2013.62'], [WARN_CURVES_WEAK]],
'ecdh-sha2-nistp521': [['5.7,d2013.62'], [WARN_CURVES_WEAK]],
'ecdh-sha2-nistt571': [[], [WARN_CURVES_WEAK]],
'ecdh-sha2-1.3.132.0.10': [[]], # ECDH over secp256k1 (i.e.: the Bitcoin curve)
'ecdh-sha2-1.3.132.0.10': [[]], # ECDH over secp256k1 (i.e.: the Bitcoin curve)
'curve25519-sha256@libssh.org': [['6.5,d2013.62,l10.6.0']],
'curve25519-sha256': [['7.4,d2018.76']],
'curve448-sha512': [[]],
@ -386,8 +399,8 @@ class SSH2(object): # pylint: disable=too-few-public-methods
'rsa1024-sha1': [[], [], [WARN_MODULUS_SIZE, WARN_HASH_WEAK]],
'rsa2048-sha256': [[]],
'sntrup4591761x25519-sha512@tinyssh.org': [['8.0'], [], [WARN_EXPERIMENTAL]],
'ext-info-c': [[]], # Extension negotiation (RFC 8308)
'ext-info-s': [[]], # Extension negotiation (RFC 8308)
'ext-info-c': [[]], # Extension negotiation (RFC 8308)
'ext-info-s': [[]], # Extension negotiation (RFC 8308)
},
'key': {
'ssh-rsa1': [[], [FAIL_WEAK_ALGORITHM]],
@ -400,7 +413,7 @@ class SSH2(object): # pylint: disable=too-few-public-methods
'ecdsa-sha2-nistp256': [['5.7,d2013.62,l10.6.4'], [WARN_CURVES_WEAK], [WARN_RNDSIG_KEY]],
'ecdsa-sha2-nistp384': [['5.7,d2013.62,l10.6.4'], [WARN_CURVES_WEAK], [WARN_RNDSIG_KEY]],
'ecdsa-sha2-nistp521': [['5.7,d2013.62,l10.6.4'], [WARN_CURVES_WEAK], [WARN_RNDSIG_KEY]],
'ecdsa-sha2-1.3.132.0.10': [[], [], [WARN_RNDSIG_KEY]], # ECDSA over secp256k1 (i.e.: the Bitcoin curve)
'ecdsa-sha2-1.3.132.0.10': [[], [], [WARN_RNDSIG_KEY]], # ECDSA over secp256k1 (i.e.: the Bitcoin curve)
'x509v3-sign-dss': [[], [FAIL_OPENSSH70_WEAK], [WARN_MODULUS_SIZE, WARN_RNDSIG_KEY]],
'x509v3-sign-rsa': [[], [], [WARN_HASH_WEAK]],
'x509v3-sign-rsa-sha256@ssh.com': [[]],
@ -416,7 +429,7 @@ class SSH2(object): # pylint: disable=too-few-public-methods
'rsa-sha2-256-cert-v01@openssh.com': [['7.8']],
'rsa-sha2-512-cert-v01@openssh.com': [['7.8']],
'ssh-rsa-sha256@ssh.com': [[]],
'ecdsa-sha2-1.3.132.0.10': [[], [], [WARN_RNDSIG_KEY]], # ECDSA over secp256k1 (i.e.: the Bitcoin curve)
'ecdsa-sha2-1.3.132.0.10': [[], [], [WARN_RNDSIG_KEY]], # ECDSA over secp256k1 (i.e.: the Bitcoin curve)
'sk-ecdsa-sha2-nistp256-cert-v01@openssh.com': [['8.2'], [WARN_CURVES_WEAK], [WARN_RNDSIG_KEY]],
'sk-ecdsa-sha2-nistp256@openssh.com': [['8.2'], [WARN_CURVES_WEAK], [WARN_RNDSIG_KEY]],
'sk-ssh-ed25519-cert-v01@openssh.com': [['8.2']],
@ -508,20 +521,20 @@ class SSH2(object): # pylint: disable=too-few-public-methods
'umac-128@openssh.com': [['6.2'], [], [WARN_ENCRYPT_AND_MAC]],
'hmac-sha1-etm@openssh.com': [['6.2'], [], [WARN_HASH_WEAK]],
'hmac-sha1-96-etm@openssh.com': [['6.2', '6.6', None], [FAIL_OPENSSH67_UNSAFE], [WARN_HASH_WEAK]],
'hmac-sha2-256-96-etm@openssh.com': [[], [], [WARN_TAG_SIZE_96]], # Despite the @openssh.com tag, it doesn't appear that this was ever shipped with OpenSSH; it is only implemented in AsyncSSH (?).
'hmac-sha2-512-96-etm@openssh.com': [[], [], [WARN_TAG_SIZE_96]], # Despite the @openssh.com tag, it doesn't appear that this was ever shipped with OpenSSH; it is only implemented in AsyncSSH (?).
'hmac-sha2-256-96-etm@openssh.com': [[], [], [WARN_TAG_SIZE_96]], # Despite the @openssh.com tag, it doesn't appear that this was ever shipped with OpenSSH; it is only implemented in AsyncSSH (?).
'hmac-sha2-512-96-etm@openssh.com': [[], [], [WARN_TAG_SIZE_96]], # Despite the @openssh.com tag, it doesn't appear that this was ever shipped with OpenSSH; it is only implemented in AsyncSSH (?).
'hmac-sha2-256-etm@openssh.com': [['6.2']],
'hmac-sha2-512-etm@openssh.com': [['6.2']],
'hmac-md5-etm@openssh.com': [['6.2', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_HASH_WEAK]],
'hmac-md5-96-etm@openssh.com': [['6.2', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_HASH_WEAK]],
'hmac-ripemd160-etm@openssh.com': [['6.2', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY]],
'umac-32@openssh.com': [[], [], [WARN_ENCRYPT_AND_MAC, WARN_TAG_SIZE]], # Despite having the @openssh.com suffix, this may never have shipped with OpenSSH (!).
'umac-32@openssh.com': [[], [], [WARN_ENCRYPT_AND_MAC, WARN_TAG_SIZE]], # Despite having the @openssh.com suffix, this may never have shipped with OpenSSH (!).
'umac-64-etm@openssh.com': [['6.2'], [], [WARN_TAG_SIZE]],
'umac-96@openssh.com': [[], [], [WARN_ENCRYPT_AND_MAC]], # Despite having the @openssh.com suffix, this may never have shipped with OpenSSH (!).
'umac-96@openssh.com': [[], [], [WARN_ENCRYPT_AND_MAC]], # Despite having the @openssh.com suffix, this may never have shipped with OpenSSH (!).
'umac-128-etm@openssh.com': [['6.2']],
'aes128-gcm': [[]],
'aes256-gcm': [[]],
'chacha20-poly1305@openssh.com': [[]], # Despite the @openssh.com tag, this was never shipped as a MAC in OpenSSH (only as a cipher); it is only implemented as a MAC in Syncplify.
'chacha20-poly1305@openssh.com': [[]], # Despite the @openssh.com tag, this was never shipped as a MAC in OpenSSH (only as a cipher); it is only implemented as a MAC in Syncplify.
}
} # type: Dict[str, Dict[str, List[List[Optional[str]]]]]
@ -700,7 +713,7 @@ class SSH2(object): # pylint: disable=too-few-public-methods
'ecdh-sha2-nistp256': KexNISTP256,
'ecdh-sha2-nistp384': KexNISTP384,
'ecdh-sha2-nistp521': KexNISTP521,
#'kexguess2@matt.ucc.asn.au': ???
# 'kexguess2@matt.ucc.asn.au': ???
}
# Pick the first kex algorithm that the server supports, which we
@ -735,14 +748,14 @@ class SSH2(object): # pylint: disable=too-few-public-methods
# If the connection is closed, re-open it and get the kex again.
if not s.is_connected():
s.connect()
unused = None # pylint: disable=unused-variable
unused = None # pylint: disable=unused-variable
unused, unused, err = s.get_banner()
if err is not None:
s.close()
return
# Parse the server's initial KEX.
packet_type = 0 # pylint: disable=unused-variable
packet_type = 0 # pylint: disable=unused-variable
packet_type, payload = s.read_packet()
SSH2.Kex.parse(payload)
@ -798,7 +811,6 @@ class SSH2(object): # pylint: disable=too-few-public-methods
else:
host_key_types[host_key_type]['parsed'] = True
# Performs DH group exchanges to find what moduli are supported, and checks
# their size.
class GEXTest(object):
@ -811,14 +823,14 @@ class SSH2(object): # pylint: disable=too-few-public-methods
return
s.connect()
unused = None # pylint: disable=unused-variable
unused = None # pylint: disable=unused-variable
unused, unused, err = s.get_banner()
if err is not None:
s.close()
return False
# Parse the server's initial KEX.
packet_type = 0 # pylint: disable=unused-variable
packet_type = 0 # pylint: disable=unused-variable
packet_type, payload = s.read_packet(2)
kex = SSH2.Kex.parse(payload)
@ -865,7 +877,7 @@ class SSH2(object): # pylint: disable=too-few-public-methods
# got here, doesn't mean the server is vulnerable...
smallest_modulus = kex_group.get_dh_modulus_size()
except Exception as e: # pylint: disable=bare-except
except Exception: # pylint: disable=bare-except
pass
finally:
s.close()
@ -887,9 +899,9 @@ class SSH2(object): # pylint: disable=too-few-public-methods
kex_group.send_init_gex(s, bits, bits, bits)
kex_group.recv_reply(s, False)
smallest_modulus = kex_group.get_dh_modulus_size()
except Exception as e: # pylint: disable=bare-except
#import traceback
#print(traceback.format_exc())
except Exception: # pylint: disable=bare-except
# import traceback
# print(traceback.format_exc())
pass
finally:
# The server is in a state that is not re-testable,
@ -897,7 +909,6 @@ class SSH2(object): # pylint: disable=too-few-public-methods
# connection.
s.close()
if smallest_modulus > 0:
kex.set_dh_modulus_size(gex_alg, smallest_modulus)
@ -919,6 +930,7 @@ class SSH2(object): # pylint: disable=too-few-public-methods
if reconnect_failed:
break
class SSH1(object):
class CRC32(object):
def __init__(self):
@ -935,8 +947,8 @@ class SSH1(object):
def calc(self, v):
# type: (binary_type) -> int
crc, l = 0, len(v)
for i in range(l):
crc, length = 0, len(v)
for i in range(length):
n = ord(v[i:i + 1])
n = n ^ (crc & 0xff)
crc = (crc >> 8) ^ self._table[n]
@ -955,11 +967,11 @@ class SSH1(object):
class KexDB(object): # pylint: disable=too-few-public-methods
# pylint: disable=bad-whitespace
FAIL_PLAINTEXT = 'no encryption/integrity'
FAIL_PLAINTEXT = 'no encryption/integrity'
FAIL_OPENSSH37_REMOVE = 'removed since OpenSSH 3.7'
FAIL_NA_BROKEN = 'not implemented in OpenSSH, broken algorithm'
FAIL_NA_UNSAFE = 'not implemented in OpenSSH (server), unsafe algorithm'
TEXT_CIPHER_IDEA = 'cipher used by commercial SSH'
FAIL_NA_BROKEN = 'not implemented in OpenSSH, broken algorithm'
FAIL_NA_UNSAFE = 'not implemented in OpenSSH (server), unsafe algorithm'
TEXT_CIPHER_IDEA = 'cipher used by commercial SSH'
ALGORITHMS = {
'key': {
@ -1189,6 +1201,7 @@ class ReadBuf(object):
self._len = 0
super(ReadBuf, self).reset()
class WriteBuf(object):
def __init__(self, data=None):
# type: (Optional[binary_type]) -> None
@ -1285,15 +1298,15 @@ class SSH(object): # pylint: disable=too-few-public-methods
class Protocol(object): # pylint: disable=too-few-public-methods
# pylint: disable=bad-whitespace
SMSG_PUBLIC_KEY = 2
MSG_DEBUG = 4
MSG_KEXINIT = 20
MSG_NEWKEYS = 21
MSG_KEXDH_INIT = 30
MSG_DEBUG = 4
MSG_KEXINIT = 20
MSG_NEWKEYS = 21
MSG_KEXDH_INIT = 30
MSG_KEXDH_REPLY = 31
MSG_KEXDH_GEX_REQUEST = 34
MSG_KEXDH_GEX_GROUP = 31
MSG_KEXDH_GEX_INIT = 32
MSG_KEXDH_GEX_REPLY = 33
MSG_KEXDH_GEX_GROUP = 31
MSG_KEXDH_GEX_INIT = 32
MSG_KEXDH_GEX_REPLY = 33
class Product(object): # pylint: disable=too-few-public-methods
OpenSSH = 'OpenSSH'
@ -1609,7 +1622,7 @@ class SSH(object): # pylint: disable=too-few-public-methods
def __getitem__(self, product):
# type: (str) -> Sequence[Optional[str]]
return tuple(self.__storage.get(product, [None]*4))
return tuple(self.__storage.get(product, [None] * 4))
def __str__(self):
# type: () -> str
@ -1640,7 +1653,7 @@ class SSH(object): # pylint: disable=too-few-public-methods
ssh_versions[ssh_prod] = ssh_ver
for ssh_product, ssh_version in ssh_versions.items():
if ssh_product not in self.__storage:
self.__storage[ssh_product] = [None]*4
self.__storage[ssh_product] = [None] * 4
prev = self[ssh_product][pos]
if (prev is None or
(prev < ssh_version and pos % 2 == 0) or
@ -1783,19 +1796,18 @@ class SSH(object): # pylint: disable=too-few-public-methods
if software is not None:
if software.product not in vproducts:
unknown_software = True
#
# The code below is commented out because it would try to guess what the server is,
# usually resulting in wild & incorrect recommendations.
#
# if software is None:
# ssh_timeframe = self.get_ssh_timeframe(for_server)
# for product in vproducts:
# if product not in ssh_timeframe:
# continue
# version = ssh_timeframe.get_from(product, for_server)
# if version is not None:
# software = SSH.Software(None, product, version, None, None)
# break
# The code below is commented out because it would try to guess what the server is,
# usually resulting in wild & incorrect recommendations.
# if software is None:
# ssh_timeframe = self.get_ssh_timeframe(for_server)
# for product in vproducts:
# if product not in ssh_timeframe:
# continue
# version = ssh_timeframe.get_from(product, for_server)
# if version is not None:
# software = SSH.Software(None, product, version, None, None)
# break
rec = {} # type: Dict[int, Dict[str, Dict[str, Dict[str, int]]]]
if software is None:
unknown_software = True
@ -2057,7 +2069,6 @@ class SSH(object): # pylint: disable=too-few-public-methods
self.client_host = None
self.client_port = None
def _resolve(self, ipvo):
# type: (Sequence[int]) -> Iterable[Tuple[int, Tuple[Any, ...]]]
ipvo = tuple([x for x in utils.unique_seq(ipvo) if x in (4, 6)])
@ -2081,7 +2092,6 @@ class SSH(object): # pylint: disable=too-few-public-methods
out.fail('[exception] {0}'.format(e))
sys.exit(1)
# Listens on a server socket and accepts one connection (used for
# auditing client connections).
def listen_and_accept(self):
@ -2093,7 +2103,7 @@ class SSH(object): # pylint: disable=too-few-public-methods
s.bind(('0.0.0.0', self.__port))
s.listen()
self.__sock_map[s.fileno()] = s
except Exception as e:
except Exception:
print("Warning: failed to listen on any IPv4 interfaces.")
pass
@ -2105,7 +2115,7 @@ class SSH(object): # pylint: disable=too-few-public-methods
s.bind(('::', self.__port))
s.listen()
self.__sock_map[s.fileno()] = s
except Exception as e:
except Exception:
print("Warning: failed to listen on any IPv6 interfaces.")
pass
@ -2139,7 +2149,6 @@ class SSH(object): # pylint: disable=too-few-public-methods
c.settimeout(self.__timeout)
self.__sock = c
def connect(self):
# type: () -> None
err = None
@ -2169,10 +2178,10 @@ class SSH(object): # pylint: disable=too-few-public-methods
banner = SSH_HEADER.format('1.5' if sshv == 1 else '2.0')
if self.__state < self.SM_BANNER_SENT:
self.send_banner(banner)
# rto = self.__sock.gettimeout()
# self.__sock.settimeout(0.7)
# rto = self.__sock.gettimeout()
# self.__sock.settimeout(0.7)
s, e = self.recv()
# self.__sock.settimeout(rto)
# self.__sock.settimeout(rto)
if s < 0:
return self.__banner, self.__header, e
e = None
@ -2352,12 +2361,11 @@ class KexDH(object): # pragma: nocover
self.__hostkey_type = None
self.__hostkey_e = 0
self.__hostkey_n = 0
self.__hostkey_n_len = 0 # Length of the host key modulus.
self.__ca_n_len = 0 # Length of the CA key modulus (if hostkey is a cert).
self.__hostkey_n_len = 0 # Length of the host key modulus.
self.__ca_n_len = 0 # Length of the CA key modulus (if hostkey is a cert).
self.__f = 0
self.__h_sig = 0
def set_params(self, g, p):
self.__g = g
self.__p = p
@ -2365,7 +2373,6 @@ class KexDH(object): # pragma: nocover
self.__x = 0
self.__e = 0
def send_init(self, s, init_msg=SSH.Protocol.MSG_KEXDH_INIT):
# type: (SSH.Socket) -> None
r = random.SystemRandom()
@ -2395,17 +2402,16 @@ class KexDH(object): # pragma: nocover
return None
hostkey_len = f_len = h_sig_len = 0 # pylint: disable=unused-variable
hostkey_type_len = hostkey_e_len = 0 # pylint: disable=unused-variable
key_id_len = principles_len = 0 # pylint: disable=unused-variable
critical_options_len = extensions_len = 0 # pylint: disable=unused-variable
nonce_len = ca_key_len = ca_key_type_len = 0 # pylint: disable=unused-variable
hostkey_type_len = hostkey_e_len = 0 # pylint: disable=unused-variable
key_id_len = principles_len = 0 # pylint: disable=unused-variable
critical_options_len = extensions_len = 0 # pylint: disable=unused-variable
nonce_len = ca_key_len = ca_key_type_len = 0 # pylint: disable=unused-variable
ca_key_len = ca_key_type_len = ca_key_e_len = 0 # pylint: disable=unused-variable
key_id = principles = None # pylint: disable=unused-variable
critical_options = extensions = None # pylint: disable=unused-variable
valid_after = valid_before = None # pylint: disable=unused-variable
key_id = principles = None # pylint: disable=unused-variable
critical_options = extensions = None # pylint: disable=unused-variable
nonce = ca_key = ca_key_type = None # pylint: disable=unused-variable
ca_key_e = ca_key_n = None # pylint: disable=unused-variable
ca_key_e = ca_key_n = None # pylint: disable=unused-variable
# Get the host key blob, F, and signature.
ptr = 0
@ -2455,12 +2461,10 @@ class KexDH(object): # pragma: nocover
# The principles, which are... I don't know what.
principles, principles_len, ptr = KexDH.__get_bytes(hostkey, ptr)
# The timestamp that this certificate is valid after.
valid_after = hostkey[ptr:ptr + 8]
# Skip over the timestamp that this certificate is valid after.
ptr += 8
# The timestamp that this certificate is valid before.
valid_before = hostkey[ptr:ptr + 8]
# Skip over the timestamp that this certificate is valid before.
ptr += 8
# TODO: validate the principles, and time range.
@ -2895,7 +2899,7 @@ def output_fingerprints(algs, sha256=True):
if algs.ssh1kex is not None:
name = 'ssh-rsa1'
fp = SSH.Fingerprint(algs.ssh1kex.host_key_fingerprint_data)
#bits = algs.ssh1kex.host_key_bits
# bits = algs.ssh1kex.host_key_bits
fps.append((name, fp))
if algs.ssh2kex is not None:
host_keys = algs.ssh2kex.host_keys()
@ -2917,8 +2921,8 @@ def output_fingerprints(algs, sha256=True):
for fpp in fps:
name, fp = fpp
fpo = fp.sha256 if sha256 else fp.md5
#p = '' if out.batch else ' ' * (padlen - len(name))
#out.good('(fin) {0}{1} -- {2} {3}'.format(name, p, bits, fpo))
# p = '' if out.batch else ' ' * (padlen - len(name))
# out.good('(fin) {0}{1} -- {2} {3}'.format(name, p, bits, fpo))
out.good('(fin) {0}: {1}'.format(name, fpo))
if len(obuf) > 0:
out.head('# fingerprints')
@ -2994,7 +2998,7 @@ def output_recommendations(algs, software, padlen=0):
else:
title = ''
out.head('# algorithm recommendations {0}'.format(title))
obuf.flush(True) # Sort the output so that it is always stable (needed for repeatable testing).
obuf.flush(True) # Sort the output so that it is always stable (needed for repeatable testing).
out.sep()
return ret
@ -3018,7 +3022,7 @@ def output_info(algs, software, client_audit, any_problems, padlen=0):
def output(banner, header, client_host=None, kex=None, pkm=None):
# type: (Optional[SSH.Banner], List[text_type], Optional[SSH2.Kex], Optional[SSH1.PublicKeyMessage]) -> None
client_audit = (client_host != None) # If set, this is a client audit.
client_audit = client_host is not None # If set, this is a client audit.
sshv = 1 if pkm is not None else 2
algs = SSH.Algorithms(pkm, kex)
with OutputBuffer() as obuf:
@ -3077,11 +3081,11 @@ def output(banner, header, client_host=None, kex=None, pkm=None):
perfect_config = output_recommendations(algs, software, maxlen)
output_info(algs, software, client_audit, not perfect_config)
# If we encountered any unknown algorithms, ask the user to report them.
if len(unknown_algorithms) > 0:
out.warn("\n\n!!! WARNING: unknown algorithm(s) found!: %s. Please email the full output above to the maintainer (jtesta@positronsecurity.com), or create a Github issue at <https://github.com/jtesta/ssh-audit/issues>.\n" % ','.join(unknown_algorithms))
class Utils(object):
@classmethod
def _type_err(cls, v, target):
@ -3204,6 +3208,7 @@ class Utils(object):
except: # pylint: disable=bare-except
return -1.0
def build_struct(banner, kex=None, pkm=None, client_host=None):
res = {
"banner": {
@ -3281,6 +3286,7 @@ def build_struct(banner, kex=None, pkm=None, client_host=None):
return res
def audit(aconf, sshv=None):
# type: (AuditConf, Optional[int]) -> None
out.batch = aconf.batch
@ -3350,9 +3356,11 @@ def audit(aconf, sshv=None):
utils = Utils()
out = Output()
def main():
conf = AuditConf.from_cmdline(sys.argv[1:], usage)
audit(conf)
if __name__ == '__main__': # pragma: nocover
main()

View File

@ -52,7 +52,6 @@ class TestErrors(object):
assert 'timed out' in lines[-1]
def test_recv_empty(self, output_spy, virtual_socket):
vsocket = virtual_socket
lines = self._audit(output_spy)
assert len(lines) == 1
assert 'did not receive banner' in lines[-1]

View File

@ -25,7 +25,7 @@ class TestResolve(object):
conf = self._conf()
output_spy.begin()
with pytest.raises(SystemExit):
r = list(s._resolve(conf.ipvo))
list(s._resolve(conf.ipvo))
lines = output_spy.flush()
assert len(lines) == 1
assert 'hostname nor servname provided' in lines[-1]
@ -40,7 +40,6 @@ class TestResolve(object):
assert len(r) == 0
def test_resolve_ipv4(self, virtual_socket):
vsocket = virtual_socket
conf = self._conf()
conf.ipv4 = True
s = self.ssh.Socket('localhost', 22)
@ -49,7 +48,6 @@ class TestResolve(object):
assert r[0] == (socket.AF_INET, ('127.0.0.1', 22))
def test_resolve_ipv6(self, virtual_socket):
vsocket = virtual_socket
s = self.ssh.Socket('localhost', 22)
conf = self._conf()
conf.ipv6 = True
@ -58,7 +56,6 @@ class TestResolve(object):
assert r[0] == (socket.AF_INET6, ('::1', 22))
def test_resolve_ipv46_both(self, virtual_socket):
vsocket = virtual_socket
s = self.ssh.Socket('localhost', 22)
conf = self._conf()
r = list(s._resolve(conf.ipvo))
@ -67,7 +64,6 @@ class TestResolve(object):
assert r[1] == (socket.AF_INET6, ('::1', 22))
def test_resolve_ipv46_order(self, virtual_socket):
vsocket = virtual_socket
s = self.ssh.Socket('localhost', 22)
conf = self._conf()
conf.ipv4 = True

View File

@ -1,6 +1,5 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import socket
import pytest
@ -12,17 +11,17 @@ class TestSocket(object):
def test_invalid_host(self, virtual_socket):
with pytest.raises(ValueError):
s = self.ssh.Socket(None, 22)
self.ssh.Socket(None, 22)
def test_invalid_port(self, virtual_socket):
with pytest.raises(ValueError):
s = self.ssh.Socket('localhost', 'abc')
self.ssh.Socket('localhost', 'abc')
with pytest.raises(ValueError):
s = self.ssh.Socket('localhost', -1)
self.ssh.Socket('localhost', -1)
with pytest.raises(ValueError):
s = self.ssh.Socket('localhost', 0)
self.ssh.Socket('localhost', 0)
with pytest.raises(ValueError):
s = self.ssh.Socket('localhost', 65536)
self.ssh.Socket('localhost', 65536)
def test_not_connected_socket(self, virtual_socket):
sock = self.ssh.Socket('localhost', 22)

View File

@ -95,10 +95,10 @@ class TestSSH1(object):
skey, hkey = self._server_key(), self._host_key()
pkm = self.ssh1.PublicKeyMessage(cookie, skey, hkey, pflags, cmask, amask)
self._assert_pkm_fields(pkm, skey, hkey)
for skey2 in ([], [0], [0,1], [0,1,2,3]):
for skey2 in ([], [0], [0, 1], [0, 1, 2, 3]):
with pytest.raises(ValueError):
pkm = self.ssh1.PublicKeyMessage(cookie, skey2, hkey, pflags, cmask, amask)
for hkey2 in ([], [0], [0,1], [0,1,2,3]):
for hkey2 in ([], [0], [0, 1], [0, 1, 2, 3]):
with pytest.raises(ValueError):
print(hkey2)
pkm = self.ssh1.PublicKeyMessage(cookie, skey, hkey2, pflags, cmask, amask)

View File

@ -1,6 +1,7 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import struct, os
import os
import struct
import pytest

View File

@ -105,15 +105,15 @@ class TestVersionCompare(object):
versions.append('2015.{0}'.format(i))
for i in range(72, 75):
versions.append('2016.{0}'.format(i))
l = len(versions)
for i in range(l):
length = len(versions)
for i in range(length):
v = versions[i]
s = self.get_dropbear_software(v)
assert s.compare_version(v) == 0
if i - 1 >= 0:
vbefore = versions[i - 1]
assert s.compare_version(vbefore) > 0
if i + 1 < l:
if i + 1 < length:
vnext = versions[i + 1]
assert s.compare_version(vnext) < 0
@ -164,15 +164,15 @@ class TestVersionCompare(object):
versions.append('6.{0}'.format(i))
for i in range(0, 4):
versions.append('7.{0}'.format(i))
l = len(versions)
for i in range(l):
length = len(versions)
for i in range(length):
v = versions[i]
s = self.get_openssh_software(v)
assert s.compare_version(v) == 0
if i - 1 >= 0:
vbefore = versions[i - 1]
assert s.compare_version(vbefore) > 0
if i + 1 < l:
if i + 1 < length:
vnext = versions[i + 1]
assert s.compare_version(vnext) < 0
@ -202,14 +202,14 @@ class TestVersionCompare(object):
versions.append('0.6.{0}'.format(i))
for i in range(0, 5):
versions.append('0.7.{0}'.format(i))
l = len(versions)
for i in range(l):
length = len(versions)
for i in range(length):
v = versions[i]
s = self.get_libssh_software(v)
assert s.compare_version(v) == 0
if i - 1 >= 0:
vbefore = versions[i - 1]
assert s.compare_version(vbefore) > 0
if i + 1 < l:
if i + 1 < length:
vnext = versions[i + 1]
assert s.compare_version(vnext) < 0

51
tox.ini
View File

@ -80,7 +80,7 @@ commands =
deps =
flake8
commands =
flake8 {posargs:{env:SSHAUDIT}}
flake8 {posargs:{env:SSHAUDIT} {toxinidir}/packages/setup.py {toxinidir}/test} --statistics
[testenv:vulture]
deps =
@ -137,42 +137,13 @@ max-module-lines = 2500
[flake8]
ignore =
# indentation contains tabs
W191,
# blank line contains whitespace
W293,
# indentation contains mixed spaces and tabs
E101,
# multiple spaces before operator
E221,
# multiple spaces after operator
E241,
# multiple imports on one line
E401,
# line too long
E501,
# module imported but unused
F401,
# undefined name
F821,
# these exceptions should be handled one by one
E117, # over-indented
E126, # continuation line over-indented for hanging indent
E128, # continuation line under-indented for visual indent
E226, # missing whitespace around arithmetic operator
E231, # missing whitespace after ','
E251, # unexpected spaces around keyword / parameter equals
E261, # at least two spaces before inline comment
E265, # block comment should start with '# '
E301, # expected 1 blank line, found 0
E302, # expected 2 blank lines, found 1
E303, # too many blank lines (2)
E305, # expected 2 blank lines after class or function definition, found 1
E711, # comparison to None should be 'if cond is not None:'
E712, # comparison to False should be 'if cond is False:' or 'if not cond:'
E722, # do not use bare 'except'
E741, # ambiguous variable name 'l'
F601, # dictionary key 'ecdsa-sha2-1.3.132.0.10' repeated with different values
F841, # local variable 'e' is assigned to but never used
W504, # line break after binary operator
W605, # invalid escape sequence '\s'
W191, # indentation contains tabs
E101, # indentation contains mixed spaces and tabs
E241, # multiple spaces after operator; should be kept for tabular data
E501, # line too long
E117, # over-indented
E126, # continuation line over-indented for hanging indent
E128, # continuation line under-indented for visual indent
E722, # do not use bare 'except'
F601, # dictionary key 'ecdsa-sha2-1.3.132.0.10' repeated with different values
W504, # line break after binary operator; this (or W503) has to stay