Merged all_my_patches branch to master, since a new project maintainer is needed.

This commit is contained in:
Joe Testa 2019-08-16 08:30:45 -04:00
commit b35ca6c6f3
27 changed files with 2951 additions and 756 deletions

37
.appveyor.yml Normal file
View File

@ -0,0 +1,37 @@
version: '1.7.1.dev.{build}'
build: off
branches:
only:
- master
- develop
environment:
matrix:
- PYTHON: "C:\\Python26"
- PYTHON: "C:\\Python26-x64"
- PYTHON: "C:\\Python27"
- PYTHON: "C:\\Python27-x64"
- PYTHON: "C:\\Python33"
- PYTHON: "C:\\Python33-x64"
- PYTHON: "C:\\Python34"
- PYTHON: "C:\\Python34-x64"
- PYTHON: "C:\\Python35"
- PYTHON: "C:\\Python35-x64"
- PYTHON: "C:\\Python36"
- PYTHON: "C:\\Python36-x64"
matrix:
fast_finish: true
cache:
- '%LOCALAPPDATA%\pip\Cache'
- .downloads -> .appveyor.yml
install:
- "cmd /c .\\test\\tools\\ci-win.cmd install"
test_script:
- "cmd /c .\\test\\tools\\ci-win.cmd test"
on_failure:
- ps: get-content .tox\*\log\*

7
.gitignore vendored
View File

@ -1,5 +1,8 @@
*~
*.pyc
html/
venv/
venv*/
.cache/
.tox
.coverage*
reports/
.scannerwork/

View File

@ -1,18 +1,80 @@
language: python
python:
- 2.6
- 2.7
- 3.3
- 3.4
- 3.5
- pypy
- pypy3
install:
- pip install --upgrade pytest
- pip install --upgrade pytest-cov
- pip install --upgrade coveralls
script:
- py.test --cov-report= --cov=ssh-audit -v test
after_success:
- coveralls
sudo: false
matrix:
include:
# (default)
- os: linux
python: 2.6
- os: linux
python: 2.7
env: SQ=1
- os: linux
python: 3.3
- os: linux
python: 3.4
- os: linux
python: 3.5
- os: linux
python: 3.6
- os: linux
python: pypy
- os: linux
python: pypy3
- os: linux
python: 3.7-dev
# Ubuntu 12.04
- os: linux
dist: precise
language: generic
env: PY_VER=py26,py27,py33,py34,py35,py36,pypy,pypy3 PY_ORIGIN=pyenv
# Ubuntu 14.04
- os: linux
dist: trusty
language: generic
env: PY_VER=py26,py27,py33,py34,py35,py36,pypy,pypy3 PY_ORIGIN=pyenv
# macOS 10.12 Sierra
- os: osx
osx_image: xcode8.3
language: generic
env: PY_VER=py26,py27,py33,py34,py35,py36,pypy,pypy3
# Mac OS X 10.11 El Capitan
- os: osx
osx_image: xcode7.3
language: generic
env: PY_VER=py26,py27,py33,py34,py35,py36,pypy,pypy3
# Mac OS X 10.10 Yosemite
- os: osx
osx_image: xcode6.4
language: generic
env: PY_VER=py26,py27,py33,py34,py35,py36,pypy,pypy3
allow_failures:
# PyPy3 on Travis CI is out of date
- python: pypy3
# Python nightly could fail
- python: 3.7-dev
- env: PY_VER=py37
- env: PY_VER=py37/pyenv
- env: PY_VER=py37 PY_ORIGIN=pyenv
fast_finish: true
cache:
- pip
- directories:
- $HOME/.pyenv.cache
- $HOME/.bin
before_install:
- source test/tools/ci-linux.sh
- ci_step_before_install
install:
- ci_step_install
script:
- ci_step_script
after_success:
- ci_step_success
after_failure:
- ci_step_failure

View File

