From 468742a2b8433079f09465249c05250569bfd567 Mon Sep 17 00:00:00 2001 From: Ian Lee Date: Tue, 19 Sep 2017 18:38:00 -0700 Subject: [PATCH 01/18] Beginning of work to develop a tox configuration for testing --- tox.ini | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 tox.ini diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..f208f7d9 --- /dev/null +++ b/tox.ini @@ -0,0 +1,6 @@ +[tox] +envlist = py27,py36 + +[testenv:flake8] +deps = flake8 +commands = flake8 From c4215d5bb7318647eaeaad2d4be2978060f4c187 Mon Sep 17 00:00:00 2001 From: Ian Lee Date: Tue, 19 Sep 2017 18:38:34 -0700 Subject: [PATCH 02/18] Added tox dependency to requirements.txt --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 9371b6ea..39ec1ab8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,3 +6,4 @@ requests_cache pytablewriter publicsuffix pyopenssl==17.2.0 +tox From 3a1c323f8a62e6a763db40552ea034d1ecdf9dfc Mon Sep 17 00:00:00 2001 From: Ian Lee Date: Tue, 19 Sep 2017 18:39:34 -0700 Subject: [PATCH 03/18] Added Python 3.6 classifier --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 07c33eb9..6ae6c505 100755 --- a/setup.py +++ b/setup.py @@ -49,6 +49,7 @@ 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', ], # What does your project relate to? From 425c436604caaa726f38f63b9b0a98b8417703cc Mon Sep 17 00:00:00 2001 From: Ian Lee Date: Tue, 19 Sep 2017 18:41:23 -0700 Subject: [PATCH 04/18] Updated tox.ini to include testing py3.4, py3.5 and running flake8 --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index f208f7d9..f259b931 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27,py36 +envlist = py27,py34,py35,py36,flake8 [testenv:flake8] deps = flake8 From a089f85d8d320ab4995ceef902ae0acd9be5c2d5 Mon Sep 17 00:00:00 2001 From: Ian Lee Date: Tue, 19 Sep 2017 19:36:20 -0700 Subject: [PATCH 05/18] Dont fail on missing interpreters --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index f259b931..b6ff31da 100644 --- a/tox.ini +++ b/tox.ini @@ -1,4 +1,5 @@ [tox] +skip_missing_interpreters = true envlist = py27,py34,py35,py36,flake8 [testenv:flake8] From 11b2766f337dd86905a769c83f477c42cf1f39f4 Mon Sep 17 00:00:00 2001 From: Ian Lee Date: Tue, 19 Sep 2017 19:36:58 -0700 Subject: [PATCH 06/18] Use pytest as test runner --- requirements.txt | 1 + tox.ini | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/requirements.txt b/requirements.txt index 39ec1ab8..5de4bc42 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,3 +7,4 @@ pytablewriter publicsuffix pyopenssl==17.2.0 tox +pytest diff --git a/tox.ini b/tox.ini index b6ff31da..e4d88452 100644 --- a/tox.ini +++ b/tox.ini @@ -2,6 +2,10 @@ skip_missing_interpreters = true envlist = py27,py34,py35,py36,flake8 +[testenv] +deps = pytest +commands = pytest + [testenv:flake8] deps = flake8 commands = flake8 From 83181cd67e9c132e62c07a7aba89616e8cabeef3 Mon Sep 17 00:00:00 2001 From: Ian Lee Date: Tue, 19 Sep 2017 19:37:39 -0700 Subject: [PATCH 07/18] Created first set of tests Leverages the *.badssl.com sites as the "known" / "expected" behavior --- tests/test_badssl.py | 59 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 tests/test_badssl.py diff --git a/tests/test_badssl.py b/tests/test_badssl.py new file mode 100644 index 00000000..f1346bf4 --- /dev/null +++ b/tests/test_badssl.py @@ -0,0 +1,59 @@ +import unittest + +from pshtt.models import Domain, Endpoint +from pshtt.pshtt import basic_check, hsts_check + + +def inspect(base_domain): + """ + Mostly copied from pshtt.pshtt.inspect() + """ + domain = Domain(base_domain) + domain.http = Endpoint("http", "root", base_domain) + domain.httpwww = Endpoint("http", "www", base_domain) + domain.https = Endpoint("https", "root", base_domain) + domain.httpswww = Endpoint("https", "www", base_domain) + + return domain + + # Analyze HTTP endpoint responsiveness and behavior. + basic_check(domain.http) + basic_check(domain.httpwww) + basic_check(domain.https) + basic_check(domain.httpswww) + + # Analyze HSTS header, if present, on each HTTPS endpoint. + hsts_check(domain.https) + hsts_check(domain.httpswww) + + return domain + + +class TestCertificate(unittest.TestCase): + def test_https_expired(self): + domain = inspect('expired.badssl.com') + basic_check(domain.https) + + unittest.assertTrue(domain.https.https_expired_cert) + + def test_https_bad_hostname(self): + domain = inspect('wrong.host.badssl.com') + basic_check(domain.https) + + unittest.assertTrue(domain.https.https_bad_hostname) + + def test_https_bad_chain(self): + domain = inspect('untrusted-root.badssl.com') + basic_check(domain.https) + + unittest.assertTrue(domain.https.https_bad_chain) + + def test_https_self_signed_cert(self): + domain = inspect('self-signed.badssl.com') + basic_check(domain.https) + + unittest.assertTrue(domain.https.https_self_signed_cert) + + +if __name__ == '__main__': + unittest.main() From e91bcc9239737516f7561f521754448683b3b73c Mon Sep 17 00:00:00 2001 From: Ian Lee Date: Thu, 21 Sep 2017 09:24:51 -0700 Subject: [PATCH 08/18] Fixed test cases to properly assert state --- tests/test_badssl.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_badssl.py b/tests/test_badssl.py index f1346bf4..61904758 100644 --- a/tests/test_badssl.py +++ b/tests/test_badssl.py @@ -34,25 +34,25 @@ def test_https_expired(self): domain = inspect('expired.badssl.com') basic_check(domain.https) - unittest.assertTrue(domain.https.https_expired_cert) + self.assertTrue(domain.https.https_expired_cert) def test_https_bad_hostname(self): domain = inspect('wrong.host.badssl.com') basic_check(domain.https) - unittest.assertTrue(domain.https.https_bad_hostname) + self.assertTrue(domain.https.https_bad_hostname) def test_https_bad_chain(self): domain = inspect('untrusted-root.badssl.com') basic_check(domain.https) - unittest.assertTrue(domain.https.https_bad_chain) + self.assertTrue(domain.https.https_bad_chain) def test_https_self_signed_cert(self): domain = inspect('self-signed.badssl.com') basic_check(domain.https) - unittest.assertTrue(domain.https.https_self_signed_cert) + self.assertTrue(domain.https.https_self_signed_cert) if __name__ == '__main__': From 25e659fb26b365008570e4b020a048b3ef3b3024 Mon Sep 17 00:00:00 2001 From: Ian Lee Date: Wed, 27 Sep 2017 17:04:42 -0700 Subject: [PATCH 09/18] Enable running pytest tests in travis build --- .travis.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index fdc9692b..226fbb75 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,9 +8,11 @@ python: - '3.6' install: - - pip install flake8 - pip install -r requirements.txt + - pip install flake8 + - pip install coveralls + - pip install pytest script: + - coverage run --source=pshtt pytest - flake8 . - From f887558f109783b50253bd0e0758272a5981cd65 Mon Sep 17 00:00:00 2001 From: Ian Lee Date: Wed, 27 Sep 2017 17:14:24 -0700 Subject: [PATCH 10/18] Converted to use pytest-cov package for test coverage --- .travis.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 226fbb75..1072d8a9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,9 +10,8 @@ python: install: - pip install -r requirements.txt - pip install flake8 - - pip install coveralls - - pip install pytest + - pip install pytest-cov pytest coveralls script: - - coverage run --source=pshtt pytest + - pytest --cov=pshtt - flake8 . From 17fbe24965e13d878059b2aab4b44b03c75c706c Mon Sep 17 00:00:00 2001 From: Ian Lee Date: Wed, 27 Sep 2017 17:16:38 -0700 Subject: [PATCH 11/18] Apparently, need to explicitly install pshtt to get pytest to work right https://travis-ci.org/IanLee1521/pshtt/builds/280668751 --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 1072d8a9..79562a5f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,6 +11,7 @@ install: - pip install -r requirements.txt - pip install flake8 - pip install pytest-cov pytest coveralls + - pip install . script: - pytest --cov=pshtt From 049ea38934efe06abeb47cb0ae68433abba6fdd7 Mon Sep 17 00:00:00 2001 From: Ian Lee Date: Fri, 29 Sep 2017 08:04:06 -0700 Subject: [PATCH 12/18] Updated tox.ini to use pytest-cov --- .travis.yml | 2 +- tox.ini | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 79562a5f..caf8bb2f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,7 @@ install: - pip install -r requirements.txt - pip install flake8 - pip install pytest-cov pytest coveralls - - pip install . + - pip install -e . script: - pytest --cov=pshtt diff --git a/tox.ini b/tox.ini index e4d88452..41cca8ed 100644 --- a/tox.ini +++ b/tox.ini @@ -1,10 +1,14 @@ [tox] -skip_missing_interpreters = true envlist = py27,py34,py35,py36,flake8 +skip_missing_interpreters = true +; usedevelop = true [testenv] -deps = pytest -commands = pytest +deps = + pytest-cov + pytest + coveralls +commands = pytest --cov=pshtt [testenv:flake8] deps = flake8 From d60362dd5dca31fd8b316bdadcb469b508388db6 Mon Sep 17 00:00:00 2001 From: Ian Lee Date: Fri, 29 Sep 2017 20:01:12 -0700 Subject: [PATCH 13/18] Run coveralls command in Travis --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index caf8bb2f..760a2e93 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,3 +16,4 @@ install: script: - pytest --cov=pshtt - flake8 . + - coveralls From 2043167a4c1e76a4655311f8696c29d2cfe3e6db Mon Sep 17 00:00:00 2001 From: Ian Lee Date: Sat, 30 Sep 2017 13:00:06 -0700 Subject: [PATCH 14/18] Added Travis CI build status badge --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 133481a9..80231bd2 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ ## Pushing HTTPS :lock: +[![Build Status](https://travis-ci.org/dhs-ncats/pshtt.svg?branch=master)](https://travis-ci.org/dhs-ncats/pshtt) + `pshtt` (_"pushed"_) is a tool to scan domains for HTTPS best practices. It saves its results to a CSV (or JSON). `pshtt` was developed to _push_ organizations— especially large ones like the US Federal Government :us: — to adopt HTTPS across the enterprise. Federal .gov domains must comply with [M-15-13](https://https.cio.gov), a 2015 memorandum from the White House Office of Management and Budget that requires federal agencies to enforce HTTPS on their public web sites and services by the end of 2016. Much has been done, and [still more yet to do](https://18f.gsa.gov/2017/01/04/tracking-the-us-governments-progress-on-moving-https/). From 654805d49f39988917a0600d3075fcf63c35635b Mon Sep 17 00:00:00 2001 From: Ian Lee Date: Sun, 1 Oct 2017 15:39:11 -0700 Subject: [PATCH 15/18] Added test cases for `is_live()` --- tests/test_pshtt.py | 74 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 tests/test_pshtt.py diff --git a/tests/test_pshtt.py b/tests/test_pshtt.py new file mode 100644 index 00000000..8e6b2d4b --- /dev/null +++ b/tests/test_pshtt.py @@ -0,0 +1,74 @@ +import unittest + +from pshtt.models import Domain, Endpoint +from pshtt.pshtt import is_live + + +class TestLiveliness(unittest.TestCase): + def setUp(self): + base_domain = 'example.com' + self.domain = Domain(base_domain) + + self.domain.http = Endpoint("http", "root", base_domain) + self.domain.httpwww = Endpoint("http", "www", base_domain) + self.domain.https = Endpoint("https", "root", base_domain) + self.domain.httpswww = Endpoint("https", "www", base_domain) + + def test_none(self): + self.assertFalse(is_live(self.domain)) + + def test_http_only(self): + self.domain.http.live = True + + self.assertTrue(is_live(self.domain)) + + def test_https_only(self): + self.domain.https.live = True + + self.assertTrue(is_live(self.domain)) + + def test_httpwww_only(self): + self.domain.httpwww.live = True + + self.assertTrue(is_live(self.domain)) + + def test_httpswww_only(self): + self.domain.httpswww.live = True + + self.assertTrue(is_live(self.domain)) + + def test_http_both(self): + self.domain.http.live = True + self.domain.httpwww.live = True + + self.assertTrue(is_live(self.domain)) + + def test_https_both(self): + self.domain.https.live = True + self.domain.httpswww.live = True + + self.assertTrue(is_live(self.domain)) + + def test_www_neither(self): + self.domain.http.live = True + self.domain.https.live = True + + self.assertTrue(is_live(self.domain)) + + def test_www_both(self): + self.domain.httpwww.live = True + self.domain.httpswww.live = True + + self.assertTrue(is_live(self.domain)) + + def test_all(self): + self.domain.http.live = True + self.domain.https.live = True + self.domain.httpwww.live = True + self.domain.httpswww.live = True + + self.assertTrue(is_live(self.domain)) + + +if __name__ == '__main__': + unittest.main() From 0bdcd52a2b15fc63b94cba9de0d5be5489393157 Mon Sep 17 00:00:00 2001 From: Ian Lee Date: Mon, 2 Oct 2017 10:29:20 -0700 Subject: [PATCH 16/18] Skip the live tests against badssl for now These require more work with `vcrpy` or similar mocking before enabling --- tests/test_badssl.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_badssl.py b/tests/test_badssl.py index 61904758..7ee00cd1 100644 --- a/tests/test_badssl.py +++ b/tests/test_badssl.py @@ -29,6 +29,7 @@ def inspect(base_domain): return domain +@unittest.skip('Disable live tests against badssl for now') class TestCertificate(unittest.TestCase): def test_https_expired(self): domain = inspect('expired.badssl.com') From 2108ff07249ba7a3e5300d32fa6b117a21affaac Mon Sep 17 00:00:00 2001 From: Ian Lee Date: Sat, 7 Oct 2017 08:25:06 -0700 Subject: [PATCH 17/18] Added a new set of tests for definitions This is meant to encompass checks for things like "Supports HTTPS" and "Enforces HTTPS" which have more complicated logic associated with them. --- tests/test_definitions.py | 89 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 tests/test_definitions.py diff --git a/tests/test_definitions.py b/tests/test_definitions.py new file mode 100644 index 00000000..649b23c0 --- /dev/null +++ b/tests/test_definitions.py @@ -0,0 +1,89 @@ +import unittest + +from pshtt.models import Domain, Endpoint +from pshtt import pshtt as api + + +class TestUsesHTTPS(unittest.TestCase): + def setUp(self): + base_domain = 'example.com' + self.domain = Domain(base_domain) + + self.domain.http = Endpoint("http", "root", base_domain) + self.domain.httpwww = Endpoint("http", "www", base_domain) + self.domain.https = Endpoint("https", "root", base_domain) + self.domain.httpswww = Endpoint("https", "www", base_domain) + + @unittest.skip('Still working on definition') + def test_definition(self): + self.domain.https.live = True + self.domain.https.https_valid = True + self.domain.https.https_valid = True + + self.assertTrue(api.is_domain_supports_https(self.domain)) + + +class TestBadChain(unittest.TestCase): + def setUp(self): + base_domain = 'example.com' + self.domain = Domain(base_domain) + + self.domain.http = Endpoint("http", "root", base_domain) + self.domain.httpwww = Endpoint("http", "www", base_domain) + self.domain.https = Endpoint("https", "root", base_domain) + self.domain.httpswww = Endpoint("https", "www", base_domain) + + def test_bad_chain_root(self): + self.domain.https.https_bad_chain = True + self.domain.canonical = self.domain.https + + self.assertTrue(api.is_bad_chain(self.domain)) + + def test_bad_chain_www(self): + self.domain.httpswww.https_bad_chain = True + self.domain.canonical = self.domain.httpswww + + self.assertTrue(api.is_bad_chain(self.domain)) + + def test_bad_chain_both(self): + self.domain.https.https_bad_chain = True + self.domain.httpswww.https_bad_chain = True + + self.domain.canonical = self.domain.https + self.assertTrue(api.is_bad_chain(self.domain)) + + self.domain.canonical = self.domain.httpswww + self.assertTrue(api.is_bad_chain(self.domain)) + + +class TestBadHostname(unittest.TestCase): + def setUp(self): + base_domain = 'example.com' + self.domain = Domain(base_domain) + + self.domain.http = Endpoint("http", "root", base_domain) + self.domain.httpwww = Endpoint("http", "www", base_domain) + self.domain.https = Endpoint("https", "root", base_domain) + self.domain.httpswww = Endpoint("https", "www", base_domain) + + def test_bad_hostname_root(self): + self.domain.https.https_bad_hostname = True + self.domain.canonical = self.domain.https + + self.assertTrue(api.is_bad_hostname(self.domain)) + + def test_bad_hostname_www(self): + self.domain.httpswww.https_bad_hostname = True + self.domain.canonical = self.domain.httpswww + + self.assertTrue(api.is_bad_hostname(self.domain)) + + def test_bad_hostname_both(self): + self.domain.https.https_bad_hostname = True + self.domain.httpswww.https_bad_hostname = True + + self.domain.canonical = self.domain.https + self.assertTrue(api.is_bad_hostname(self.domain)) + + self.domain.canonical = self.domain.httpswww + self.assertTrue(api.is_bad_hostname(self.domain)) From c098879c3a3c08c22f8dedd1b0f9287a06cd75fa Mon Sep 17 00:00:00 2001 From: Ian Lee Date: Thu, 19 Oct 2017 13:22:22 -0700 Subject: [PATCH 18/18] Only run coveralls on testing success --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index ac97211b..8ca7a88e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,4 +17,6 @@ install: script: - pytest --cov=pshtt - flake8 . + +after_success: - coveralls