@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (C) 2016 Andris Raugulis (moo@arthepsy.eu)
Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@ -1,6 +1,8 @@
# ssh-audit
[![build status](https://api.travis-ci.org/arthepsy/ssh-audit.svg)](https://travis-ci.org/arthepsy/ssh-audit)
[![coverage status](https://coveralls.io/repos/github/arthepsy/ssh-audit/badge.svg)](https://coveralls.io/github/arthepsy/ssh-audit)
[![travis build status](https://api.travis-ci.org/arthepsy/ssh-audit.svg?branch=develop)](https://travis-ci.org/arthepsy/ssh-audit)
[![appveyor build status](https://ci.appveyor.com/api/projects/status/4m5r73m0r023edil/branch/develop?svg=true)](https://ci.appveyor.com/project/arthepsy/ssh-audit)
[![codecov](https://codecov.io/gh/arthepsy/ssh-audit/branch/develop/graph/badge.svg)](https://codecov.io/gh/arthepsy/ssh-audit)
[![Quality Gate](https://sonarqube.com/api/badges/gate?key=arthepsy-github%3Assh-audit%3Adevelop&template=ROUNDED)](https://sq.evolutiongaming.com/dashboard?id=arthepsy-github%3Assh-audit%3Adevelop)
**ssh-audit** is a tool for ssh server auditing.
## Features

File diff suppressed because it is too large Load Diff

View File

@ -40,6 +40,41 @@ def output_spy():
return _OutputSpy()
class _VirtualGlobalSocket(object):
def __init__(self, vsocket):
self.vsocket = vsocket
self.addrinfodata = {}
# pylint: disable=unused-argument
def create_connection(self, address, timeout=0, source_address=None):
# pylint: disable=protected-access
return self.vsocket._connect(address, True)
# pylint: disable=unused-argument
def socket(self,
family=socket.AF_INET,
socktype=socket.SOCK_STREAM,
proto=0,
fileno=None):
return self.vsocket
def getaddrinfo(self, host, port, family=0, socktype=0, proto=0, flags=0):
key = '{0}#{1}'.format(host, port)
if key in self.addrinfodata:
data = self.addrinfodata[key]
if isinstance(data, Exception):
raise data
return data
if host == 'localhost':
r = []
if family in (0, socket.AF_INET):
r.append((socket.AF_INET, 1, 6, '', ('127.0.0.1', port)))
if family in (0, socket.AF_INET6):
r.append((socket.AF_INET6, 1, 6, '', ('::1', port)))
return r
return []
class _VirtualSocket(object):
def __init__(self):
self.sock_address = ('127.0.0.1', 0)
@ -49,6 +84,7 @@ class _VirtualSocket(object):
self.rdata = []
self.sdata = []
self.errors = {}
self.gsock = _VirtualGlobalSocket(self)
def _check_err(self, method):
method_error = self.errors.get(method)
@ -113,18 +149,8 @@ class _VirtualSocket(object):
@pytest.fixture()
def virtual_socket(monkeypatch):
vsocket = _VirtualSocket()
# pylint: disable=unused-argument
def _socket(family=socket.AF_INET,
socktype=socket.SOCK_STREAM,
proto=0,
fileno=None):
return vsocket
def _cc(address, timeout=0, source_address=None):
# pylint: disable=protected-access
return vsocket._connect(address, True)
monkeypatch.setattr(socket, 'create_connection', _cc)
monkeypatch.setattr(socket, 'socket', _socket)
gsock = vsocket.gsock
monkeypatch.setattr(socket, 'create_connection', gsock.create_connection)
monkeypatch.setattr(socket, 'socket', gsock.socket)
monkeypatch.setattr(socket, 'getaddrinfo', gsock.getaddrinfo)
return vsocket

View File

@ -1,10 +0,0 @@
#!/bin/sh
_cdir=$(cd -- "$(dirname "$0")" && pwd)
type py.test > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo "err: py.test (Python testing framework) not found."
exit 1
fi
cd -- "${_cdir}/.."
mkdir -p html
py.test -v --cov-report=html:html/coverage --cov=ssh-audit test

View File

@ -1,10 +0,0 @@
#!/bin/sh
_cdir=$(cd -- "$(dirname "$0")" && pwd)
type mypy > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo "err: mypy (Optional Static Typing for Python) not found."
exit 1
fi
_htmldir="${_cdir}/../html/mypy-py2"
mkdir -p "${_htmldir}"
mypy --python-version 2.7 --config-file "${_cdir}/mypy.ini" --html-report "${_htmldir}" "${_cdir}/../ssh-audit.py"

View File

@ -1,10 +0,0 @@
#!/bin/sh
_cdir=$(cd -- "$(dirname "$0")" && pwd)
type mypy > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo "err: mypy (Optional Static Typing for Python) not found."
exit 1
fi
_htmldir="${_cdir}/../html/mypy-py3"
mkdir -p "${_htmldir}"
mypy --python-version 3.5 --config-file "${_cdir}/mypy.ini" --html-report "${_htmldir}" "${_cdir}/../ssh-audit.py"

View File

@ -1,9 +0,0 @@
[mypy]
silent_imports = True
disallow_untyped_calls = True
disallow_untyped_defs = True
check_untyped_defs = True
disallow-subclassing-any = True
warn-incomplete-stub = True
warn-redundant-casts = True

View File

@ -1,13 +0,0 @@
#!/bin/sh
_cdir=$(cd -- "$(dirname "$0")" && pwd)
type prospector > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo "err: prospector (Python Static Analysis) not found."
exit 1
fi
if [ X"$1" == X"" ]; then
_file="${_cdir}/../ssh-audit.py"
else
_file="$1"
fi
prospector -E --profile-path "${_cdir}" -P prospector "${_file}"

View File

@ -1,42 +0,0 @@
strictness: veryhigh
doc-warnings: false
pylint:
disable:
- multiple-imports
- invalid-name
- trailing-whitespace
options:
max-args: 8 # default: 5
max-locals: 20 # default: 15
max-returns: 6
max-branches: 15 # default: 12
max-statements: 60 # default: 50
max-parents: 7
max-attributes: 8 # default: 7
min-public-methods: 1 # default: 2
max-public-methods: 20
max-bool-expr: 5
max-nested-blocks: 6 # default: 5
max-line-length: 80 # default: 100
ignore-long-lines: ^\s*(#\s+type:\s+.*|[A-Z0-9_]+\s+=\s+.*|('.*':\s+)?\[.*\],?)$
max-module-lines: 2500 # default: 10000
pep8:
disable:
- W191 # indentation contains tabs
- W293 # blank line contains whitespace
- E101 # indentation contains mixed spaces and tabs
- E401 # multiple imports on one line
- E501 # line too long
- E221 # multiple spaces before operator
pyflakes:
disable:
- F401 # module imported but unused
- F821 # undefined name
mccabe:
options:
max-complexity: 15

6
test/stubs/colorama.pyi Normal file
View File

@ -0,0 +1,6 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from typing import Optional
def init(autoreset: bool = False, convert: Optional[bool] = None, strip: Optional[bool] = None, wrap: bool = True) -> None: ...

View File

@ -10,8 +10,8 @@ class TestAuditConf(object):
self.AuditConf = ssh_audit.AuditConf
self.usage = ssh_audit.usage
@classmethod
def _test_conf(cls, conf, **kwargs):
@staticmethod
def _test_conf(conf, **kwargs):
options = {
'host': None,
'port': 22,
@ -20,7 +20,7 @@ class TestAuditConf(object):
'batch': False,
'colors': True,
'verbose': False,
'minlevel': 'info',
'level': 'info',
'ipv4': True,
'ipv6': True,
'ipvo': ()
@ -34,7 +34,7 @@ class TestAuditConf(object):
assert conf.batch is options['batch']
assert conf.colors is options['colors']
assert conf.verbose is options['verbose']
assert conf.minlevel == options['minlevel']
assert conf.level == options['level']
assert conf.ipv4 == options['ipv4']
assert conf.ipv6 == options['ipv6']
assert conf.ipvo == options['ipvo']
@ -115,14 +115,14 @@ class TestAuditConf(object):
conf.ipvo = (4, 4, 4, 6, 6)
assert conf.ipvo == (4, 6)
def test_audit_conf_minlevel(self):
def test_audit_conf_level(self):
conf = self.AuditConf()
for level in ['info', 'warn', 'fail']:
conf.minlevel = level
assert conf.minlevel == level
conf.level = level
assert conf.level == level
for level in ['head', 'good', 'unknown', None]:
with pytest.raises(ValueError) as excinfo:
conf.minlevel = level
conf.level = level
excinfo.match(r'.*invalid level.*')
def test_audit_conf_cmdline(self):
@ -148,6 +148,14 @@ class TestAuditConf(object):
self._test_conf(conf, host='localhost', port=2222)
conf = c('-p 2222 localhost')
self._test_conf(conf, host='localhost', port=2222)
conf = c('2001:4860:4860::8888')
self._test_conf(conf, host='2001:4860:4860::8888')
conf = c('[2001:4860:4860::8888]:22')
self._test_conf(conf, host='2001:4860:4860::8888')
conf = c('[2001:4860:4860::8888]:2222')
self._test_conf(conf, host='2001:4860:4860::8888', port=2222)
conf = c('-p 2222 2001:4860:4860::8888')
self._test_conf(conf, host='2001:4860:4860::8888', port=2222)
with pytest.raises(SystemExit):
conf = c('localhost:')
with pytest.raises(SystemExit):
@ -183,10 +191,10 @@ class TestAuditConf(object):
conf = c('-v localhost')
self._test_conf(conf, host='localhost', verbose=True)
conf = c('-l info localhost')
self._test_conf(conf, host='localhost', minlevel='info')
self._test_conf(conf, host='localhost', level='info')
conf = c('-l warn localhost')
self._test_conf(conf, host='localhost', minlevel='warn')
self._test_conf(conf, host='localhost', level='warn')
conf = c('-l fail localhost')
self._test_conf(conf, host='localhost', minlevel='fail')
self._test_conf(conf, host='localhost', level='fail')
with pytest.raises(SystemExit):
conf = c('-l something localhost')

View File

@ -1,6 +1,7 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import socket
import errno
import pytest
@ -17,46 +18,99 @@ class TestErrors(object):
conf.batch = True
return conf
def _audit(self, spy, conf=None, sysexit=True):
if conf is None:
conf = self._conf()
spy.begin()
if sysexit:
with pytest.raises(SystemExit):
self.audit(conf)
else:
self.audit(conf)
lines = spy.flush()
return lines
def test_connection_unresolved(self, output_spy, virtual_socket):
vsocket = virtual_socket
vsocket.gsock.addrinfodata['localhost#22'] = []
lines = self._audit(output_spy)
assert len(lines) == 1
assert 'has no DNS records' in lines[-1]
def test_connection_refused(self, output_spy, virtual_socket):
vsocket = virtual_socket
vsocket.errors['connect'] = socket.error(61, 'Connection refused')
output_spy.begin()
with pytest.raises(SystemExit):
self.audit(self._conf())
lines = output_spy.flush()
vsocket.errors['connect'] = socket.error(errno.ECONNREFUSED, 'Connection refused')
lines = self._audit(output_spy)
assert len(lines) == 1
assert 'Connection refused' in lines[-1]
def test_connection_closed_before_banner(self, output_spy, virtual_socket):
def test_connection_timeout(self, output_spy, virtual_socket):
vsocket = virtual_socket
vsocket.rdata.append(socket.error(54, 'Connection reset by peer'))
output_spy.begin()
with pytest.raises(SystemExit):
self.audit(self._conf())
lines = output_spy.flush()
vsocket.errors['connect'] = socket.timeout('timed out')
lines = self._audit(output_spy)
assert len(lines) == 1
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]
def test_recv_timeout(self, output_spy, virtual_socket):
vsocket = virtual_socket
vsocket.rdata.append(socket.timeout('timed out'))
lines = self._audit(output_spy)
assert len(lines) == 1
assert 'did not receive banner' in lines[-1]
assert 'timed out' in lines[-1]
def test_recv_retry_till_timeout(self, output_spy, virtual_socket):
vsocket = virtual_socket
vsocket.rdata.append(socket.error(errno.EAGAIN, 'Resource temporarily unavailable'))
vsocket.rdata.append(socket.error(errno.EWOULDBLOCK, 'Resource temporarily unavailable'))
vsocket.rdata.append(socket.error(errno.EAGAIN, 'Resource temporarily unavailable'))
vsocket.rdata.append(socket.timeout('timed out'))
lines = self._audit(output_spy)
assert len(lines) == 1
assert 'did not receive banner' in lines[-1]
assert 'timed out' in lines[-1]
def test_recv_retry_till_reset(self, output_spy, virtual_socket):
vsocket = virtual_socket
vsocket.rdata.append(socket.error(errno.EAGAIN, 'Resource temporarily unavailable'))
vsocket.rdata.append(socket.error(errno.EWOULDBLOCK, 'Resource temporarily unavailable'))
vsocket.rdata.append(socket.error(errno.EAGAIN, 'Resource temporarily unavailable'))
vsocket.rdata.append(socket.error(errno.ECONNRESET, 'Connection reset by peer'))
lines = self._audit(output_spy)
assert len(lines) == 1
assert 'did not receive banner' in lines[-1]
assert 'reset by peer' in lines[-1]
def test_connection_closed_before_banner(self, output_spy, virtual_socket):
vsocket = virtual_socket
vsocket.rdata.append(socket.error(errno.ECONNRESET, 'Connection reset by peer'))
lines = self._audit(output_spy)
assert len(lines) == 1
assert 'did not receive banner' in lines[-1]
assert 'reset by peer' in lines[-1]
def test_connection_closed_after_header(self, output_spy, virtual_socket):
vsocket = virtual_socket
vsocket.rdata.append(b'header line 1\n')
vsocket.rdata.append(b'\n')
vsocket.rdata.append(b'header line 2\n')
vsocket.rdata.append(socket.error(54, 'Connection reset by peer'))
output_spy.begin()
with pytest.raises(SystemExit):
self.audit(self._conf())
lines = output_spy.flush()
vsocket.rdata.append(socket.error(errno.ECONNRESET, 'Connection reset by peer'))
lines = self._audit(output_spy)
assert len(lines) == 3
assert 'did not receive banner' in lines[-1]
assert 'reset by peer' in lines[-1]
def test_connection_closed_after_banner(self, output_spy, virtual_socket):
vsocket = virtual_socket
vsocket.rdata.append(b'SSH-2.0-ssh-audit-test\r\n')
vsocket.rdata.append(socket.error(54, 'Connection reset by peer'))
output_spy.begin()
with pytest.raises(SystemExit):
self.audit(self._conf())
lines = output_spy.flush()
lines = self._audit(output_spy)
assert len(lines) == 2
assert 'error reading packet' in lines[-1]
assert 'reset by peer' in lines[-1]
@ -64,10 +118,7 @@ class TestErrors(object):
def test_empty_data_after_banner(self, output_spy, virtual_socket):
vsocket = virtual_socket
vsocket.rdata.append(b'SSH-2.0-ssh-audit-test\r\n')
output_spy.begin()
with pytest.raises(SystemExit):
self.audit(self._conf())
lines = output_spy.flush()
lines = self._audit(output_spy)
assert len(lines) == 2
assert 'error reading packet' in lines[-1]
assert 'empty' in lines[-1]
@ -76,10 +127,7 @@ class TestErrors(object):
vsocket = virtual_socket
vsocket.rdata.append(b'SSH-2.0-ssh-audit-test\r\n')
vsocket.rdata.append(b'xxx\n')
output_spy.begin()
with pytest.raises(SystemExit):
self.audit(self._conf())
lines = output_spy.flush()
lines = self._audit(output_spy)
assert len(lines) == 2
assert 'error reading packet' in lines[-1]
assert 'xxx' in lines[-1]
@ -87,10 +135,7 @@ class TestErrors(object):
def test_non_ascii_banner(self, output_spy, virtual_socket):
vsocket = virtual_socket
vsocket.rdata.append(b'SSH-2.0-ssh-audit-test\xc3\xbc\r\n')
output_spy.begin()
with pytest.raises(SystemExit):
self.audit(self._conf())
lines = output_spy.flush()
lines = self._audit(output_spy)
assert len(lines) == 3
assert 'error reading packet' in lines[-1]
assert 'ASCII' in lines[-2]
@ -100,10 +145,7 @@ class TestErrors(object):
vsocket = virtual_socket
vsocket.rdata.append(b'SSH-2.0-ssh-audit-test\r\n')
vsocket.rdata.append(b'\x81\xff\n')
output_spy.begin()
with pytest.raises(SystemExit):
self.audit(self._conf())
lines = output_spy.flush()
lines = self._audit(output_spy)
assert len(lines) == 2
assert 'error reading packet' in lines[-1]
assert '\\x81\\xff' in lines[-1]
@ -112,12 +154,9 @@ class TestErrors(object):
vsocket = virtual_socket
vsocket.rdata.append(b'SSH-1.3-ssh-audit-test\r\n')
vsocket.rdata.append(b'Protocol major versions differ.\n')
output_spy.begin()
with pytest.raises(SystemExit):
conf = self._conf()
conf.ssh1, conf.ssh2 = True, False
self.audit(conf)
lines = output_spy.flush()
lines = self._audit(output_spy, conf)
assert len(lines) == 3
assert 'error reading packet' in lines[-1]
assert 'major versions differ' in lines[-1]

View File

@ -41,13 +41,13 @@ class TestOutput(object):
out = self.Output()
# default: on
assert out.batch is False
assert out.colors is True
assert out.minlevel == 'info'
assert out.use_colors is True
assert out.level == 'info'
def test_output_colors(self, output_spy):
out = self.Output()
# test without colors
out.colors = False
out.use_colors = False
output_spy.begin()
out.info('info color')
assert output_spy.flush() == [u'info color']
@ -66,7 +66,7 @@ class TestOutput(object):
if not out.colors_supported:
return
# test with colors
out.colors = True
out.use_colors = True
output_spy.begin()
out.info('info color')
assert output_spy.flush() == [u'info color']
@ -93,29 +93,29 @@ class TestOutput(object):
def test_output_levels(self):
out = self.Output()
assert out.getlevel('info') == 0
assert out.getlevel('good') == 0
assert out.getlevel('warn') == 1
assert out.getlevel('fail') == 2
assert out.getlevel('unknown') > 2
assert out.get_level('info') == 0
assert out.get_level('good') == 0
assert out.get_level('warn') == 1
assert out.get_level('fail') == 2
assert out.get_level('unknown') > 2
def test_output_minlevel_property(self):
def test_output_level_property(self):
out = self.Output()
out.minlevel = 'info'
assert out.minlevel == 'info'
out.minlevel = 'good'
assert out.minlevel == 'info'
out.minlevel = 'warn'
assert out.minlevel == 'warn'
out.minlevel = 'fail'
assert out.minlevel == 'fail'
out.minlevel = 'invalid level'
assert out.minlevel == 'unknown'
out.level = 'info'
assert out.level == 'info'
out.level = 'good'
assert out.level == 'info'
out.level = 'warn'
assert out.level == 'warn'
out.level = 'fail'
assert out.level == 'fail'
out.level = 'invalid level'
assert out.level == 'unknown'
def test_output_minlevel(self, output_spy):
def test_output_level(self, output_spy):
out = self.Output()
# visible: all
out.minlevel = 'info'
out.level = 'info'
output_spy.begin()
out.info('info color')
out.head('head color')
@ -124,7 +124,7 @@ class TestOutput(object):
out.fail('fail color')
assert len(output_spy.flush()) == 5
# visible: head, warn, fail
out.minlevel = 'warn'
out.level = 'warn'
output_spy.begin()
out.info('info color')
out.head('head color')
@ -133,7 +133,7 @@ class TestOutput(object):
out.fail('fail color')
assert len(output_spy.flush()) == 3
# visible: head, fail
out.minlevel = 'fail'
out.level = 'fail'
output_spy.begin()
out.info('info color')
out.head('head color')
@ -142,7 +142,7 @@ class TestOutput(object):
out.fail('fail color')
assert len(output_spy.flush()) == 2
# visible: head
out.minlevel = 'invalid level'
out.level = 'invalid level'
output_spy.begin()
out.info('info color')
out.head('head color')
@ -155,7 +155,7 @@ class TestOutput(object):
out = self.Output()
# visible: all
output_spy.begin()
out.minlevel = 'info'
out.level = 'info'
out.batch = False
out.info('info color')
out.head('head color')
@ -165,7 +165,7 @@ class TestOutput(object):
assert len(output_spy.flush()) == 5
# visible: all except head
output_spy.begin()
out.minlevel = 'info'
out.level = 'info'
out.batch = True
out.info('info color')
out.head('head color')

85
test/test_resolve.py Normal file
View File

@ -0,0 +1,85 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import socket
import pytest
# pylint: disable=attribute-defined-outside-init,protected-access
class TestResolve(object):
@pytest.fixture(autouse=True)
def init(self, ssh_audit):
self.AuditConf = ssh_audit.AuditConf
self.audit = ssh_audit.audit
self.ssh = ssh_audit.SSH
def _conf(self):
conf = self.AuditConf('localhost', 22)
conf.colors = False
conf.batch = True
return conf
def test_resolve_error(self, output_spy, virtual_socket):
vsocket = virtual_socket
vsocket.gsock.addrinfodata['localhost#22'] = socket.gaierror(8, 'hostname nor servname provided, or not known')
s = self.ssh.Socket('localhost', 22)
conf = self._conf()
output_spy.begin()
with pytest.raises(SystemExit):
r = list(s._resolve(conf.ipvo))
lines = output_spy.flush()
assert len(lines) == 1
assert 'hostname nor servname provided' in lines[-1]
def test_resolve_hostname_without_records(self, output_spy, virtual_socket):
vsocket = virtual_socket
vsocket.gsock.addrinfodata['localhost#22'] = []
s = self.ssh.Socket('localhost', 22)
conf = self._conf()
output_spy.begin()
r = list(s._resolve(conf.ipvo))
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)
r = list(s._resolve(conf.ipvo))
assert len(r) == 1
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
r = list(s._resolve(conf.ipvo))
assert len(r) == 1
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))
assert len(r) == 2
assert r[0] == (socket.AF_INET, ('127.0.0.1', 22))
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
conf.ipv6 = True
r = list(s._resolve(conf.ipvo))
assert len(r) == 2
assert r[0] == (socket.AF_INET, ('127.0.0.1', 22))
assert r[1] == (socket.AF_INET6, ('::1', 22))
conf = self._conf()
conf.ipv6 = True
conf.ipv4 = True
r = list(s._resolve(conf.ipvo))
assert len(r) == 2
assert r[0] == (socket.AF_INET6, ('::1', 22))
assert r[1] == (socket.AF_INET, ('127.0.0.1', 22))

41
test/test_socket.py Normal file
View File

@ -0,0 +1,41 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import socket
import pytest
# pylint: disable=attribute-defined-outside-init
class TestSocket(object):
@pytest.fixture(autouse=True)
def init(self, ssh_audit):
self.ssh = ssh_audit.SSH
def test_invalid_host(self, virtual_socket):
with pytest.raises(ValueError):
s = self.ssh.Socket(None, 22)
def test_invalid_port(self, virtual_socket):
with pytest.raises(ValueError):
s = self.ssh.Socket('localhost', 'abc')
with pytest.raises(ValueError):
s = self.ssh.Socket('localhost', -1)
with pytest.raises(ValueError):
s = self.ssh.Socket('localhost', 0)
with pytest.raises(ValueError):
s = self.ssh.Socket('localhost', 65536)
def test_not_connected_socket(self, virtual_socket):
sock = self.ssh.Socket('localhost', 22)
banner, header, err = sock.get_banner()
assert banner is None
assert len(header) == 0
assert err == 'not connected'
s, e = sock.recv()
assert s == -1
assert e == 'not connected'
s, e = sock.send('nothing')
assert s == -1
assert e == 'not connected'
s, e = sock.send_packet()
assert s == -1
assert e == 'not connected'

View File

@ -168,17 +168,17 @@ class TestSoftware(object):
assert s.display(True) == str(s)
assert s.display(False) == str(s)
assert repr(s) == '<Software(product=libssh, version=0.2)>'
s = ps('SSH-2.0-libssh-0.7.3')
s = ps('SSH-2.0-libssh-0.7.4')
assert s.vendor is None
assert s.product == 'libssh'
assert s.version == '0.7.3'
assert s.version == '0.7.4'
assert s.patch is None
assert s.os is None
assert str(s) == 'libssh 0.7.3'
assert str(s) == 'libssh 0.7.4'
assert str(s) == s.display()
assert s.display(True) == str(s)
assert s.display(False) == str(s)
assert repr(s) == '<Software(product=libssh, version=0.7.3)>'
assert repr(s) == '<Software(product=libssh, version=0.7.4)>'
def test_romsshell_software(self):
ps = lambda x: self.ssh.Software.parse(self.ssh.Banner.parse(x)) # noqa

View File

@ -66,34 +66,51 @@ class TestSSH1(object):
assert fp.md5 == 'MD5:9d:26:f8:39:fc:20:9d:9b:ca:cc:4a:0f:e1:93:f5:96'
assert fp.sha256 == 'SHA256:vZdx3mhzbvVJmn08t/ruv8WDhJ9jfKYsCTuSzot+QIs'
def test_pkm_read(self):
pkm = self.ssh1.PublicKeyMessage.parse(self._pkm_payload())
assert pkm is not None
assert pkm.cookie == b'\x88\x99\xaa\xbb\xcc\xdd\xee\xff'
b, e, m = self._server_key()
def _assert_pkm_keys(self, pkm, skey, hkey):
b, e, m = skey
assert pkm.server_key_bits == b
assert pkm.server_key_public_exponent == e
assert pkm.server_key_public_modulus == m
b, e, m = self._host_key()
b, e, m = hkey
assert pkm.host_key_bits == b
assert pkm.host_key_public_exponent == e
assert pkm.host_key_public_modulus == m
fp = self.ssh.Fingerprint(pkm.host_key_fingerprint_data)
def _assert_pkm_fields(self, pkm, skey, hkey):
assert pkm is not None
assert pkm.cookie == b'\x88\x99\xaa\xbb\xcc\xdd\xee\xff'
self._assert_pkm_keys(pkm, skey, hkey)
assert pkm.protocol_flags == 2
assert pkm.supported_ciphers_mask == 72
assert pkm.supported_ciphers == ['3des', 'blowfish']
assert pkm.supported_authentications_mask == 36
assert pkm.supported_authentications == ['rsa', 'tis']
fp = self.ssh.Fingerprint(pkm.host_key_fingerprint_data)
assert fp.md5 == 'MD5:9d:26:f8:39:fc:20:9d:9b:ca:cc:4a:0f:e1:93:f5:96'
assert fp.sha256 == 'SHA256:vZdx3mhzbvVJmn08t/ruv8WDhJ9jfKYsCTuSzot+QIs'
def test_pkm_init(self):
cookie = b'\x88\x99\xaa\xbb\xcc\xdd\xee\xff'
pflags, cmask, amask = 2, 72, 36
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]):
with pytest.raises(ValueError):
pkm = self.ssh1.PublicKeyMessage(cookie, skey2, hkey, pflags, cmask, amask)
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)
def test_pkm_read(self):
pkm = self.ssh1.PublicKeyMessage.parse(self._pkm_payload())
self._assert_pkm_fields(pkm, self._server_key(), self._host_key())
def test_pkm_payload(self):
cookie = b'\x88\x99\xaa\xbb\xcc\xdd\xee\xff'
skey = self._server_key()
hkey = self._host_key()
pflags = 2
cmask = 72
amask = 36
skey, hkey = self._server_key(), self._host_key()
pflags, cmask, amask = 2, 72, 36
pkm1 = self.ssh1.PublicKeyMessage(cookie, skey, hkey, pflags, cmask, amask)
pkm2 = self.ssh1.PublicKeyMessage.parse(self._pkm_payload())
assert pkm1.payload == pkm2.payload
@ -108,7 +125,7 @@ class TestSSH1(object):
output_spy.begin()
self.audit(self._conf())
lines = output_spy.flush()
assert len(lines) == 10
assert len(lines) == 13
def test_ssh1_server_invalid_first_packet(self, output_spy, virtual_socket):
vsocket = virtual_socket
@ -121,7 +138,7 @@ class TestSSH1(object):
with pytest.raises(SystemExit):
self.audit(self._conf())
lines = output_spy.flush()
assert len(lines) == 4
assert len(lines) == 7
assert 'unknown message' in lines[-1]
def test_ssh1_server_invalid_checksum(self, output_spy, virtual_socket):

164
test/test_ssh_algorithm.py Normal file
View File

@ -0,0 +1,164 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import pytest
# pylint: disable=attribute-defined-outside-init
class TestSSHAlgorithm(object):
@pytest.fixture(autouse=True)
def init(self, ssh_audit):
self.ssh = ssh_audit.SSH
def _tf(self, v, s=None):
return self.ssh.Algorithm.Timeframe().update(v, s)
def test_get_ssh_version(self):
def ver(v):
return self.ssh.Algorithm.get_ssh_version(v)
assert ver('7.5') == ('OpenSSH', '7.5', False)
assert ver('7.5C') == ('OpenSSH', '7.5', True)
assert ver('d2016.74') == ('Dropbear SSH', '2016.74', False)
assert ver('l10.7.4') == ('libssh', '0.7.4', False)
assert ver('')[1] == ''
def test_get_since_text(self):
def gst(v):
return self.ssh.Algorithm.get_since_text(v)
assert gst(['7.5']) == 'available since OpenSSH 7.5'
assert gst(['7.5C']) == 'available since OpenSSH 7.5 (client only)'
assert gst(['7.5,']) == 'available since OpenSSH 7.5'
assert gst(['d2016.73']) == 'available since Dropbear SSH 2016.73'
assert gst(['7.5,d2016.73']) == 'available since OpenSSH 7.5, Dropbear SSH 2016.73'
assert gst(['l10.7.4']) is None
assert gst([]) is None
def test_timeframe_creation(self):
# pylint: disable=line-too-long,too-many-statements
def cmp_tf(v, s, r):
assert str(self._tf(v, s)) == str(r)
cmp_tf(['6.2'], None, {'OpenSSH': ['6.2', None, '6.2', None]})
cmp_tf(['6.2'], True, {'OpenSSH': ['6.2', None, None, None]})
cmp_tf(['6.2'], False, {'OpenSSH': [None, None, '6.2', None]})
cmp_tf(['6.2C'], None, {'OpenSSH': [None, None, '6.2', None]})
cmp_tf(['6.2C'], True, {})
cmp_tf(['6.2C'], False, {'OpenSSH': [None, None, '6.2', None]})
cmp_tf(['6.1,6.2C'], None, {'OpenSSH': ['6.1', None, '6.2', None]})
cmp_tf(['6.1,6.2C'], True, {'OpenSSH': ['6.1', None, None, None]})
cmp_tf(['6.1,6.2C'], False, {'OpenSSH': [None, None, '6.2', None]})
cmp_tf(['6.2C,6.1'], None, {'OpenSSH': ['6.1', None, '6.2', None]})
cmp_tf(['6.2C,6.1'], True, {'OpenSSH': ['6.1', None, None, None]})
cmp_tf(['6.2C,6.1'], False, {'OpenSSH': [None, None, '6.2', None]})
cmp_tf(['6.3,6.2C'], None, {'OpenSSH': ['6.3', None, '6.2', None]})
cmp_tf(['6.3,6.2C'], True, {'OpenSSH': ['6.3', None, None, None]})
cmp_tf(['6.3,6.2C'], False, {'OpenSSH': [None, None, '6.2', None]})
cmp_tf(['6.2C,6.3'], None, {'OpenSSH': ['6.3', None, '6.2', None]})
cmp_tf(['6.2C,6.3'], True, {'OpenSSH': ['6.3', None, None, None]})
cmp_tf(['6.2C,6.3'], False, {'OpenSSH': [None, None, '6.2', None]})
cmp_tf(['6.2', '6.6'], None, {'OpenSSH': ['6.2', '6.6', '6.2', '6.6']})
cmp_tf(['6.2', '6.6'], True, {'OpenSSH': ['6.2', '6.6', None, None]})
cmp_tf(['6.2', '6.6'], False, {'OpenSSH': [None, None, '6.2', '6.6']})
cmp_tf(['6.2C', '6.6'], None, {'OpenSSH': [None, '6.6', '6.2', '6.6']})
cmp_tf(['6.2C', '6.6'], True, {'OpenSSH': [None, '6.6', None, None]})
cmp_tf(['6.2C', '6.6'], False, {'OpenSSH': [None, None, '6.2', '6.6']})
cmp_tf(['6.1,6.2C', '6.6'], None, {'OpenSSH': ['6.1', '6.6', '6.2', '6.6']})
cmp_tf(['6.1,6.2C', '6.6'], True, {'OpenSSH': ['6.1', '6.6', None, None]})
cmp_tf(['6.1,6.2C', '6.6'], False, {'OpenSSH': [None, None, '6.2', '6.6']})
cmp_tf(['6.2C,6.1', '6.6'], None, {'OpenSSH': ['6.1', '6.6', '6.2', '6.6']})
cmp_tf(['6.2C,6.1', '6.6'], True, {'OpenSSH': ['6.1', '6.6', None, None]})
cmp_tf(['6.2C,6.1', '6.6'], False, {'OpenSSH': [None, None, '6.2', '6.6']})
cmp_tf(['6.3,6.2C', '6.6'], None, {'OpenSSH': ['6.3', '6.6', '6.2', '6.6']})
cmp_tf(['6.3,6.2C', '6.6'], True, {'OpenSSH': ['6.3', '6.6', None, None]})
cmp_tf(['6.3,6.2C', '6.6'], False, {'OpenSSH': [None, None, '6.2', '6.6']})
cmp_tf(['6.2C,6.3', '6.6'], None, {'OpenSSH': ['6.3', '6.6', '6.2', '6.6']})
cmp_tf(['6.2C,6.3', '6.6'], True, {'OpenSSH': ['6.3', '6.6', None, None]})
cmp_tf(['6.2C,6.3', '6.6'], False, {'OpenSSH': [None, None, '6.2', '6.6']})
cmp_tf(['6.2', '6.6', None], None, {'OpenSSH': ['6.2', '6.6', '6.2', None]})
cmp_tf(['6.2', '6.6', None], True, {'OpenSSH': ['6.2', '6.6', None, None]})
cmp_tf(['6.2', '6.6', None], False, {'OpenSSH': [None, None, '6.2', None]})
cmp_tf(['6.2C', '6.6', None], None, {'OpenSSH': [None, '6.6', '6.2', None]})
cmp_tf(['6.2C', '6.6', None], True, {'OpenSSH': [None, '6.6', None, None]})
cmp_tf(['6.2C', '6.6', None], False, {'OpenSSH': [None, None, '6.2', None]})
cmp_tf(['6.1,6.2C', '6.6', None], None, {'OpenSSH': ['6.1', '6.6', '6.2', None]})
cmp_tf(['6.1,6.2C', '6.6', None], True, {'OpenSSH': ['6.1', '6.6', None, None]})
cmp_tf(['6.1,6.2C', '6.6', None], False, {'OpenSSH': [None, None, '6.2', None]})
cmp_tf(['6.2C,6.1', '6.6', None], None, {'OpenSSH': ['6.1', '6.6', '6.2', None]})
cmp_tf(['6.2C,6.1', '6.6', None], True, {'OpenSSH': ['6.1', '6.6', None, None]})
cmp_tf(['6.2C,6.1', '6.6', None], False, {'OpenSSH': [None, None, '6.2', None]})
cmp_tf(['6.2,6.3C', '6.6', None], None, {'OpenSSH': ['6.2', '6.6', '6.3', None]})
cmp_tf(['6.2,6.3C', '6.6', None], True, {'OpenSSH': ['6.2', '6.6', None, None]})
cmp_tf(['6.2,6.3C', '6.6', None], False, {'OpenSSH': [None, None, '6.3', None]})
cmp_tf(['6.3C,6.2', '6.6', None], None, {'OpenSSH': ['6.2', '6.6', '6.3', None]})
cmp_tf(['6.3C,6.2', '6.6', None], True, {'OpenSSH': ['6.2', '6.6', None, None]})
cmp_tf(['6.3C,6.2', '6.6', None], False, {'OpenSSH': [None, None, '6.3', None]})
cmp_tf(['6.2', '6.6', '7.1'], None, {'OpenSSH': ['6.2', '6.6', '6.2', '7.1']})
cmp_tf(['6.2', '6.6', '7.1'], True, {'OpenSSH': ['6.2', '6.6', None, None]})
cmp_tf(['6.2', '6.6', '7.1'], False, {'OpenSSH': [None, None, '6.2', '7.1']})
cmp_tf(['6.1,6.2C', '6.6', '7.1'], None, {'OpenSSH': ['6.1', '6.6', '6.2', '7.1']})
cmp_tf(['6.1,6.2C', '6.6', '7.1'], True, {'OpenSSH': ['6.1', '6.6', None, None]})
cmp_tf(['6.1,6.2C', '6.6', '7.1'], False, {'OpenSSH': [None, None, '6.2', '7.1']})
cmp_tf(['6.2C,6.1', '6.6', '7.1'], None, {'OpenSSH': ['6.1', '6.6', '6.2', '7.1']})
cmp_tf(['6.2C,6.1', '6.6', '7.1'], True, {'OpenSSH': ['6.1', '6.6', None, None]})
cmp_tf(['6.2C,6.1', '6.6', '7.1'], False, {'OpenSSH': [None, None, '6.2', '7.1']})
cmp_tf(['6.2,6.3C', '6.6', '7.1'], None, {'OpenSSH': ['6.2', '6.6', '6.3', '7.1']})
cmp_tf(['6.2,6.3C', '6.6', '7.1'], True, {'OpenSSH': ['6.2', '6.6', None, None]})
cmp_tf(['6.2,6.3C', '6.6', '7.1'], False, {'OpenSSH': [None, None, '6.3', '7.1']})
cmp_tf(['6.3C,6.2', '6.6', '7.1'], None, {'OpenSSH': ['6.2', '6.6', '6.3', '7.1']})
cmp_tf(['6.3C,6.2', '6.6', '7.1'], True, {'OpenSSH': ['6.2', '6.6', None, None]})
cmp_tf(['6.3C,6.2', '6.6', '7.1'], False, {'OpenSSH': [None, None, '6.3', '7.1']})
tf1 = self._tf(['6.1,d2016.72,6.2C', '6.6,d2016.73', '7.1,d2016.74'])
tf2 = self._tf(['d2016.72,6.2C,6.1', 'd2016.73,6.6', 'd2016.74,7.1'])
tf3 = self._tf(['d2016.72,6.2C,6.1', '6.6,d2016.73', '7.1,d2016.74'])
# check without caring for output order
ov = "'OpenSSH': ['6.1', '6.6', '6.2', '7.1']"
dv = "'Dropbear SSH': ['2016.72', '2016.73', '2016.72', '2016.74']"
assert len(str(tf1)) == len(str(tf2)) == len(str(tf3))
assert ov in str(tf1) and ov in str(tf2) and ov in str(tf3)
assert dv in str(tf1) and dv in str(tf2) and dv in str(tf3)
assert ov in repr(tf1) and ov in repr(tf2) and ov in repr(tf3)
assert dv in repr(tf1) and dv in repr(tf2) and dv in repr(tf3)
def test_timeframe_object(self):
tf = self._tf(['6.1,6.2C', '6.6', '7.1'])
assert 'OpenSSH' in tf
assert 'Dropbear SSH' not in tf
assert 'libssh' not in tf
assert 'unknown' not in tf
assert tf['OpenSSH'] == ('6.1', '6.6', '6.2', '7.1')
assert tf['Dropbear SSH'] == (None, None, None, None)
assert tf['libssh'] == (None, None, None, None)
assert tf['unknown'] == (None, None, None, None)
assert tf.get_from('OpenSSH', True) == '6.1'
assert tf.get_till('OpenSSH', True) == '6.6'
assert tf.get_from('OpenSSH', False) == '6.2'
assert tf.get_till('OpenSSH', False) == '7.1'
tf = self._tf(['6.1,d2016.72,6.2C', '6.6,d2016.73', '7.1,d2016.74'])
assert 'OpenSSH' in tf
assert 'Dropbear SSH' in tf
assert 'libssh' not in tf
assert 'unknown' not in tf
assert tf['OpenSSH'] == ('6.1', '6.6', '6.2', '7.1')
assert tf['Dropbear SSH'] == ('2016.72', '2016.73', '2016.72', '2016.74')
assert tf['libssh'] == (None, None, None, None)
assert tf['unknown'] == (None, None, None, None)
assert tf.get_from('OpenSSH', True) == '6.1'
assert tf.get_till('OpenSSH', True) == '6.6'
assert tf.get_from('OpenSSH', False) == '6.2'
assert tf.get_till('OpenSSH', False) == '7.1'
assert tf.get_from('Dropbear SSH', True) == '2016.72'
assert tf.get_till('Dropbear SSH', True) == '2016.73'
assert tf.get_from('Dropbear SSH', False) == '2016.72'
assert tf.get_till('Dropbear SSH', False) == '2016.74'
ov = "'OpenSSH': ['6.1', '6.6', '6.2', '7.1']"
dv = "'Dropbear SSH': ['2016.72', '2016.73', '2016.72', '2016.74']"
assert ov in str(tf)
assert dv in str(tf)
assert ov in repr(tf)
assert dv in repr(tf)

218
test/test_utils.py Normal file
View File

@ -0,0 +1,218 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import sys
import pytest
# pylint: disable=attribute-defined-outside-init
class TestUtils(object):
@pytest.fixture(autouse=True)
def init(self, ssh_audit):
self.utils = ssh_audit.Utils
self.PY3 = sys.version_info >= (3,)
def test_to_bytes_py2(self):
if self.PY3:
return
# binary_type (native str, bytes as str)
assert self.utils.to_bytes('fran\xc3\xa7ais') == 'fran\xc3\xa7ais'
assert self.utils.to_bytes(b'fran\xc3\xa7ais') == 'fran\xc3\xa7ais'
# text_type (unicode)
assert self.utils.to_bytes(u'fran\xe7ais') == 'fran\xc3\xa7ais'
# other
with pytest.raises(TypeError):
self.utils.to_bytes(123)
def test_to_bytes_py3(self):
if not self.PY3:
return
# binary_type (bytes)
assert self.utils.to_bytes(b'fran\xc3\xa7ais') == b'fran\xc3\xa7ais'
# text_type (native str as unicode, unicode)
assert self.utils.to_bytes('fran\xe7ais') == b'fran\xc3\xa7ais'
assert self.utils.to_bytes(u'fran\xe7ais') == b'fran\xc3\xa7ais'
# other
with pytest.raises(TypeError):
self.utils.to_bytes(123)
def test_to_utext_py2(self):
if self.PY3:
return
# binary_type (native str, bytes as str)
assert self.utils.to_utext('fran\xc3\xa7ais') == u'fran\xe7ais'
assert self.utils.to_utext(b'fran\xc3\xa7ais') == u'fran\xe7ais'
# text_type (unicode)
assert self.utils.to_utext(u'fran\xe7ais') == u'fran\xe7ais'
# other
with pytest.raises(TypeError):
self.utils.to_utext(123)
def test_to_utext_py3(self):
if not self.PY3:
return
# binary_type (bytes)
assert self.utils.to_utext(b'fran\xc3\xa7ais') == u'fran\xe7ais'
# text_type (native str as unicode, unicode)
assert self.utils.to_utext('fran\xe7ais') == 'fran\xe7ais'
assert self.utils.to_utext(u'fran\xe7ais') == u'fran\xe7ais'
# other
with pytest.raises(TypeError):
self.utils.to_utext(123)
def test_to_ntext_py2(self):
if self.PY3:
return
# str (native str, bytes as str)
assert self.utils.to_ntext('fran\xc3\xa7ais') == 'fran\xc3\xa7ais'
assert self.utils.to_ntext(b'fran\xc3\xa7ais') == 'fran\xc3\xa7ais'
# text_type (unicode)
assert self.utils.to_ntext(u'fran\xe7ais') == 'fran\xc3\xa7ais'
# other
with pytest.raises(TypeError):
self.utils.to_ntext(123)
def test_to_ntext_py3(self):
if not self.PY3:
return
# str (native str)
assert self.utils.to_ntext('fran\xc3\xa7ais') == 'fran\xc3\xa7ais'
assert self.utils.to_ntext(u'fran\xe7ais') == 'fran\xe7ais'
# binary_type (bytes)
assert self.utils.to_ntext(b'fran\xc3\xa7ais') == 'fran\xe7ais'
# other
with pytest.raises(TypeError):
self.utils.to_ntext(123)
def test_is_ascii_py2(self):
if self.PY3:
return
# text_type (unicode)
assert self.utils.is_ascii(u'francais') is True
assert self.utils.is_ascii(u'fran\xe7ais') is False
# str
assert self.utils.is_ascii('francais') is True
assert self.utils.is_ascii('fran\xc3\xa7ais') is False
# other
assert self.utils.is_ascii(123) is False
def test_is_ascii_py3(self):
if not self.PY3:
return
# text_type (str)
assert self.utils.is_ascii('francais') is True
assert self.utils.is_ascii(u'francais') is True
assert self.utils.is_ascii('fran\xe7ais') is False
assert self.utils.is_ascii(u'fran\xe7ais') is False
# other
assert self.utils.is_ascii(123) is False
def test_to_ascii_py2(self):
if self.PY3:
return
# text_type (unicode)
assert self.utils.to_ascii(u'francais') == 'francais'
assert self.utils.to_ascii(u'fran\xe7ais') == 'fran?ais'
assert self.utils.to_ascii(u'fran\xe7ais', 'ignore') == 'franais'
# str
assert self.utils.to_ascii('francais') == 'francais'
assert self.utils.to_ascii('fran\xc3\xa7ais') == 'fran??ais'
assert self.utils.to_ascii('fran\xc3\xa7ais', 'ignore') == 'franais'
with pytest.raises(TypeError):
self.utils.to_ascii(123)
def test_to_ascii_py3(self):
if not self.PY3:
return
# text_type (str)
assert self.utils.to_ascii('francais') == 'francais'
assert self.utils.to_ascii(u'francais') == 'francais'
assert self.utils.to_ascii('fran\xe7ais') == 'fran?ais'
assert self.utils.to_ascii('fran\xe7ais', 'ignore') == 'franais'
assert self.utils.to_ascii(u'fran\xe7ais') == 'fran?ais'
assert self.utils.to_ascii(u'fran\xe7ais', 'ignore') == 'franais'
with pytest.raises(TypeError):
self.utils.to_ascii(123)
def test_is_print_ascii_py2(self):
if self.PY3:
return
# text_type (unicode)
assert self.utils.is_print_ascii(u'francais') is True
assert self.utils.is_print_ascii(u'francais\n') is False
assert self.utils.is_print_ascii(u'fran\xe7ais') is False
assert self.utils.is_print_ascii(u'fran\xe7ais\n') is False
# str
assert self.utils.is_print_ascii('francais') is True
assert self.utils.is_print_ascii('francais\n') is False
assert self.utils.is_print_ascii('fran\xc3\xa7ais') is False
# other
assert self.utils.is_print_ascii(123) is False
def test_is_print_ascii_py3(self):
if not self.PY3:
return
# text_type (str)
assert self.utils.is_print_ascii('francais') is True
assert self.utils.is_print_ascii('francais\n') is False
assert self.utils.is_print_ascii(u'francais') is True
assert self.utils.is_print_ascii(u'francais\n') is False
assert self.utils.is_print_ascii('fran\xe7ais') is False
assert self.utils.is_print_ascii(u'fran\xe7ais') is False
# other
assert self.utils.is_print_ascii(123) is False
def test_to_print_ascii_py2(self):
if self.PY3:
return
# text_type (unicode)
assert self.utils.to_print_ascii(u'francais') == 'francais'
assert self.utils.to_print_ascii(u'francais\n') == 'francais?'
assert self.utils.to_print_ascii(u'fran\xe7ais') == 'fran?ais'
assert self.utils.to_print_ascii(u'fran\xe7ais\n') == 'fran?ais?'
assert self.utils.to_print_ascii(u'fran\xe7ais', 'ignore') == 'franais'
assert self.utils.to_print_ascii(u'fran\xe7ais\n', 'ignore') == 'franais'
# str
assert self.utils.to_print_ascii('francais') == 'francais'
assert self.utils.to_print_ascii('francais\n') == 'francais?'
assert self.utils.to_print_ascii('fran\xc3\xa7ais') == 'fran??ais'
assert self.utils.to_print_ascii('fran\xc3\xa7ais\n') == 'fran??ais?'
assert self.utils.to_print_ascii('fran\xc3\xa7ais', 'ignore') == 'franais'
assert self.utils.to_print_ascii('fran\xc3\xa7ais\n', 'ignore') == 'franais'
with pytest.raises(TypeError):
self.utils.to_print_ascii(123)
def test_to_print_ascii_py3(self):
if not self.PY3:
return
# text_type (str)
assert self.utils.to_print_ascii('francais') == 'francais'
assert self.utils.to_print_ascii('francais\n') == 'francais?'
assert self.utils.to_print_ascii(u'francais') == 'francais'
assert self.utils.to_print_ascii(u'francais\n') == 'francais?'
assert self.utils.to_print_ascii('fran\xe7ais') == 'fran?ais'
assert self.utils.to_print_ascii('fran\xe7ais\n') == 'fran?ais?'
assert self.utils.to_print_ascii('fran\xe7ais', 'ignore') == 'franais'
assert self.utils.to_print_ascii('fran\xe7ais\n', 'ignore') == 'franais'
assert self.utils.to_print_ascii(u'fran\xe7ais') == 'fran?ais'
assert self.utils.to_print_ascii(u'fran\xe7ais\n') == 'fran?ais?'
assert self.utils.to_print_ascii(u'fran\xe7ais', 'ignore') == 'franais'
assert self.utils.to_print_ascii(u'fran\xe7ais\n', 'ignore') == 'franais'
with pytest.raises(TypeError):
self.utils.to_print_ascii(123)
def test_ctoi(self):
assert self.utils.ctoi(123) == 123
assert self.utils.ctoi('ABC') == 65
def test_parse_int(self):
assert self.utils.parse_int(123) == 123
assert self.utils.parse_int('123') == 123
assert self.utils.parse_int(-123) == -123
assert self.utils.parse_int('-123') == -123
assert self.utils.parse_int('abc') == 0
def test_unique_seq(self):
assert self.utils.unique_seq((1, 2, 2, 3, 3, 3)) == (1, 2, 3)
assert self.utils.unique_seq((3, 3, 3, 2, 2, 1)) == (3, 2, 1)
assert self.utils.unique_seq([1, 2, 2, 3, 3, 3]) == [1, 2, 3]
assert self.utils.unique_seq([3, 3, 3, 2, 2, 1]) == [3, 2, 1]

View File

@ -200,7 +200,7 @@ class TestVersionCompare(object):
versions.append('0.5.{0}'.format(i))
for i in range(0, 6):
versions.append('0.6.{0}'.format(i))
for i in range(0, 4):
for i in range(0, 5):
versions.append('0.7.{0}'.format(i))
l = len(versions)
for i in range(l):

412
test/tools/ci-linux.sh Executable file
View File

@ -0,0 +1,412 @@
#!/bin/sh
CI_VERBOSE=1
ci_err_msg() { echo "[ci] error: $1" >&2; }
ci_err() { [ $1 -ne 0 ] && ci_err_msg "$2" && exit 1; }
ci_is_osx() { [ X"$(uname -s)" == X"Darwin" ]; }
ci_get_pypy_ver() {
local _v="$1"
[ -z "$_v" ] && _v=$(python -V 2>&1)
case "$_v" in
pypy-*|pypy2-*|pypy3-*|pypy3.*) echo "$_v"; return 0 ;;
pypy|pypy2|pypy3) echo "$_v-unknown"; return 0 ;;
esac
echo "$_v" | tail -1 | grep -qi pypy
if [ $? -eq 0 ]; then
local _py_ver=$(echo "$_v" | head -1 | cut -d ' ' -sf 2)
local _pypy_ver=$(echo "$_v" | tail -1 | cut -d ' ' -sf 2)
[ -z "${_py_ver} " ] && _py_ver=2
[ -z "${_pypy_ver}" ] && _pypy_ver="unknown"
case "${_py_ver}" in
2*) echo "pypy-${_pypy_ver}" ;;
3.3*) echo "pypy3.3-${_pypy_ver}" ;;
3.5*) echo "pypy3.5-${_pypy_ver}" ;;
*) echo "pypy3-${_pypy_ver}" ;;
esac
return 0
else
return 1
fi
}
ci_get_py_ver() {
local _v
case "$1" in
py26) _v=2.6.9 ;;
py27) _v=2.7.13 ;;
py33) _v=3.3.6 ;;
py34) _v=3.4.6 ;;
py35) _v=3.5.3 ;;
py36) _v=3.6.1 ;;
py37) _v=3.7-dev ;;
pypy) ci_is_osx && _v=pypy2-5.7.0 || _v=pypy-portable-5.7.0 ;;
pypy3) ci_is_osx && _v=pypy3.3-5.5-alpha || _v=pypy3-portable-5.7.0 ;;
*)
[ -z "$1" ] && set -- "$(python -V 2>&1)"
_v=$(ci_get_pypy_ver "$1")
[ -z "$_v" ] && _v=$(echo "$_v" | head -1 | cut -d ' ' -sf 2)
;;
esac
echo "${_v}"
return 0
}
ci_get_py_env() {
[ -z "$1" ] && set -- "$(python -V 2>&1)"
case "$(ci_get_pypy_ver "$1")" in
pypy|pypy2|pypy-*|pypy2-*) echo "pypy" ;;
pypy3|pypy3*) echo "pypy3" ;;
*)
local _v=$(echo "$1" | head -1 | sed -e 's/[^0-9]//g' | cut -c1-2)
echo "py${_v}"
esac
return 0
}
ci_pyenv_setup() {
[ ${CI_VERBOSE} -gt 0 ] && echo "[ci] install pyenv"
rm -rf ~/.pyenv
git clone --depth 1 https://github.com/yyuu/pyenv.git ~/.pyenv
PYENV_ROOT=$HOME/.pyenv
PATH="$HOME/.pyenv/bin:$PATH"
eval "$(pyenv init -)"
ci_err $? "failed to init pyenv"
[ ${CI_VERBOSE} -gt 0 ] && echo "[ci] pyenv init: $(pyenv -v 2>&1)"
return 0
}
ci_pyenv_install() {
CI_PYENV_CACHE=~/.pyenv.cache
type pyenv > /dev/null 2>&1
ci_err $? "pyenv not found"
local _py_ver=$(ci_get_py_ver "$1")
local _py_env=$(ci_get_py_env "${_py_ver}")
local _nocache
case "${_py_env}" in
py37) _nocache=1 ;;
esac
[ ${CI_VERBOSE} -gt 0 ] && echo "[ci] pyenv install: ${_py_env}/${_py_ver}"
[ -z "${PYENV_ROOT}" ] && PYENV_ROOT="$HOME/.pyenv"
local _py_ver_dir="${PYENV_ROOT}/versions/${_py_ver}"
local _py_ver_cached_dir="${CI_PYENV_CACHE}/${_py_ver}"
if [ -z "${_nocache}" ]; then
if [ ! -d "${_py_ver_dir}" ]; then
if [ -d "${_py_ver_cached_dir}" ]; then
[ ${CI_VERBOSE} -gt 0 ] && echo "[ci] pyenv reuse ${_py_ver}"
ln -s "${_py_ver_cached_dir}" "${_py_ver_dir}"
fi
fi
fi
if [ ! -d "${_py_ver_dir}" ]; then
pyenv install -s "${_py_ver}"
ci_err $? "pyenv failed to install ${_py_ver}"
if [ -z "${_nocache}" ]; then
[ ${CI_VERBOSE} -gt 0 ] && echo "[ci] pyenv cache ${_py_ver}"
rm -rf -- "${_py_ver_cached_dir}"
mkdir -p -- "${CI_PYENV_CACHE}"
mv "${_py_ver_dir}" "${_py_ver_cached_dir}"
ln -s "${_py_ver_cached_dir}" "${_py_ver_dir}"
fi
fi
pyenv rehash
return 0
}
ci_pyenv_use() {
type pyenv > /dev/null 2>&1
ci_err $? "pyenv not found"
local _py_ver=$(ci_get_py_ver "$1")
pyenv shell "${_py_ver}"
ci_err $? "pyenv could not use ${_py_ver}"
[ ${CI_VERBOSE} -gt 0 ] && echo "[ci] pyenv using python: $(python -V 2>&1)"
return 0
}
ci_pip_setup() {
local _py_ver=$(ci_get_py_ver "$1")
local _py_env=$(ci_get_py_env "${_py_ver}")
[ ${CI_VERBOSE} -gt 0 ] && echo "[ci] install pip/venv for ${_py_env}/${_py_ver}"
PIPOPT=$(python -c 'import sys; print("" if hasattr(sys, "real_prefix") else "--user")')
if [ -z "${_py_env##py2*}" ]; then
curl -O https://bootstrap.pypa.io/get-pip.py
python get-pip.py ${PIPOPT}
ci_err $? "failed to install pip"
fi
if [ X"${_py_env}" == X"py26" ]; then
python -c 'import pip; pip.main();' install ${PIPOPT} -U pip virtualenv
else
python -m pip install ${PIPOPT} -U pip virtualenv
fi
ci_err $? "failed to upgrade pip/venv" || return 0
}
ci_venv_setup() {
local _py_ver=$(ci_get_py_ver "$1")
local _py_env=$(ci_get_py_env "${_py_ver}")
[ ${CI_VERBOSE} -gt 0 ] && echo "[ci] create venv for ${_py_env}/${_py_ver}"
local VENV_DIR=~/.venv/${_py_ver}
mkdir -p -- ~/.venv
rm -rf -- "${VENV_DIR}"
if [ X"${_py_env}" == X"py26" ]; then
python -c 'import virtualenv; virtualenv.main();' "${VENV_DIR}"
else
python -m virtualenv "${VENV_DIR}"
fi
ci_err $? "failed to create venv" || return 0
}
ci_venv_use() {
local _py_ver=$(ci_get_py_ver "$1")
local _py_env=$(ci_get_py_env "${_py_ver}")
local VENV_DIR=~/.venv/${_py_ver}
. "${VENV_DIR}/bin/activate"
ci_err $? "could not actiavte virtualenv"
[ ${CI_VERBOSE} -gt 0 ] && echo "[ci] venv using python: $(python -V 2>&1)"
return 0
}
ci_get_filedir() {
local _sdir=$(cd -- "$(dirname "$0")" && pwd)
local _pdir=$(pwd)
if [ -z "${_pdir##${_sdir}*}" ]; then
_sdir="${_pdir}"
fi
local _first=1
while [ X"${_sdir}" != X"/" ]; do
if [ ${_first} -eq 1 ]; then
_first=0
local _f=$(find "${_sdir}" -name "$1" | head -1)
if [ -n "${_f}" ]; then
echo $(dirname -- "${_f}")
return 0
fi
else
_f=$(find "${_sdir}" -mindepth 1 -maxdepth 1 -name "$1" | head -1)
fi
[ -n "${_f}" ] && echo "${_sdir}" && return 0
_sdir=$(cd -- "${_sdir}/.." && pwd)
done
return 1
}
ci_sq_ensure_java() {
type java >/dev/null 2>&1
if [ $? -ne 0 ]; then
ci_err_msg "java not found"
return 1
fi
local _java_ver=$(java -version 2>&1 | head -1 | sed -e 's/[^0-9\._]//g')
if [ -z "${_java_ver##1.8*}" ]; then
return 0
fi
ci_err_msg "unsupported java version: ${_java_ver}"
return 1
}
ci_sq_ensure_scanner() {
local _cli_version="3.0.0.702"
local _cli_basedir="$HOME/.bin"
local _cli_postfix=""
case "$(uname -s)" in
Linux)
[ X"$(uname -m)" = X"x86_64" ] && _cli_postfix="-linux"
[ X"$(uname -m)" = X"amd64" ] && _cli_postfix="-linux"
;;
Darwin) _cli_postfix="-macosx" ;;
esac
if [ X"${_cli_postfix}" = X"" ]; then
ci_sq_ensure_java || return 1
fi
if [ X"${SONAR_SCANNER_PATH}" != X"" ]; then
if [ -e "${SONAR_SCANNER_PATH}" ]; then
return 0
fi
fi
local _cli_fname="sonar-scanner-cli-${_cli_version}${_cli_postfix}"
[ ${CI_VERBOSE} -gt 0 ] && echo "[ci] ensure scanner ${_cli_fname}"
local _cli_dname="sonar-scanner-${_cli_version}${_cli_postfix}"
local _cli_archive="${_cli_basedir}/${_cli_fname}.zip"
local _cli_dir="${_cli_basedir}/${_cli_dname}"
local _cli_url="https://sonarsource.bintray.com/Distribution/sonar-scanner-cli/${_cli_fname}.zip"
if [ ! -e "${_cli_archive}" ]; then
mkdir -p -- "${_cli_basedir}" > /dev/null 2>&1
if [ $? -ne 0 ]; then
ci_err_msg "could not create ${_cli_basedir}"
return 1
fi
[ ${CI_VERBOSE} -gt 0 ] && echo "[ci] downloading ${_cli_fname}"
curl -kL -o "${_cli_archive}" "${_cli_url}"
[ $? -ne 0 ] && ci_err_msg "download failed" && return 1
[ ! -e "${_cli_archive}" ] && ci_err_msg "download verify" && return 1
fi
if [ ! -d "${_cli_dir}" ]; then
[ ${CI_VERBOSE} -gt 0 ] && echo "[ci] extracting ${_cli_fname}"
unzip -od "${_cli_basedir}" "${_cli_archive}"
[ $? -ne 0 ] && ci_err_msg "extract failed" && return 1
[ ! -d "${_cli_dir}" ] && ci_err_msg "extract verify" && return 1
fi
if [ ! -e "${_cli_dir}/bin/sonar-scanner" ]; then
ci_err_msg "sonar-scanner binary not found."
return 1
fi
SONAR_SCANNER_PATH="${_cli_dir}/bin/sonar-scanner"
return 0
}
ci_sq_run() {
if [ X"${SONAR_SCANNER_PATH}" = X"" ]; then
ci_err_msg "environment variable SONAR_SCANNER_PATH not set"
return 1
fi
if [ X"${SONAR_HOST_URL}" = X"" ]; then
ci_err_msg "environment variable SONAR_HOST_URL not set"
return 1
fi
if [ X"${SONAR_AUTH_TOKEN}" = X"" ]; then
ci_err_msg "environment variable SONAR_AUTH_TOKEN not set"
return 1
fi
local _pdir=$(ci_get_filedir "ssh-audit.py")
if [ -z "${_pdir}" ]; then
ci_err_msg "failed to find project directory"
return 1
fi
local _odir=$(pwd)
cd -- "${_pdir}"
local _branch=$(git name-rev --name-only HEAD | cut -d '~' -f 1)
case "${_branch}" in
master) ;;
develop) ;;
*) ci_err_msg "unknown branch: ${_branch}"; return 1 ;;
esac
local _junit=$(cd -- "${_pdir}" && ls -1 reports/junit.*.xml | sort -r | head -1)
if [ X"${_junit}" = X"" ]; then
ci_err_msg "no junit.xml found"
return 1
fi
local _project_ver=$(grep VERSION ssh-audit.py | head -1 | cut -d "'" -f 2)
if [ -z "${_project_ver}" ]; then
ci_err_msg "failed to get project version"
return 1
fi
if [ -z "${_project_ver##*dev}" ]; then
local _git_commit=$(git rev-parse --short=8 HEAD)
_project_ver="${_project_ver}.${_git_commit}"
fi
[ ${CI_VERBOSE} -gt 0 ] && echo "[ci] run sonar-scanner for ${_project_ver}"
"${SONAR_SCANNER_PATH}" -X \
-Dsonar.projectKey=arthepsy-github:ssh-audit \
-Dsonar.sources=ssh-audit.py \
-Dsonar.tests=test \
-Dsonar.test.inclusions=test/*.py \
-Dsonar.host.url="${SONAR_HOST_URL}" \
-Dsonar.projectName=ssh-audit \
-Dsonar.projectVersion="${_project_ver}" \
-Dsonar.branch="${_branch}" \
-Dsonar.python.coverage.overallReportPath=reports/coverage.xml \
-Dsonar.python.xunit.reportPath="${_junit}" \
-Dsonar.organization=arthepsy-github \
-Dsonar.login="${SONAR_AUTH_TOKEN}"
cd -- "${_odir}"
return 0
}
ci_run_wrapped() {
local _versions=$(echo "${PY_VER}" | sed -e 's/,/ /g')
[ -z "${_versions}" ] && eval "$1"
for _i in ${_versions}; do
local _v=$(echo "$_i" | cut -d '/' -f 1)
local _o=$(echo "$_i" | cut -d '/' -sf 2)
[ -z "${_o}" ] && _o="${PY_ORIGIN}"
eval "$1" "${_v}" "${_o}" || return 1
done
return 0
}
ci_step_before_install_wrapped() {
local _py_ver="$1"
local _py_ori="$2"
case "${_py_ori}" in
pyenv)
if [ "${CI_PYENV_SETUP}" -eq 0 ]; then
ci_pyenv_setup
CI_PYENV_SETUP=1
fi
ci_pyenv_install "${_py_ver}" || return 1
ci_pyenv_use "${_py_ver}" || return 1
;;
esac
ci_pip_setup "${_py_ver}" || return 1
ci_venv_setup "${_py_ver}" || return 1
return 0
}
ci_step_before_install() {
if ci_is_osx; then
[ ${CI_VERBOSE} -gt 0 ] && sw_vers
brew update || brew update
brew install autoconf pkg-config openssl readline xz
brew upgrade autoconf pkg-config openssl readline xz
PY_ORIGIN=pyenv
fi
CI_PYENV_SETUP=0
ci_run_wrapped "ci_step_before_install_wrapped" || return 1
if [ "${CI_PYENV_SETUP}" -eq 1 ]; then
pyenv shell --unset
[ ${CI_VERBOSE} -gt 0 ] && pyenv versions
fi
return 0
}
ci_step_install_wrapped() {
local _py_ver="$1"
ci_venv_use "${_py_ver}"
pip install -U tox coveralls codecov
ci_err $? "failed to install dependencies" || return 0
}
ci_step_script_wrapped() {
local _py_ver="$1"
local _py_ori="$2"
local _py_env=$(ci_get_py_env "${_py_ver}")
ci_venv_use "${_py_ver}" || return 1
if [ -z "${_py_env##*py3*}" ]; then
if [ -z "${_py_env##*pypy3*}" ]; then
# NOTE: workaround for travis environment
_pydir=$(dirname $(which python))
ln -s -- "${_pydir}/python" "${_pydir}/pypy3"
# NOTE: do not lint, as it hangs when flake8 is run
# NOTE: do not type, as it can't install dependencies
TOXENV=${_py_env}-test
else
TOXENV=${_py_env}-test,${_py_env}-type,${_py_env}-lint
fi
else
# NOTE: do not type, as it isn't supported on py2x
TOXENV=${_py_env}-test,${_py_env}-lint
fi
tox -e $TOXENV,cov
ci_err $? "tox failed" || return 0
}
ci_step_success_wrapped() {
local _py_ver="$1"
local _py_ori="$2"
if [ X"${SQ}" = X"1" ]; then
ci_sq_ensure_scanner && ci_sq_run
fi
ci_venv_use "${_py_ver}" || return 1
coveralls
codecov
}
ci_step_failure() {
cat .tox/log/*
cat .tox/*/log/*
}
ci_step_install() { ci_run_wrapped "ci_step_install_wrapped"; }
ci_step_script() { ci_run_wrapped "ci_step_script_wrapped"; }
ci_step_success() { ci_run_wrapped "ci_step_success_wrapped"; }

131
test/tools/ci-win.cmd Normal file
View File

@ -0,0 +1,131 @@
@ECHO OFF
IF "%PYTHON%" == "" (
ECHO PYTHON environment variable not set
EXIT 1
)
SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%"
FOR /F %%i IN ('python -c "import platform; print(platform.python_version());"') DO (
SET PYTHON_VERSION=%%i
)
SET PYTHON_VERSION_MAJOR=%PYTHON_VERSION:~0,1%
IF "%PYTHON_VERSION:~3,1%" == "." (
SET PYTHON_VERSION_MINOR=%PYTHON_VERSION:~2,1%
) ELSE (
SET PYTHON_VERSION_MINOR=%PYTHON_VERSION:~2,2%
)
FOR /F %%i IN ('python -c "import struct; print(struct.calcsize(\"P\")*8)"') DO (
SET PYTHON_ARCH=%%i
)
CALL :devenv
IF /I "%1"=="" (
SET target=test
) ELSE (
SET target=%1
)
echo [CI] TARGET=%target%
GOTO %target%
:devenv
SET WIN_SDK_ROOT=C:\Program Files\Microsoft SDKs\Windows
SET VS2015_ROOT=C:\Program Files (x86)\Microsoft Visual Studio 14.0
IF %PYTHON_VERSION_MAJOR% == 2 (
SET WINDOWS_SDK_VERSION="v7.0"
) ELSE IF %PYTHON_VERSION_MAJOR% == 3 (
IF %PYTHON_VERSION_MAJOR% LEQ 4 (
SET WINDOWS_SDK_VERSION="v7.1"
) ELSE (
SET WINDOWS_SDK_VERSION="2015"
)
) ELSE (
ECHO Unsupported Python version: "%PYTHON_VERSION%"
EXIT 1
)
SETLOCAL ENABLEDELAYEDEXPANSION
IF %PYTHON_ARCH% == 32 (SET PYTHON_ARCHX=x86) ELSE (SET PYTHON_ARCHX=x64)
IF %WINDOWS_SDK_VERSION% == "2015" (
"%VS2015_ROOT%\VC\vcvarsall.bat" %PYTHON_ARCHX%
) ELSE (
SET DISTUTILS_USE_SDK=1
SET MSSdk=1
"%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Setup\WindowsSdkVer.exe" -q -version:%WINDOWS_SDK_VERSION%
"%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Bin\SetEnv.cmd" /%PYTHON_ARCHX% /release
)
GOTO :eof
:install
pip install --user --upgrade pip virtualenv
SET VENV_DIR=.venv\%PYTHON_VERSION%
rmdir /s /q %VENV_DIR% > nul 2>nul
mkdir .venv > nul 2>nul
IF "%PYTHON_VERSION_MAJOR%%PYTHON_VERSION_MINOR%" == "26" (
python -c "import virtualenv; virtualenv.main();" %VENV_DIR%
) ELSE (
python -m virtualenv %VENV_DIR%
)
CALL %VENV_DIR%\Scripts\activate
python -V
pip install tox
deactivate
GOTO :eof
:install_deps
SET LXML_FILE=
SET LXML_URL=
IF %PYTHON_VERSION_MAJOR% == 3 (
IF %PYTHON_VERSION_MINOR% == 3 (
IF %PYTHON_ARCH% == 32 (
SET LXML_FILE=lxml-3.7.3.win32-py3.3.exe
SET LXML_URL=https://pypi.python.org/packages/66/fd/b82a54e7a15e91184efeef4b659379d0581a73cf78239d70feb0f0877841/lxml-3.7.3.win32-py3.3.exe
) ELSE (
SET LXML_FILE=lxml-3.7.3.win-amd64-py3.3.exe
SET LXML_URL=https://pypi.python.org/packages/dc/bc/4742b84793fa1fd991b5d2c6f2e5d32695659d6cfedf5c66aef9274a8723/lxml-3.7.3.win-amd64-py3.3.exe
)
) ELSE IF %PYTHON_VERSION_MINOR% == 4 (
IF %PYTHON_ARCH% == 32 (
SET LXML_FILE=lxml-3.7.3.win32-py3.4.exe
SET LXML_URL=https://pypi.python.org/packages/88/33/265459d68d465ddc707621e6471989f5c2cb0d43f230f516800ffd629af7/lxml-3.7.3.win32-py3.4.exe
) ELSE (
SET LXML_FILE=lxml-3.7.3.win-amd64-py3.4.exe
SET LXML_URL=https://pypi.python.org/packages/2d/65/e47db7f36a69a1b59b4f661e42d699d6c43e663b8fd91035e6f7681d017e/lxml-3.7.3.win-amd64-py3.4.exe
)
)
)
IF NOT "%LXML_FILE%" == "" (
CALL :download %LXML_URL% .downloads\%LXML_FILE%
easy_install --user .downloads\%LXML_FILE%
)
GOTO :eof
:test
SET VENV_DIR=.venv\%PYTHON_VERSION%
CALL %VENV_DIR%\Scripts\activate
IF "%TOXENV%" == "" (
SET TOXENV=py%PYTHON_VERSION_MAJOR%%PYTHON_VERSION_MINOR%
)
IF "%PYTHON_VERSION_MAJOR%%PYTHON_VERSION_MINOR%" == "26" (
SET TOX=python -c "from tox import cmdline; cmdline()"
) ELSE (
SET TOX=python -m tox
)
IF %PYTHON_VERSION_MAJOR% == 3 (
IF %PYTHON_VERSION_MINOR% LEQ 4 (
:: Python 3.3 and 3.4 does not support typed-ast (mypy dependency)
%TOX% --sitepackages -e %TOXENV%-test,%TOXENV%-lint,cov || EXIT 1
) ELSE (
%TOX% --sitepackages -e %TOXENV%-test,%TOXENV%-type,%TOXENV%-lint,cov || EXIT 1
)
) ELSE (
%TOX% --sitepackages -e %TOXENV%-test,%TOXENV%-lint,cov || EXIT 1
)
GOTO :eof
:download
IF NOT EXIST %2 (
IF NOT EXIST .downloads\ mkdir .downloads
powershell -command "(new-object net.webclient).DownloadFile('%1', '%2')" || EXIT 1
)
GOTO :eof

158
tox.ini Normal file
View File

@ -0,0 +1,158 @@
[tox]
envlist =
py26-{test,vulture}
py{27,py,py3}-{test,pylint,flake8,vulture}
py{33,34,35,36,37}-{test,mypy,pylint,flake8,vulture}
cov
skipsdist = true
skip_missing_interpreters = true
[testenv]
deps =
test: pytest==3.0.7
test,cov: {[testenv:cov]deps}
test,py{33,34,35,36,37}-{type,mypy}: colorama==0.3.7
py{33,34,35,36,37}-{type,mypy}: {[testenv:mypy]deps}
py{27,py,py3,33,34,35,36,37}-{lint,pylint},lint: {[testenv:pylint]deps}
py{27,py,py3,33,34,35,36,37}-{lint,flake8},lint: {[testenv:flake8]deps}
py{27,py,py3,33,34,35,36,37}-{lint,vulture},lint: {[testenv:vulture]deps}
setenv =
SSHAUDIT = {toxinidir}/ssh-audit.py
test: COVERAGE_FILE = {toxinidir}/.coverage.{envname}
type,mypy: MYPYPATH = {toxinidir}/test/stubs
type,mypy: MYPYHTML = {toxinidir}/reports/html/mypy
commands =
test: coverage run --source ssh-audit -m -- \
test: pytest -v --junitxml={toxinidir}/reports/junit.{envname}.xml {posargs:test}
test: coverage report --show-missing
test: coverage html -d {toxinidir}/reports/html/coverage.{envname}
py{33,34,35,36,37}-{type,mypy}: {[testenv:mypy]commands}
py{27,py,py3,33,34,35,36,37}-{lint,pylint},lint: {[testenv:pylint]commands}
py{27,py,py3,33,34,35,36,37}-{lint,flake8},lint: {[testenv:flake8]commands}
py{27,py,py3,33,34,35,36,37}-{lint,vulture},lint: {[testenv:vulture]commands}
ignore_outcome =
type: true
lint: true
[testenv:cov]
deps =
coverage==4.3.4
setenv =
COVERAGE_FILE = {toxinidir}/.coverage
commands =
coverage erase
coverage combine
coverage report --show-missing
coverage xml -i -o {toxinidir}/reports/coverage.xml
coverage html -d {toxinidir}/reports/html/coverage
[testenv:mypy]
deps =
colorama==0.3.7
lxml==3.7.3
mypy==0.501
commands =
mypy \
--show-error-context \
--config-file {toxinidir}/tox.ini \
--html-report {env:MYPYHTML}.py3.{envname} \
{posargs:{env:SSHAUDIT}}
mypy \
-2 \
--no-warn-incomplete-stub \
--show-error-context \
--config-file {toxinidir}/tox.ini \
--html-report {env:MYPYHTML}.py2.{envname} \
{posargs:{env:SSHAUDIT}}
[testenv:pylint]
deps =
mccabe
pylint
commands =
pylint \
--rcfile tox.ini \
--load-plugins=pylint.extensions.bad_builtin \
--load-plugins=pylint.extensions.check_elif \
--load-plugins=pylint.extensions.mccabe \
{posargs:{env:SSHAUDIT}}
[testenv:flake8]
deps =
flake8
commands =
flake8 {posargs:{env:SSHAUDIT}}
[testenv:vulture]
deps =
vulture
commands =
python -c "import sys; from subprocess import Popen, PIPE; \
a = ['vulture'] + r'{posargs:{env:SSHAUDIT}}'.split(' '); \
o = Popen(a, shell=False, stdout=PIPE).communicate()[0]; \
l = [x for x in o.split(b'\n') if x and b'Unused import' not in x]; \
print(b'\n'.join(l).decode('utf-8')); \
sys.exit(1 if len(l) > 0 else 0)"
[mypy]
ignore_missing_imports = False
follow_imports = error
disallow_untyped_calls = True
disallow_untyped_defs = True
check_untyped_defs = True
disallow_subclassing_any = True
warn_incomplete_stub = True
warn_redundant_casts = True
warn_return_any = True
warn_unused_ignores = True
strict_optional = True
strict_boolean = True
[pylint]
reports = no
#output-format = colorized
indent-string = \t
disable =
locally-disabled,
bad-continuation,
multiple-imports,
invalid-name,
trailing-whitespace,
missing-docstring
max-complexity = 15
max-args = 8
max-locals = 20
max-returns = 6
max-branches = 15
max-statements = 60
max-parents = 7
max-attributes = 8
min-public-methods = 1
max-public-methods = 20
max-bool-expr = 5
max-nested-blocks = 6
max-line-length = 80
ignore-long-lines = ^\s*(#\s+type:\s+.*|[A-Z0-9_]+\s+=\s+.*|('.*':\s+)?\[.*\],?|assert\s+.*)$
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