From 6e01130d8e39c8315da53f7e5ab89a201ccaa027 Mon Sep 17 00:00:00 2001 From: Vespian Date: Mon, 10 Mar 2014 13:38:29 +0100 Subject: [PATCH 01/44] Fix README.md Add missing dependencies to the list, remove old reference to USAGE.md. Change-Id: Ic42048e08f66cd85f8954feaf41e1dfe07e99f86 --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9ebaf8c..24047e2 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,10 @@ In order to run certchecker you need to following dependencies installed: - Google's protobuf library - yaml bindings for python (http://pyyaml.org/) - Dulwich - python implementation of GIT (https://www.samba.org/~jelmer/dulwich/docs/) -- ssh command in your PATH +- *ssh* command in your PATH - argparse library +- dnspython library (http://www.dnspython.org/) +- pyOpenSSL (https://launchpad.net/pyopenssl/) You can also use debian packaging rules from debian/ directory to build a deb package. @@ -123,9 +125,8 @@ certcheck:DATA_TTL == 25 hours. ### Maintenance In order to not to let the "$repo_tmpdir/repository" repository grow endlessly -a 'git gc' command should be executed once a day by i.e. a cronjob. It should +a 'git gc' command should be executed once a day by i.e. a cronjob. It will repack all the packs and remove dangling objects. -Please see the doc/USAGE.md file for details. ## Contributing From d3fbf69109a73f3a4dd3ed10888e269f8c3bab0e Mon Sep 17 00:00:00 2001 From: Vespian Date: Fri, 16 May 2014 16:03:51 +0200 Subject: [PATCH 02/44] Rename certcheck to check_cert Change-Id: Idff8d4924171072c127c054bb4f8d1661fb3d700 --- .coveragerc | 2 +- .gitignore | 2 +- README.md | 28 ++-- bin/{certcheck => check_cert} | 6 +- {certcheck => check_cert}/__init__.py | 5 +- debian/.gitignore | 2 +- debian/changelog | 6 +- debian/control | 4 +- setup.py | 8 +- test/fabric/{certcheck.yml => check_cert.yml} | 2 +- ...mopconfig.yml => check_cert_mopconfig.yml} | 10 +- test/fabric/malformed.yml | 2 +- test/modules/file_paths.py | 4 +- .../{certcheck => check_cert}/__init__.py | 0 .../test_check_cert.py} | 134 +++++++++--------- 15 files changed, 108 insertions(+), 107 deletions(-) rename bin/{certcheck => check_cert} (86%) rename {certcheck => check_cert}/__init__.py (99%) rename test/fabric/{certcheck.yml => check_cert.yml} (94%) rename test/fabric/{certcheck_mopconfig.yml => check_cert_mopconfig.yml} (79%) rename test/moduletests/{certcheck => check_cert}/__init__.py (100%) rename test/moduletests/{certcheck/test_certcheck.py => check_cert/test_check_cert.py} (81%) diff --git a/.coveragerc b/.coveragerc index 5e7d51a..e137868 100644 --- a/.coveragerc +++ b/.coveragerc @@ -17,7 +17,7 @@ exclude_lines = if 0: if __name__ == .__main__.: source = - ./certcheck/ + ./check_cert/ [html] diff --git a/.gitignore b/.gitignore index 9022dbd..c13b8b0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ *.pyc /.coverage /build* -certcheck.egg-info/* +check_cert.egg-info/* debian/files diff --git a/README.md b/README.md index 24047e2..d739a2a 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ -# _Certchecker_ +# _check_cert_ -_Certchecker is a certificate expiration check capable of scanning GIT repos +_check_cert is a certificate expiration check capable of scanning GIT repos and sending data on expiring/expired certificates back to the monitoring system (currently only Riemann)._ ## Project Setup -In order to run certchecker you need to following dependencies installed: +In order to run check_certer you need to following dependencies installed: - Bernhard - Riemann client library (https://github.com/banjiewen/bernhard) - Google's protobuf library - yaml bindings for python (http://pyyaml.org/) @@ -27,7 +27,7 @@ Actions taken by the script are determined by its command line and the configuration file. The command line has a build-in help system: ``` -usage: certcheck [-h] [--version] -c CONFIG_FILE [-v] [-s] [-d] +usage: check_cert [-h] [--version] -c CONFIG_FILE [-v] [-s] [-d] Simple certificate expiration check @@ -47,7 +47,7 @@ The configuration file is a plain YAML document. It's syntax is as follows: ``` --- -lockfile: /tmp/certcheck.lock +lockfile: /tmp/check_cert.lock warn_treshold: 30 critical_treshold: 15 riemann_hosts: @@ -59,14 +59,14 @@ riemann_hosts: - _riemann._udp riemann_tags: - production - - class::certcheck + - class::check_cert repo_host: git.example.net repo_port: 22 repo_url: /example-repo repo_masterbranch: refs/heads/production -repo_localdir: /tmp/certcheck-temprepo -repo_user: certcheck -repo_pubkey: ./certcheck_id_rsa +repo_localdir: /tmp/check_cert-temprepo +repo_user: check_cert +repo_pubkey: ./check_cert_id_rsa # format - dict, hash as a key, and value as a comment # sha1sum ./certificate_to_be_ignored ignored_certs: @@ -86,9 +86,9 @@ The connection is established using the $repo_pubkey pubkey, and the $repo_user itself should have very limited privileges. Next, the repository is scanned in search of files ending with one of the -certcheck:CERTIFICATE_EXTENSIONS extensions. Currently all possible +check_cert:CERTIFICATE_EXTENSIONS extensions. Currently all possible certificate extensions are listed but only ['pem', 'crt', 'cer'] are currently -supported (see certcheck:get_cert_expiration method). For the remaing ones +supported (see check_cert:get_cert_expiration method). For the remaing ones only a warning is issued. For each certificate found a sha1sum is computed, and if the result is found in @@ -120,7 +120,7 @@ IP addresses/ports of the Riemann instances can be defined in two ways: the SRV entry itself. The final metric is send to *all* Riemann instances with TTL equal to -certcheck:DATA_TTL == 25 hours. +check_cert:DATA_TTL == 25 hours. ### Maintenance @@ -145,7 +145,7 @@ test/ directory you can find: Unittests can be started either by using *nosetest* command: ``` -certcheck/ (master✗) # nosetests +check_cert/ (master✗) # nosetests [20:33:02] ...... ---------------------------------------------------------------------- @@ -157,7 +157,7 @@ OK or by issuing the *run_tests.py* command: ``` -certcheck/ (master✗) # run_tests.py +check_cert/ (master✗) # run_tests.py [20:33:04] Created test certificate expired_3_days.pem Created test certificate expire_6_days.pem diff --git a/bin/certcheck b/bin/check_cert similarity index 86% rename from bin/certcheck rename to bin/check_cert index 707f8c7..7d6af20 100755 --- a/bin/certcheck +++ b/bin/check_cert @@ -14,10 +14,10 @@ # License for the specific language governing permissions and limitations under # the License. -import certcheck +import check_cert if __name__ == '__main__': - args_dict = certcheck.parse_command_line() + args_dict = check_cert.parse_command_line() - certcheck.main(**args_dict) + check_cert.main(**args_dict) diff --git a/certcheck/__init__.py b/check_cert/__init__.py similarity index 99% rename from certcheck/__init__.py rename to check_cert/__init__.py index 5e51f47..de1cabd 100755 --- a/certcheck/__init__.py +++ b/check_cert/__init__.py @@ -1,5 +1,6 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +# Copyright (c) 2014 Pawel Rozlach # Copyright (c) 2013 Spotify AB # # Licensed under the Apache License, Version 2.0 (the "License"); you may not @@ -47,7 +48,7 @@ LOCKFILE_LOCATION = './'+os.path.basename(__file__)+'.lock' CONFIGFILE_LOCATION = './'+os.path.basename(__file__)+'.conf' DATA_TTL = 25*60*60 # Data gathered by the script run is valid for 25 hours. -SERVICE_NAME = 'certcheck' +SERVICE_NAME = 'check_cert' CERTIFICATE_EXTENSIONS = ['der', 'crt', 'pem', 'cer', 'p12', 'pfx', ] @@ -602,7 +603,7 @@ def main(config_file, std_err=False, verbose=True, dont_send=False): handler.setFormatter(fmt) logger.addHandler(handler) - logger.info("Certcheck is starting, command line arguments:" + + logger.info("check_cert is starting, command line arguments:" + "config_file={0}, ".format(config_file) + "std_err={0}, ".format(std_err) + "verbose={0}, ".format(verbose) diff --git a/debian/.gitignore b/debian/.gitignore index 6f373e7..14cd0c8 100644 --- a/debian/.gitignore +++ b/debian/.gitignore @@ -1,2 +1,2 @@ -certcheck* +check_cert* diff --git a/debian/changelog b/debian/changelog index 1298e41..df10399 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,4 +1,4 @@ -certcheck (0.3.0) stable; urgency=low +check_cert (0.3.0) stable; urgency=low * Documentation refactoring * Make unittests nosetest compatible @@ -6,14 +6,14 @@ certcheck (0.3.0) stable; urgency=low -- Vespian Mon, 18 Sep 2013 14:33:43 +0000 -certcheck (0.2.0) stable; urgency=low +check_cert (0.2.0) stable; urgency=low * Git integration. * Drop scanning directories in favour of direct git interfacing. -- Pawel Rozlach Mon, 18 Sep 2013 14:33:43 +0000 -certcheck (0.1.0) unstable; urgency=low +check_cert (0.1.0) unstable; urgency=low * Initial release. diff --git a/debian/control b/debian/control index be17909..a09b77e 100644 --- a/debian/control +++ b/debian/control @@ -1,4 +1,4 @@ -Source: certcheck +Source: check_cert Section: utils Priority: extra Maintainer: Vespian @@ -9,7 +9,7 @@ Build-Depends: python (>= 2.6.6-3~), debhelper (>= 8), python-dnspython, Standards-Version: 3.9.3 X-Python-Version: >= 2.6 -Package: certcheck +Package: check_cert Version: 0.2.0 Architecture: any Depends: ${python:Depends}, python-openssl, python-bernhard, python-argparse, diff --git a/setup.py b/setup.py index 98583a8..2d21979 100755 --- a/setup.py +++ b/setup.py @@ -17,12 +17,12 @@ from setuptools import setup -setup(name='certcheck', +setup(name='check_cert', version='0.3.0', author='Vespian', author_email='vespian a t wp.pl', license='ASF2.0', - url='https://github.com/vespian/certcheck', + url='https://github.com/vespian/check_cert', description='Certificate checking tool', - packages=['certcheck'], - scripts=['bin/certcheck']) + packages=['check_cert'], + scripts=['bin/check_cert']) diff --git a/test/fabric/certcheck.yml b/test/fabric/check_cert.yml similarity index 94% rename from test/fabric/certcheck.yml rename to test/fabric/check_cert.yml index c455908..5db0752 100644 --- a/test/fabric/certcheck.yml +++ b/test/fabric/check_cert.yml @@ -1,5 +1,5 @@ --- -lockfile: ./certcheck.lock +lockfile: ./check_cert.lock warn_treshold: 30 critical_treshold: 15 riemann_hosts: diff --git a/test/fabric/certcheck_mopconfig.yml b/test/fabric/check_cert_mopconfig.yml similarity index 79% rename from test/fabric/certcheck_mopconfig.yml rename to test/fabric/check_cert_mopconfig.yml index 321cd5f..2ae965a 100644 --- a/test/fabric/certcheck_mopconfig.yml +++ b/test/fabric/check_cert_mopconfig.yml @@ -1,5 +1,5 @@ --- -lockfile: /tmp/certcheck.lock +lockfile: /tmp/check_cert.lock warn_treshold: 30 critical_treshold: 15 riemann_hosts: @@ -11,14 +11,14 @@ riemann_hosts: - _riemann._udp riemann_tags: - production - - class::certcheck + - class::check_cert repo_host: git.example.com repo_port: 22 repo_url: /sample-repo repo_masterbranch: refs/heads/production -repo_localdir: /tmp/certcheck-temprepo -repo_user: certcheck -repo_pubkey: /home/vespian/work/tmp_tickets/cert_check/certcheck_id_rsa +repo_localdir: /tmp/check_cert-temprepo +repo_user: check_cert +repo_pubkey: /home/vespian/work/tmp_tickets/cert_check/check_cert_id_rsa # sha1sum ./certificate_to_be_ignored # format - dict, hash as a key, and value as a comment ignored_certs: diff --git a/test/fabric/malformed.yml b/test/fabric/malformed.yml index 62dd35b..c077e0d 100644 --- a/test/fabric/malformed.yml +++ b/test/fabric/malformed.yml @@ -1,5 +1,5 @@ { - "lockfile": "./certcheck.lock", + "lockfile": "./check_cert.lock", "warn_treshold": 30, "critical_treshold": 15, "riemann_hosts": [ diff --git a/test/modules/file_paths.py b/test/modules/file_paths.py index 719b316..61171a9 100644 --- a/test/modules/file_paths.py +++ b/test/modules/file_paths.py @@ -37,10 +37,10 @@ EXPIRE_41_DAYS_DER, BROKEN_CERT, IGNORED_CERT]) #Configfile location -TEST_CONFIG_FILE = op.join(_fabric_base_dir, 'certcheck.yml') +TEST_CONFIG_FILE = op.join(_fabric_base_dir, 'check_cert.yml') TEST_MALFORMED_CONFIG_FILE = op.join(_fabric_base_dir, 'malformed.yml') TEST_NONEXISTANT_CONFIG_FILE = op.join(_fabric_base_dir, - 'certcheck.yml.nonexistant') + 'check_cert.yml.nonexistant') #Test lockfile location: TEST_LOCKFILE = op.join(_fabric_base_dir, 'filelock.pid') diff --git a/test/moduletests/certcheck/__init__.py b/test/moduletests/check_cert/__init__.py similarity index 100% rename from test/moduletests/certcheck/__init__.py rename to test/moduletests/check_cert/__init__.py diff --git a/test/moduletests/certcheck/test_certcheck.py b/test/moduletests/check_cert/test_check_cert.py similarity index 81% rename from test/moduletests/certcheck/test_certcheck.py rename to test/moduletests/check_cert/test_check_cert.py index b6072c8..598917a 100644 --- a/test/moduletests/certcheck/test_certcheck.py +++ b/test/moduletests/check_cert/test_check_cert.py @@ -43,7 +43,7 @@ #Local imports: import file_paths as paths -import certcheck +import check_cert class TestCertCheck(unittest.TestCase): @@ -94,35 +94,35 @@ def setUpClass(cls): @mock.patch('sys.exit') def test_config_file_parsing(self, SysExitMock, LoggingErrorMock): #Test malformed file loading - certcheck.ScriptConfiguration.load_config(paths.TEST_MALFORMED_CONFIG_FILE) + check_cert.ScriptConfiguration.load_config(paths.TEST_MALFORMED_CONFIG_FILE) self.assertTrue(LoggingErrorMock.called) SysExitMock.assert_called_once_with(1) SysExitMock.reset_mock() #Test non-existent file loading - certcheck.ScriptConfiguration.load_config(paths.TEST_NONEXISTANT_CONFIG_FILE) + check_cert.ScriptConfiguration.load_config(paths.TEST_NONEXISTANT_CONFIG_FILE) self.assertTrue(LoggingErrorMock.called) SysExitMock.assert_called_once_with(1) #Load the config file - certcheck.ScriptConfiguration.load_config(paths.TEST_CONFIG_FILE) + check_cert.ScriptConfiguration.load_config(paths.TEST_CONFIG_FILE) #String: - self.assertEqual(certcheck.ScriptConfiguration.get_val("repo_host"), + self.assertEqual(check_cert.ScriptConfiguration.get_val("repo_host"), "git.foo.net") #List of strings - self.assertEqual(certcheck.ScriptConfiguration.get_val("riemann_tags"), + self.assertEqual(check_cert.ScriptConfiguration.get_val("riemann_tags"), ['abc', 'def']) #Integer: - self.assertEqual(certcheck.ScriptConfiguration.get_val("warn_treshold"), 30) + self.assertEqual(check_cert.ScriptConfiguration.get_val("warn_treshold"), 30) #Key not in config file: with self.assertRaises(KeyError): - certcheck.ScriptConfiguration.get_val("not_a_field") + check_cert.ScriptConfiguration.get_val("not_a_field") - @mock.patch.object(certcheck.ScriptStatus, 'notify_immediate') # same as below + @mock.patch.object(check_cert.ScriptStatus, 'notify_immediate') # same as below @mock.patch('logging.warn') # Unused, but masks error messages - @mock.patch.object(certcheck.ScriptStatus, 'update') + @mock.patch.object(check_cert.ScriptStatus, 'update') def test_cert_expiration_parsing(self, UpdateMock, *unused): IGNORED_CERTS = ['42b270cbd03eaa8c16c386e66f910195f769f8b1'] @@ -133,44 +133,44 @@ def test_cert_expiration_parsing(self, UpdateMock, *unused): #Test an expired certificate: cert = self._certpath2namedtuple(paths.EXPIRED_3_DAYS) - expiry_time = certcheck.get_cert_expiration( + expiry_time = check_cert.get_cert_expiration( cert, IGNORED_CERTS) - now self.assertEqual(expiry_time.days, -3) #Test an ignored certificate: cert = self._certpath2namedtuple(paths.IGNORED_CERT) - expiry_time = certcheck.get_cert_expiration(cert, + expiry_time = check_cert.get_cert_expiration(cert, IGNORED_CERTS) self.assertEqual(expiry_time, None) #Test a good certificate: cert = self._certpath2namedtuple(paths.EXPIRE_21_DAYS) - expiry_time = certcheck.get_cert_expiration(cert, + expiry_time = check_cert.get_cert_expiration(cert, IGNORED_CERTS) - now self.assertEqual(expiry_time.days, 21) #Test a DER certificate: cert = self._certpath2namedtuple(paths.EXPIRE_41_DAYS_DER) - certcheck.get_cert_expiration(cert, IGNORED_CERTS) + check_cert.get_cert_expiration(cert, IGNORED_CERTS) self.assertTrue(UpdateMock.called) self.assertEqual(UpdateMock.call_args_list[0][0][0], 'unknown') #Test a broken certificate: cert = self._certpath2namedtuple(paths.BROKEN_CERT) - certcheck.get_cert_expiration(cert, IGNORED_CERTS) + check_cert.get_cert_expiration(cert, IGNORED_CERTS) self.assertTrue(UpdateMock.called) self.assertEqual(UpdateMock.call_args_list[0][0][0], 'unknown') @mock.patch('logging.warn') def test_file_locking(self, LoggingWarnMock, *unused): - certcheck.ScriptLock.init(paths.TEST_LOCKFILE) + check_cert.ScriptLock.init(paths.TEST_LOCKFILE) - with self.assertRaises(certcheck.RecoverableException): - certcheck.ScriptLock.release() + with self.assertRaises(check_cert.RecoverableException): + check_cert.ScriptLock.release() - certcheck.ScriptLock.aqquire() + check_cert.ScriptLock.aqquire() - certcheck.ScriptLock.aqquire() + check_cert.ScriptLock.aqquire() self.assertTrue(LoggingWarnMock.called) self.assertTrue(os.path.exists(paths.TEST_LOCKFILE)) @@ -183,12 +183,12 @@ def test_file_locking(self, LoggingWarnMock, *unused): pid = int(pid_str) self.assertEqual(pid, os.getpid()) - certcheck.ScriptLock.release() + check_cert.ScriptLock.release() child = os.fork() if not child: #we are in the child process: - certcheck.ScriptLock.aqquire() + check_cert.ScriptLock.aqquire() time.sleep(10) #script should not do any cleanup - it is part of the tests :) else: @@ -206,27 +206,27 @@ def test_file_locking(self, LoggingWarnMock, *unused): os.kill(child, 9) assert False - with self.assertRaises(certcheck.RecoverableException): - certcheck.ScriptLock.aqquire() + with self.assertRaises(check_cert.RecoverableException): + check_cert.ScriptLock.aqquire() os.kill(child, 11) #now it should succed - certcheck.ScriptLock.aqquire() + check_cert.ScriptLock.aqquire() @mock.patch('logging.warn') # Unused, but masks error messages @mock.patch('logging.info') @mock.patch('logging.error') - @mock.patch('certcheck.bernhard') + @mock.patch('check_cert.bernhard') def test_script_status(self, RiemannMock, LoggingErrorMock, LoggingInfoMock, *unused): #There should be at least one tag defined: - certcheck.ScriptStatus.initialize(riemann_hosts_config={}, riemann_tags=[]) + check_cert.ScriptStatus.initialize(riemann_hosts_config={}, riemann_tags=[]) self.assertTrue(LoggingErrorMock.called) LoggingErrorMock.reset_mock() #There should be at least one Riemann host defined: - certcheck.ScriptStatus.initialize(riemann_hosts_config={}, + check_cert.ScriptStatus.initialize(riemann_hosts_config={}, riemann_tags=['tag1', 'tag2']) self.assertTrue(LoggingErrorMock.called) LoggingErrorMock.reset_mock() @@ -239,7 +239,7 @@ def side_effect(host, port): RiemannMock.TCPTransport = 'TCPTransport' RiemannMock.Client.side_effect = side_effect - certcheck.ScriptStatus.initialize(riemann_hosts_config={ + check_cert.ScriptStatus.initialize(riemann_hosts_config={ 'static': ['192.168.122.16:5555:udp']}, riemann_tags=['tag1', 'tag2']) self.assertTrue(LoggingErrorMock.called) @@ -249,16 +249,16 @@ def side_effect(host, port): RiemannMock.Client.reset_mock() #Mock should only allow legitimate exit_statuses - certcheck.ScriptStatus.notify_immediate("not a real status", "message") + check_cert.ScriptStatus.notify_immediate("not a real status", "message") self.assertTrue(LoggingErrorMock.called) LoggingErrorMock.reset_mock() - certcheck.ScriptStatus.update("not a real status", "message") + check_cert.ScriptStatus.update("not a real status", "message") self.assertTrue(LoggingErrorMock.called) LoggingErrorMock.reset_mock() #Done with syntax checking, now initialize the class properly: - certcheck.ScriptStatus.initialize(riemann_hosts_config={ + check_cert.ScriptStatus.initialize(riemann_hosts_config={ 'static': ['1.2.3.4:1:udp', '2.3.4.5:5555:tcp',] }, @@ -270,12 +270,12 @@ def side_effect(host, port): RiemannMock.Client.reset_mock() #Check if notify_immediate works - certcheck.ScriptStatus.notify_immediate("warn", "a warning message") + check_cert.ScriptStatus.notify_immediate("warn", "a warning message") self.assertTrue(LoggingInfoMock.called) LoggingErrorMock.reset_mock() proper_call = mock.call().send({'description': 'a warning message', - 'service': 'certcheck', + 'service': 'check_cert', 'tags': ['tag1', 'tag2'], 'state': 'warn', 'host': platform.uname()[1], @@ -288,16 +288,16 @@ def side_effect(host, port): RiemannMock.Client.reset_mock() #update method shoul escalate only up: - certcheck.ScriptStatus.update('warn', "this is a warning message.") - certcheck.ScriptStatus.update('ok', '') - certcheck.ScriptStatus.update('unknown', "this is a not-rated message.") - certcheck.ScriptStatus.update('ok', "this is an informational message.") + check_cert.ScriptStatus.update('warn', "this is a warning message.") + check_cert.ScriptStatus.update('ok', '') + check_cert.ScriptStatus.update('unknown', "this is a not-rated message.") + check_cert.ScriptStatus.update('ok', "this is an informational message.") proper_call = mock.call().send({'description': 'this is a warning message.\n' + 'this is a not-rated message.\n' + 'this is an informational message.', - 'service': 'certcheck', + 'service': 'check_cert', 'tags': ['tag1', 'tag2'], 'state': 'unknown', 'host': platform.uname()[1], @@ -305,7 +305,7 @@ def side_effect(host, port): ) # This call should be issued to *both* connection mocks, but we # simplify things here a bit: - certcheck.ScriptStatus.notify_agregated() + check_cert.ScriptStatus.notify_agregated() self.assertEqual(2, len([x for x in RiemannMock.Client.mock_calls if x == proper_call])) RiemannMock.reset_mock() @@ -315,39 +315,39 @@ def test_command_line_parsing(self, SysExitMock): old_args = sys.argv #General parsing: - sys.argv = ['./certcheck', '-v', '-s', '-d', '-c', './certcheck.json'] - parsed_cmdline = certcheck.parse_command_line() + sys.argv = ['./check_cert', '-v', '-s', '-d', '-c', './check_cert.json'] + parsed_cmdline = check_cert.parse_command_line() self.assertEqual(parsed_cmdline, {'std_err': True, - 'config_file': './certcheck.json', + 'config_file': './check_cert.json', 'verbose': True, 'dont_send': True, }) #Config file should be a mandatory argument: - sys.argv = ['./certcheck', ] + sys.argv = ['./check_cert', ] # Suppres warnings from argparse with mock.patch('sys.stderr'): - parsed_cmdline = certcheck.parse_command_line() + parsed_cmdline = check_cert.parse_command_line() SysExitMock.assert_called_once_with(2) #Test default values: - sys.argv = ['./certcheck', '-c', './certcheck.json'] - parsed_cmdline = certcheck.parse_command_line() + sys.argv = ['./check_cert', '-c', './check_cert.json'] + parsed_cmdline = check_cert.parse_command_line() self.assertEqual(parsed_cmdline, {'std_err': False, - 'config_file': './certcheck.json', + 'config_file': './check_cert.json', 'verbose': False, 'dont_send': False, }) sys.argv = old_args - @mock.patch('certcheck.sys.exit') - @mock.patch('certcheck.get_cert_expiration') - @mock.patch('certcheck.CertStore') - @mock.patch('certcheck.ScriptLock', autospec=True) - @mock.patch('certcheck.ScriptStatus', autospec=True) - @mock.patch('certcheck.ScriptConfiguration', autospec=True) - @mock.patch('certcheck.logging', autospec=True) + @mock.patch('check_cert.sys.exit') + @mock.patch('check_cert.get_cert_expiration') + @mock.patch('check_cert.CertStore') + @mock.patch('check_cert.ScriptLock', autospec=True) + @mock.patch('check_cert.ScriptStatus', autospec=True) + @mock.patch('check_cert.ScriptConfiguration', autospec=True) + @mock.patch('check_cert.logging', autospec=True) def test_script_logic(self, LoggingMock, ScriptConfigurationMock, ScriptStatusMock, ScriptLockMock, CertStoreMock, CertExpirationMock, SysExitMock): @@ -399,11 +399,11 @@ def fake_cert(cert_extensions): # Test if ScriptStatus gets properly initialized # and whether warn > crit condition is # checked as well - certcheck.ScriptConfiguration.get_val.side_effect = \ + check_cert.ScriptConfiguration.get_val.side_effect = \ script_conf_factory(warn_treshold=7) with self.assertRaises(SystemExit) as e: - certcheck.main(config_file='./certcheck.conf') + check_cert.main(config_file='./check_cert.conf') self.assertEqual(e.exception.code, 1) proper_init_call = dict(riemann_hosts_config= { @@ -414,30 +414,30 @@ def fake_cert(cert_extensions): debug=False) self.assertTrue(ScriptConfigurationMock.load_config.called) self.assertTrue(ScriptStatusMock.notify_immediate.called) - certcheck.ScriptStatus.initialize.assert_called_once_with(**proper_init_call) + check_cert.ScriptStatus.initialize.assert_called_once_with(**proper_init_call) #this time test only the negative warn threshold: - certcheck.ScriptConfiguration.get_val.side_effect = \ + check_cert.ScriptConfiguration.get_val.side_effect = \ script_conf_factory(warn_treshold=-30) ScriptStatusMock.notify_immediate.reset_mock() with self.assertRaises(SystemExit) as e: - certcheck.main(config_file='./certcheck.conf') + check_cert.main(config_file='./check_cert.conf') self.assertTrue(ScriptStatusMock.notify_immediate.called) self.assertEqual(e.exception.code, 1) #this time test only the crit threshold == 0 condition check: - certcheck.ScriptConfiguration.get_val.side_effect = \ + check_cert.ScriptConfiguration.get_val.side_effect = \ script_conf_factory(critical_treshold=-1) ScriptStatusMock.notify_immediate.reset_mock() with self.assertRaises(SystemExit) as e: - certcheck.main(config_file='./certcheck.conf') + check_cert.main(config_file='./check_cert.conf') self.assertTrue(ScriptStatusMock.notify_immediate.called) self.assertEqual(e.exception.code, 1) #test if an expired cert is properly handled: ScriptStatusMock.notify_immediate.reset_mock() - certcheck.ScriptConfiguration.get_val.side_effect = \ + check_cert.ScriptConfiguration.get_val.side_effect = \ script_conf_factory() def fake_cert_expiration(cert, ignored_certs): @@ -445,7 +445,7 @@ def fake_cert_expiration(cert, ignored_certs): return datetime.utcnow() - timedelta(days=4) CertExpirationMock.side_effect = fake_cert_expiration with self.assertRaises(SystemExit) as e: - certcheck.main(config_file='./certcheck.conf') + check_cert.main(config_file='./check_cert.conf') self.assertEqual(e.exception.code, 0) self.assertTrue(ScriptStatusMock.update.called) self.assertEqual(ScriptStatusMock.update.call_args[0][0], 'critical') @@ -464,7 +464,7 @@ def fake_cert_expiration(cert, ignored_certs): return datetime.utcnow() + timedelta(days=7) CertExpirationMock.side_effect = fake_cert_expiration with self.assertRaises(SystemExit) as e: - certcheck.main(config_file='./certcheck.conf') + check_cert.main(config_file='./check_cert.conf') self.assertEqual(e.exception.code, 0) self.assertTrue(ScriptLockMock.aqquire.called) self.assertTrue(ScriptLockMock.release.called) @@ -482,7 +482,7 @@ def fake_cert_expiration(cert, ignored_certs): return datetime.utcnow() + timedelta(days=21) CertExpirationMock.side_effect = fake_cert_expiration with self.assertRaises(SystemExit) as e: - certcheck.main(config_file='./certcheck.conf') + check_cert.main(config_file='./check_cert.conf') self.assertEqual(e.exception.code, 0) self.assertTrue(ScriptLockMock.aqquire.called) self.assertTrue(ScriptLockMock.release.called) @@ -500,7 +500,7 @@ def fake_cert_expiration(cert, ignored_certs): return datetime.utcnow() + timedelta(days=40) CertExpirationMock.side_effect = fake_cert_expiration with self.assertRaises(SystemExit) as e: - certcheck.main(config_file='./certcheck.conf') + check_cert.main(config_file='./check_cert.conf') self.assertEqual(e.exception.code, 0) self.assertTrue(ScriptLockMock.aqquire.called) self.assertTrue(ScriptLockMock.release.called) From 3b31282601ce0edaed43e7025a962f3d1c3724b2 Mon Sep 17 00:00:00 2001 From: Vespian Date: Fri, 16 May 2014 21:36:25 +0200 Subject: [PATCH 03/44] Remove code that is already in pymisc Change-Id: I168d2c10c2e19efb14109e16ce7b299e5e48d81b --- check_cert/__init__.py | 320 +---------------------------------------- 1 file changed, 6 insertions(+), 314 deletions(-) diff --git a/check_cert/__init__.py b/check_cert/__init__.py index de1cabd..b78656f 100755 --- a/check_cert/__init__.py +++ b/check_cert/__init__.py @@ -30,6 +30,8 @@ from dulwich.client import SSHGitClient, SubprocessWrapper, TraditionalGitClient from dulwich.protocol import Protocol from dulwich.repo import Repo +from pymisc.monitoring import ScriptStatus +from pymisc.script import RecoverableException, ScriptConfiguration, ScriptLock import argparse import bernhard import dns.resolver @@ -47,19 +49,10 @@ #Constants: LOCKFILE_LOCATION = './'+os.path.basename(__file__)+'.lock' CONFIGFILE_LOCATION = './'+os.path.basename(__file__)+'.conf' -DATA_TTL = 25*60*60 # Data gathered by the script run is valid for 25 hours. SERVICE_NAME = 'check_cert' CERTIFICATE_EXTENSIONS = ['der', 'crt', 'pem', 'cer', 'p12', 'pfx', ] -class RecoverableException(Exception): - """ - Exception used to differentiate between errors which should be reported - to Riemann, and the ones that should be only logged due to their severity - """ - pass - - class PubkeySSHGitClient(SSHGitClient): """ Simple class used to add pubkey authentication to the SSHGitClient class. @@ -202,311 +195,6 @@ def wants_all_certs(path): return certs -class ScriptConfiguration(object): - """ - Simple file configuration class basing on the YAML format - """ - _config = dict() - - @classmethod - def load_config(cls, file_path): - """ - @param string file_path path to the configuration file - """ - try: - with open(file_path, 'r') as fh: - cls._config = yaml.load(fh) - except IOError as e: - logging.error("Failed to open config file {0}: {1}".format( - file_path, e)) - sys.exit(1) - except (yaml.parser.ParserError, ValueError) as e: - logging.error("File {0} is not a proper yaml document: {1}".format( - file_path, e)) - sys.exit(1) - - @classmethod - def get_val(cls, key): - return cls._config[key] - - -class ScriptStatus(object): - - _STATES = {'ok': 0, - 'warn': 1, - 'critical': 2, - 'unknown': 3, - } - - _exit_status = 'ok' - _exit_message = '' - _riemann_connections = [] - _riemann_tags = None - _hostname = '' - _debug = None - - @classmethod - def _send_data(cls, event): - """ - Send script status to all Riemann servers using all the protocols that - were configured. - """ - for riemann_connection in cls._riemann_connections: - logging.info('Sending event {0}, '.format(str(event)) + - 'using Riemann conn {0}:{1}'.format( - riemann_connection.host, riemann_connection.port) - ) - if not cls._debug: - try: - riemann_connection.send(event) - except Exception as e: - logging.exception("Failed to send event to Rieman host: " + - "{0}".format(str(e)) - ) - continue - else: - logging.info("Event sent succesfully") - else: - logging.info('Debug flag set, I am performing no-op instead of ' - 'real sent call') - - @classmethod - def _name2ip(cls, name): - """ - Resolve a dns name. In case it is already an IP - just return it. - """ - if re.match('\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}', name): - #IP entry: - return name - else: - #Hostname, we need to resolve it: - try: - ipaddr = dns.resolver.query(name, 'A') - except dns.resolver.NXDOMAIN: - logging.error("A record for {0} was not found".format(name)) - return name # Let somebody else worry about it ;) - - return ipaddr[0].to_text() - - @classmethod - def _resolve_srv_hosts(cls, name): - """ - Find Riemann servers by resolving SRV record, provide some sanity - checks as well. - """ - result = [] - logging.debug("Resolving " + name) - if name.find('._udp') > 0: - proto = 'udp' - elif name.find('._tcp') > 0: - proto = 'tcp' - else: - raise RecoverableException("Entry {0} ".format(name) + - "is not a valid SRV name") - - try: - resolved = dns.resolver.query(name, 'SRV') - except dns.resolver.NXDOMAIN: - logging.error("Entry {0} does not exist, skipping.") - return [] - - for rdata in resolved: - entry = namedtuple("RiemannHost", ['host', 'port', 'proto']) - entry.host = cls._name2ip(rdata.target.to_text()) - if entry.host is None: - continue - entry.port = rdata.port - entry.proto = proto - result.append(entry) - logging.debug("String {0} resolved as {1}".format(name, str(entry))) - - return result - - @classmethod - def _resolve_static_entry(cls, name): - """ - Find Riemann servers by resolving plain A record, provide some sanity - checks as well. - """ - entry = namedtuple("RiemannHost", ['host', 'port', 'proto']) - try: - a, b, c = name.split(":") - entry.host = cls._name2ip(a) - if entry.host is None: - raise ValueError() - entry.port = int(b) # Raises ValueError by itself - if c in ['tcp', 'udp']: - entry.proto = c - else: - raise ValueError() - except ValueError: - logging.error("String {0} is not a valid ip:port:proto entry") - return [] - - logging.debug("String {0} resolved as {1}".format(name, str(entry))) - return [entry] - - @classmethod - def initialize(cls, riemann_hosts_config, riemann_tags, debug=False): - cls._riemann_tags = riemann_tags - cls._hostname = socket.gethostname() - cls._debug = debug - cls._exit_status = 'ok' - cls._exit_message = '' - cls._riemann_connections = [] # FIXME - we should probably do - # some disconect here if we re-initialize - # probably using conn.shutdown() call - - if not riemann_tags: - logging.error('there should be at least one Riemann tag defined.') - return # should it sys.exit or just return ?? - tmp = [] - if "static" in riemann_hosts_config: - for line in riemann_hosts_config["static"]: - tmp.extend(cls._resolve_static_entry(line)) - - if "by_srv" in riemann_hosts_config: - for line in riemann_hosts_config["by_srv"]: - tmp.extend(cls._resolve_srv_hosts(line)) - - for riemann_host in tmp: - try: - if riemann_host.proto == 'tcp': - riemann_connection = bernhard.Client(riemann_host.host, - riemann_host.port, - bernhard.TCPTransport) - elif riemann_host.proto == 'udp': - riemann_connection = bernhard.Client(riemann_host.host, - riemann_host.port, - bernhard.UDPTransport) - else: - logging.error("Unsupported transport {0}".format(riemann_host.proto) + - ", not connected to {1}".format(riemann_host)) - except Exception as e: - logging.exception("Failed to connect to Rieman host " + - "{0}: {1}, ".format(riemann_host, str(e)) + - "address has been exluded from the list.") - continue - - logging.debug("Connected to Riemann instance {0}".format(riemann_host)) - cls._riemann_connections.append(riemann_connection) - - if not cls._riemann_connections: - logging.error("There are no active connections to Riemann, " + - "metrics will not be send!") - - @classmethod - def notify_immediate(cls, exit_status, exit_message): - """ - Imediatelly send given data to Riemann - """ - if exit_status not in cls._STATES: - logging.error("Trying to issue an immediate notification" + - "with malformed exit_status: " + exit_status) - return - - if not exit_message: - logging.error("Trying to issue an immediate" + - "notification without any message") - return - - logging.warn("notify_immediate, " + - "exit_status=<{0}>, exit_message=<{1}>".format( - exit_status, exit_message)) - event = { - 'host': cls._hostname, - 'service': SERVICE_NAME, - 'state': exit_status, - 'description': exit_message, - 'tags': cls._riemann_tags, - 'ttl': DATA_TTL, - } - - cls._send_data(event) - - @classmethod - def notify_agregated(cls): - """ - Send all agregated data to Riemann - """ - - if cls._exit_status == 'ok' and cls._exit_message == '': - cls._exit_message = 'All certificates are OK' - - logging.debug("notify_agregated, " + - "exit_status=<{0}>, exit_message=<{1}>".format( - cls._exit_status, cls._exit_message)) - - event = { - 'host': cls._hostname, - 'service': SERVICE_NAME, - 'state': cls._exit_status, - 'description': cls._exit_message, - 'tags': cls._riemann_tags, - 'ttl': DATA_TTL, - } - - cls._send_data(event) - - @classmethod - def update(cls, exit_status, exit_message): - """ - Accumullate a small bit of data in class fields - """ - if exit_status not in cls._STATES: - logging.error("Trying to do the status update" + - "with malformed exit_status: " + exit_status) - return - - logging.info("updating script status, " + - "exit_status=<{0}>, exit_message=<{1}>".format( - exit_status, exit_message)) - if cls._STATES[cls._exit_status] < cls._STATES[exit_status]: - cls._exit_status = exit_status - # ^ we only escalate up... - if exit_message: - if cls._exit_message: - cls._exit_message += '\n' - cls._exit_message += exit_message - - -class ScriptLock(object): - #python lockfile isn't usefull, we have to write our own class - _fh = None - _file_path = None - - @classmethod - def init(cls, file_path): - cls._file_path = file_path - - @classmethod - def aqquire(cls): - if cls._fh: - logging.warn("File lock already aquired") - return - try: - cls._fh = open(cls._file_path, 'w') - #flock is nice because it is automatically released when the - #process dies/terminates - fcntl.flock(cls._fh, fcntl.LOCK_EX | fcntl.LOCK_NB) - except IOError: - if cls._fh: - cls._fh.close() - raise RecoverableException("{0} ".format(cls._file_path) + - "is already locked by a different " + - "process or cannot be created.") - cls._fh.write(str(os.getpid())) - cls._fh.flush() - - @classmethod - def release(cls): - if not cls._fh: - raise RecoverableException("Trying to release non-existant lock") - cls._fh.close() - cls._fh = None - os.unlink(cls._file_path) - - def parse_command_line(): parser = argparse.ArgumentParser( description='Certificate checking tool', @@ -631,8 +319,12 @@ def main(config_file, std_err=False, verbose=True, dont_send=False): #Initialize Riemann reporting: ScriptStatus.initialize( + riemann_enabled=ScriptConfiguration.get_val("riemann_enabled"), riemann_hosts_config=ScriptConfiguration.get_val("riemann_hosts"), riemann_tags=ScriptConfiguration.get_val("riemann_tags"), + riemann_ttl=ScriptConfiguration.get_val("riemann_ttl"), + riemann_service_name=SERVICE_NAME, + nrpe_enabled=ScriptConfiguration.get_val("nrpe_enabled"), debug=dont_send, ) From dc215f662f4923ca02ba7b8de107e30a2a9ce6c7 Mon Sep 17 00:00:00 2001 From: Vespian Date: Fri, 16 May 2014 21:37:10 +0200 Subject: [PATCH 04/44] Refactor get_cert_expiration error handling Change-Id: If7f910d1fd1aad33a96f2cca22a518c512255024 --- check_cert/__init__.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/check_cert/__init__.py b/check_cert/__init__.py index b78656f..59b4820 100755 --- a/check_cert/__init__.py +++ b/check_cert/__init__.py @@ -260,16 +260,10 @@ def get_cert_expiration(certificate, ignored_certs): #Return datetime object: return datetime.strptime(expiry_date, '%Y%m%d%H%M%SZ') except Exception as e: - msg = "Script cannot parse certificate {0}: {1}".format( - certificate.path, str(e)) - logging.warn(msg) - ScriptStatus.update('unknown', msg) return None else: - ScriptStatus.update('unknown', - "Certificate {0} is of unsupported type, ".format( - certificate.path) + - "the script cannot check the expiry date.") + logging.error("Certificate {0} ".format(certificate.path) + + "is of unsupported type.") return None @@ -367,6 +361,8 @@ def main(config_file, std_err=False, verbose=True, dont_send=False): "ignored_certs") ) if cert_expiration is None: + ScriptStatus.update('unknown', "Script cannot parse certificate" + + "{0}".format(cert.path)) continue # -3 days is in fact -4 days, 23:59:58.817181 # so we compensate and round up From 34a1291e26090da72efa31044927b9c3bc8ae0ab Mon Sep 17 00:00:00 2001 From: Vespian Date: Fri, 16 May 2014 21:38:35 +0200 Subject: [PATCH 05/44] Broken cert should be different thant ignored one Change-Id: I546e74ad82a9a984951348dbe1205a3cd77326f6 --- .../sample_cert_dir/broken_certificate.crt | 27 +------------------ 1 file changed, 1 insertion(+), 26 deletions(-) diff --git a/test/fabric/sample_cert_dir/broken_certificate.crt b/test/fabric/sample_cert_dir/broken_certificate.crt index c8bcd59..a7483b7 100644 --- a/test/fabric/sample_cert_dir/broken_certificate.crt +++ b/test/fabric/sample_cert_dir/broken_certificate.crt @@ -1,28 +1,3 @@ -D,Mn -M"!0 -|\=e -tLY - e~P -|daB -pyc -Wdu -,y|] --?U n -xMDU -6eu.^ -R&,k -kU[cs -/.ck -(bux -: 20n2 O~FK ed!: -V \ No newline at end of file +V From ae0c1087e54934d536d820ab7d2665e24824a98b Mon Sep 17 00:00:00 2001 From: Vespian Date: Fri, 16 May 2014 21:39:26 +0200 Subject: [PATCH 06/44] Fix sample configuration Change-Id: Ie871377192acedd57921d0425676f4b1ae226750 --- test/fabric/check_cert.yml | 48 +++++++++++++++++++--------- test/fabric/check_cert_mopconfig.yml | 28 ---------------- 2 files changed, 33 insertions(+), 43 deletions(-) delete mode 100644 test/fabric/check_cert_mopconfig.yml diff --git a/test/fabric/check_cert.yml b/test/fabric/check_cert.yml index 5db0752..45a83ef 100644 --- a/test/fabric/check_cert.yml +++ b/test/fabric/check_cert.yml @@ -1,22 +1,40 @@ --- -lockfile: ./check_cert.lock -warn_treshold: 30 -critical_treshold: 15 +#Global +lockfile: /tmp/check_cert.lock + +#Riemann related: +riemann_enabled: False +riemann_ttl: 60 riemann_hosts: - - 127.0.0.1 - - 127.0.0.2 -riemann_port: 1234 + static: + - 192.168.122.16:5555:udp + - 192.168.122.16:5555:tcp + by_srv: + - _riemann._tcp + - _riemann._udp riemann_tags: - - abc - - def -repo_host: git.foo.net + - production + - class::check_cert + +#Nagios related: +nrpe_enabled: True + +#Repository related: +repo_host: git.example.com repo_port: 22 -repo_url: /foo-puppet -repo_masterbranch: refs/heads/foo -repo_localdir: /tmp/foo -repo_user: foo -repo_pubkey: ./foo +repo_url: /sample-repo +repo_masterbranch: refs/heads/production +repo_localdir: /tmp/check_cert-temprepo +repo_user: check_cert +repo_pubkey: /home/vespian/work/tmp_tickets/cert_check/check_cert_id_rsa + +#Check related: +warn_treshold: 30 +critical_treshold: 15 # sha1sum ./certificate_to_be_ignored # format - dict, hash as a key, and value as a comment ignored_certs: - 42b270cbd03eaa8c16c386e66f910195f769f8b1: "certificate used during unit-tests" + 208a4ffc0bb0aa9ce6ccf6ed8fe3aa289a24f4dc: "dh2048.pem - openvpn DH params" + 418b085d32a2dd0777c53fb5eedf1a92b0f4d112: "storage_resolve - rsa key" + 99753a302f50fb085cae06ac4176c8bdf7d96016: "uits_priv_key.pem - rsa key" + b82e55a5a2eb8b9dec81cbe55ba4d90809509a16: "uits_pub_key.pem - rsa key" diff --git a/test/fabric/check_cert_mopconfig.yml b/test/fabric/check_cert_mopconfig.yml deleted file mode 100644 index 2ae965a..0000000 --- a/test/fabric/check_cert_mopconfig.yml +++ /dev/null @@ -1,28 +0,0 @@ ---- -lockfile: /tmp/check_cert.lock -warn_treshold: 30 -critical_treshold: 15 -riemann_hosts: - static: - - 192.168.122.16:5555:udp - - 192.168.122.16:5555:tcp - by_srv: - - _riemann._tcp - - _riemann._udp -riemann_tags: - - production - - class::check_cert -repo_host: git.example.com -repo_port: 22 -repo_url: /sample-repo -repo_masterbranch: refs/heads/production -repo_localdir: /tmp/check_cert-temprepo -repo_user: check_cert -repo_pubkey: /home/vespian/work/tmp_tickets/cert_check/check_cert_id_rsa -# sha1sum ./certificate_to_be_ignored -# format - dict, hash as a key, and value as a comment -ignored_certs: - 208a4ffc0bb0aa9ce6ccf6ed8fe3aa289a24f4dc: "dh2048.pem - openvpn DH params" - 418b085d32a2dd0777c53fb5eedf1a92b0f4d112: "storage_resolve - rsa key" - 99753a302f50fb085cae06ac4176c8bdf7d96016: "uits_priv_key.pem - rsa key" - b82e55a5a2eb8b9dec81cbe55ba4d90809509a16: "uits_pub_key.pem - rsa key" From e3d7e974c5c2be6594146d8bc1f4f561813919a3 Mon Sep 17 00:00:00 2001 From: Vespian Date: Fri, 16 May 2014 21:40:33 +0200 Subject: [PATCH 07/44] [doc] Fix dependencies and config info Change-Id: Ib89d2e45b2efd28e23bfe8ad122bd2aa56149f71 --- README.md | 60 +++++++++++++++++++++++++++++++------------------------ 1 file changed, 34 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index d739a2a..1a3b799 100644 --- a/README.md +++ b/README.md @@ -7,14 +7,12 @@ and sending data on expiring/expired certificates back to the monitoring system ## Project Setup In order to run check_certer you need to following dependencies installed: -- Bernhard - Riemann client library (https://github.com/banjiewen/bernhard) -- Google's protobuf library -- yaml bindings for python (http://pyyaml.org/) - Dulwich - python implementation of GIT (https://www.samba.org/~jelmer/dulwich/docs/) - *ssh* command in your PATH - argparse library -- dnspython library (http://www.dnspython.org/) - pyOpenSSL (https://launchpad.net/pyopenssl/) +- pymisc (https://github.com/vespian/pymisc) +- python >=2.6 You can also use debian packaging rules from debian/ directory to build a deb package. @@ -47,9 +45,12 @@ The configuration file is a plain YAML document. It's syntax is as follows: ``` --- +#Global lockfile: /tmp/check_cert.lock -warn_treshold: 30 -critical_treshold: 15 + +#Riemann related: +riemann_enabled: False +riemann_ttl: 60 riemann_hosts: static: - 192.168.122.16:5555:udp @@ -60,18 +61,27 @@ riemann_hosts: riemann_tags: - production - class::check_cert -repo_host: git.example.net + +#Nagios related: +nrpe_enabled: True + +#Repository related: +repo_host: git.example.com repo_port: 22 -repo_url: /example-repo +repo_url: /sample-repo repo_masterbranch: refs/heads/production repo_localdir: /tmp/check_cert-temprepo repo_user: check_cert -repo_pubkey: ./check_cert_id_rsa - # format - dict, hash as a key, and value as a comment - # sha1sum ./certificate_to_be_ignored +repo_pubkey: /home/vespian/work/tmp_tickets/cert_check/check_cert_id_rsa + +#Check related: +warn_treshold: 30 +critical_treshold: 15 +# sha1sum ./certificate_to_be_ignored +# format - dict, hash as a key, and value as a comment ignored_certs: - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: "some VPN key" - bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb: "some unused certificate" + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: "cert a" + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb: "cert b" ``` ### Operation @@ -101,9 +111,9 @@ $warn_tresh but more than $critical_tresh - a "warning" partial status is gene- rated. Unsuported certificate yields an 'unknown' state and expired ones of course the 'critical'. -All the 'partial status' updates are agregated and each message can only ele- -vate up the final status of the metric send to Riemann. Currently, the hierar- -chy is as follows: +All the 'partial status' updates are agregated by the 'pymisc' library and +each message can only elevate up the final status of the metric send to +monitoring system. Currently, the hierarchy is as follows: (lowest)ok->warn->critical->unknown(highest) @@ -111,16 +121,14 @@ script errors, exceptions and unexcpected conditions result in imidiate elevatio to 'unknown' status and sending the metric to monitoring system ASAP if only possible. -IP addresses/ports of the Riemann instances can be defined in two ways: - * statically, by providing a list of riemann instances in $riemann_servers - var. The format of the list entry is hostname:port:proto. 'proto' can be one - of 'udp' or 'tcp'. - * by providing a SRV record, i.e. '_riemann._udp'. All the values - (host, port) will be resolved automatically. Protocol is chosen basing on - the SRV entry itself. - -The final metric is send to *all* Riemann instances with TTL equal to -check_cert:DATA_TTL == 25 hours. +Interfacing with monitoring system is done by pymisc. Following options are +passed directly to the library. Please see pymisc's documentation for +information on their meaning: +* $riemann_enabled +* $riemann_ttl +* $riemann_hosts +* $riemann_tags +* $nrpe_enabled ### Maintenance From bfcd34acc6da42d57cde1adf99d7017badf61ff9 Mon Sep 17 00:00:00 2001 From: Vespian Date: Fri, 16 May 2014 21:40:57 +0200 Subject: [PATCH 08/44] Fix tests, add more tests Change-Id: I8fb84ed3fb8cef17328094348ce1085fab2ddd6f --- test/modules/file_paths.py | 1 + .../moduletests/check_cert/test_check_cert.py | 481 +++++++----------- 2 files changed, 180 insertions(+), 302 deletions(-) diff --git a/test/modules/file_paths.py b/test/modules/file_paths.py index 61171a9..02bbc03 100644 --- a/test/modules/file_paths.py +++ b/test/modules/file_paths.py @@ -30,6 +30,7 @@ EXPIRE_6_DAYS = op.join(CERTIFICATES_DIR, 'expire_6_days.pem') EXPIRE_21_DAYS = op.join(CERTIFICATES_DIR, 'expire_21_days.pem') EXPIRE_41_DAYS = op.join(CERTIFICATES_DIR, 'expire_41_days.pem') +TRUSTED_EXPIRE_41_CERT = op.join(CERTIFICATES_DIR, 'trusted_expire_41_days.pem') EXPIRE_41_DAYS_DER = op.join(CERTIFICATES_DIR, 'expire_41_days.der') BROKEN_CERT = op.join(CERTIFICATES_DIR, 'broken_certificate.crt') IGNORED_CERT = op.join(CERTIFICATES_DIR, 'ignored_certificate.crt') diff --git a/test/moduletests/check_cert/test_check_cert.py b/test/moduletests/check_cert/test_check_cert.py index 598917a..5f00073 100644 --- a/test/moduletests/check_cert/test_check_cert.py +++ b/test/moduletests/check_cert/test_check_cert.py @@ -15,21 +15,20 @@ # the License. -#Make it a bit more like python3: +# Make it a bit more like python3: from __future__ import absolute_import from __future__ import division from __future__ import nested_scopes from __future__ import print_function from __future__ import with_statement -#Global imports: +# Global imports: from collections import namedtuple from datetime import datetime, timedelta +import fileinput import os -import platform import subprocess import sys -import time major, minor, micro, releaselevel, serial = sys.version_info if major == 2 and minor < 7: import unittest2 as unittest @@ -37,16 +36,22 @@ import unittest import mock -#To perform local imports first we need to fix PYTHONPATH: +# To perform local imports first we need to fix PYTHONPATH: pwd = os.path.abspath(os.path.dirname(__file__)) sys.path.append(os.path.abspath(pwd + '/../../modules/')) -#Local imports: +# Local imports: import file_paths as paths import check_cert -class TestCertCheck(unittest.TestCase): +@mock.patch('logging.warn') +@mock.patch('logging.info') +@mock.patch('logging.error') +@mock.patch('check_cert.ScriptLock', autospec=True) +@mock.patch('check_cert.ScriptStatus', autospec=True) +@mock.patch('check_cert.ScriptConfiguration', autospec=True) +class TestCheckCert(unittest.TestCase): @staticmethod def _create_test_cert(days, path, is_der=False): openssl_cmd = ["/usr/bin/openssl", "req", "-x509", "-nodes", @@ -73,6 +78,42 @@ def _create_test_cert(days, path, is_der=False): else: print("Created test certificate {0}".format(os.path.basename(path))) + def _script_conf_factory(self, **kwargs): + """ + Provide fake configuration data objects. + """ + good_configuration = {"warn_treshold": 30, + "critical_treshold": 15, + "nrpe_enabled": True, + "riemann_enabled": True, + "riemann_hosts": { + 'static': ['1.2.3.4:1:udp', + '2.3.4.5:5555:tcp', ] + }, + "riemann_tags": ["abc", "def"], + "riemann_ttl": 60, + "repo_host": "git.foo.net", + "repo_port": 22, + "repo_url": "/foo-puppet", + "repo_masterbranch": "refs/heads/foo", + "repo_localdir": "/tmp/foo", + "repo_user": "foo", + "repo_pubkey": "./foo", + "lockfile": "./fake_lock.pid", + "ignored_certs": { + '42b270cbd03eaa8c16c386e66f910195f769f8b1': + "certificate used during unit-tests" + } + } + + def func(key): + config = good_configuration.copy() + config.update(kwargs) + self.assertIn(key, config) + return config[key] + + return func + @staticmethod def _certpath2namedtuple(path): with open(path, 'rb') as fh: @@ -83,47 +124,21 @@ def _certpath2namedtuple(path): @classmethod def setUpClass(cls): - #Prepare the test certificate tree: + # Prepare the test certificate tree: cls._create_test_cert(-3, paths.EXPIRED_3_DAYS) cls._create_test_cert(6, paths.EXPIRE_6_DAYS) cls._create_test_cert(21, paths.EXPIRE_21_DAYS) cls._create_test_cert(41, paths.EXPIRE_41_DAYS) cls._create_test_cert(41, paths.EXPIRE_41_DAYS_DER, is_der=True,) + cls._create_test_cert(41, paths.TRUSTED_EXPIRE_41_CERT) - @mock.patch('logging.error') - @mock.patch('sys.exit') - def test_config_file_parsing(self, SysExitMock, LoggingErrorMock): - #Test malformed file loading - check_cert.ScriptConfiguration.load_config(paths.TEST_MALFORMED_CONFIG_FILE) - self.assertTrue(LoggingErrorMock.called) - SysExitMock.assert_called_once_with(1) - SysExitMock.reset_mock() - - #Test non-existent file loading - check_cert.ScriptConfiguration.load_config(paths.TEST_NONEXISTANT_CONFIG_FILE) - self.assertTrue(LoggingErrorMock.called) - SysExitMock.assert_called_once_with(1) - - #Load the config file - check_cert.ScriptConfiguration.load_config(paths.TEST_CONFIG_FILE) - - #String: - self.assertEqual(check_cert.ScriptConfiguration.get_val("repo_host"), - "git.foo.net") - #List of strings - self.assertEqual(check_cert.ScriptConfiguration.get_val("riemann_tags"), - ['abc', 'def']) - #Integer: - self.assertEqual(check_cert.ScriptConfiguration.get_val("warn_treshold"), 30) - - #Key not in config file: - with self.assertRaises(KeyError): - check_cert.ScriptConfiguration.get_val("not_a_field") - - @mock.patch.object(check_cert.ScriptStatus, 'notify_immediate') # same as below - @mock.patch('logging.warn') # Unused, but masks error messages - @mock.patch.object(check_cert.ScriptStatus, 'update') - def test_cert_expiration_parsing(self, UpdateMock, *unused): + # Simulate a sample certificate that has non-standard header + for line in fileinput.input(paths.TRUSTED_EXPIRE_41_CERT, inplace=True): + print(line.replace('-----BEGIN CERTIFICATE-----', + '-----BEGIN TRUSTED CERTIFICATE-----'), end="") + + def test_cert_expiration_parsing(self, ScriptConfigurationMock, ScriptStatusMock, + *unused): IGNORED_CERTS = ['42b270cbd03eaa8c16c386e66f910195f769f8b1'] # -3 days is in fact -4 days, 23:59:58.817181 @@ -131,190 +146,41 @@ def test_cert_expiration_parsing(self, UpdateMock, *unused): # additionally, openssl uses utc dates now = datetime.utcnow() - timedelta(days=1) - #Test an expired certificate: + # Test an expired certificate: cert = self._certpath2namedtuple(paths.EXPIRED_3_DAYS) - expiry_time = check_cert.get_cert_expiration( - cert, IGNORED_CERTS) - now + expiry_time = check_cert.get_cert_expiration(cert, IGNORED_CERTS) - now self.assertEqual(expiry_time.days, -3) - #Test an ignored certificate: + # Test an ignored certificate: cert = self._certpath2namedtuple(paths.IGNORED_CERT) - expiry_time = check_cert.get_cert_expiration(cert, - IGNORED_CERTS) + expiry_time = check_cert.get_cert_expiration(cert, IGNORED_CERTS) self.assertEqual(expiry_time, None) - #Test a good certificate: + # Test a good certificate: cert = self._certpath2namedtuple(paths.EXPIRE_21_DAYS) - expiry_time = check_cert.get_cert_expiration(cert, - IGNORED_CERTS) - now + expiry_time = check_cert.get_cert_expiration(cert, IGNORED_CERTS) - now self.assertEqual(expiry_time.days, 21) - #Test a DER certificate: + # Test a DER certificate: cert = self._certpath2namedtuple(paths.EXPIRE_41_DAYS_DER) - check_cert.get_cert_expiration(cert, IGNORED_CERTS) - self.assertTrue(UpdateMock.called) - self.assertEqual(UpdateMock.call_args_list[0][0][0], 'unknown') + expiry_time = check_cert.get_cert_expiration(cert, IGNORED_CERTS) + self.assertIs(expiry_time, None) - #Test a broken certificate: + # Test a broken certificate: cert = self._certpath2namedtuple(paths.BROKEN_CERT) - check_cert.get_cert_expiration(cert, IGNORED_CERTS) - self.assertTrue(UpdateMock.called) - self.assertEqual(UpdateMock.call_args_list[0][0][0], 'unknown') - - @mock.patch('logging.warn') - def test_file_locking(self, LoggingWarnMock, *unused): - check_cert.ScriptLock.init(paths.TEST_LOCKFILE) - - with self.assertRaises(check_cert.RecoverableException): - check_cert.ScriptLock.release() - - check_cert.ScriptLock.aqquire() - - check_cert.ScriptLock.aqquire() - self.assertTrue(LoggingWarnMock.called) - - self.assertTrue(os.path.exists(paths.TEST_LOCKFILE)) - self.assertTrue(os.path.isfile(paths.TEST_LOCKFILE)) - self.assertFalse(os.path.islink(paths.TEST_LOCKFILE)) - - with open(paths.TEST_LOCKFILE, 'r') as fh: - pid_str = fh.read() - self.assertGreater(len(pid_str), 0) - pid = int(pid_str) - self.assertEqual(pid, os.getpid()) + expiry_time = check_cert.get_cert_expiration(cert, IGNORED_CERTS) + self.assertIs(expiry_time, None) - check_cert.ScriptLock.release() - - child = os.fork() - if not child: - #we are in the child process: - check_cert.ScriptLock.aqquire() - time.sleep(10) - #script should not do any cleanup - it is part of the tests :) - else: - #parent - timer = 0 - while timer < 3: - if os.path.isfile(paths.TEST_LOCKFILE): - break - else: - timer += 0.1 - time.sleep(0.1) - else: - # Child did not create pidfile in 3 s, - # we should clean up and bork: - os.kill(child, 9) - assert False - - with self.assertRaises(check_cert.RecoverableException): - check_cert.ScriptLock.aqquire() - - os.kill(child, 11) - - #now it should succed - check_cert.ScriptLock.aqquire() - - @mock.patch('logging.warn') # Unused, but masks error messages - @mock.patch('logging.info') - @mock.patch('logging.error') - @mock.patch('check_cert.bernhard') - def test_script_status(self, RiemannMock, LoggingErrorMock, LoggingInfoMock, - *unused): - #There should be at least one tag defined: - check_cert.ScriptStatus.initialize(riemann_hosts_config={}, riemann_tags=[]) - self.assertTrue(LoggingErrorMock.called) - LoggingErrorMock.reset_mock() - - #There should be at least one Riemann host defined: - check_cert.ScriptStatus.initialize(riemann_hosts_config={}, - riemann_tags=['tag1', 'tag2']) - self.assertTrue(LoggingErrorMock.called) - LoggingErrorMock.reset_mock() - - #Riemann exceptions should be properly handled/reported: - def side_effect(host, port): - raise Exception("Raising exception for {0}:{1} pair") - - RiemannMock.UDPTransport = 'UDPTransport' - RiemannMock.TCPTransport = 'TCPTransport' - RiemannMock.Client.side_effect = side_effect - - check_cert.ScriptStatus.initialize(riemann_hosts_config={ - 'static': ['192.168.122.16:5555:udp']}, - riemann_tags=['tag1', 'tag2']) - self.assertTrue(LoggingErrorMock.called) - LoggingErrorMock.reset_mock() - - RiemannMock.Client.side_effect = None - RiemannMock.Client.reset_mock() - - #Mock should only allow legitimate exit_statuses - check_cert.ScriptStatus.notify_immediate("not a real status", "message") - self.assertTrue(LoggingErrorMock.called) - LoggingErrorMock.reset_mock() - - check_cert.ScriptStatus.update("not a real status", "message") - self.assertTrue(LoggingErrorMock.called) - LoggingErrorMock.reset_mock() - - #Done with syntax checking, now initialize the class properly: - check_cert.ScriptStatus.initialize(riemann_hosts_config={ - 'static': ['1.2.3.4:1:udp', - '2.3.4.5:5555:tcp',] - }, - riemann_tags=['tag1', 'tag2']) - - proper_calls = [mock.call('1.2.3.4', 1, 'UDPTransport'), - mock.call('2.3.4.5', 5555, 'TCPTransport')] - RiemannMock.Client.assert_has_calls(proper_calls) - RiemannMock.Client.reset_mock() - - #Check if notify_immediate works - check_cert.ScriptStatus.notify_immediate("warn", "a warning message") - self.assertTrue(LoggingInfoMock.called) - LoggingErrorMock.reset_mock() - - proper_call = mock.call().send({'description': 'a warning message', - 'service': 'check_cert', - 'tags': ['tag1', 'tag2'], - 'state': 'warn', - 'host': platform.uname()[1], - 'ttl': 90000} - ) - # This call should be issued to *both* connection mocks, but we - # simplify things here a bit: - self.assertEqual(2, len([x for x in RiemannMock.Client.mock_calls - if x == proper_call])) - RiemannMock.Client.reset_mock() - - #update method shoul escalate only up: - check_cert.ScriptStatus.update('warn', "this is a warning message.") - check_cert.ScriptStatus.update('ok', '') - check_cert.ScriptStatus.update('unknown', "this is a not-rated message.") - check_cert.ScriptStatus.update('ok', "this is an informational message.") - - proper_call = mock.call().send({'description': - 'this is a warning message.\n' + - 'this is a not-rated message.\n' + - 'this is an informational message.', - 'service': 'check_cert', - 'tags': ['tag1', 'tag2'], - 'state': 'unknown', - 'host': platform.uname()[1], - 'ttl': 90000} - ) - # This call should be issued to *both* connection mocks, but we - # simplify things here a bit: - check_cert.ScriptStatus.notify_agregated() - self.assertEqual(2, len([x for x in RiemannMock.Client.mock_calls - if x == proper_call])) - RiemannMock.reset_mock() + # Test a "TRUSTED" certificate: + cert = self._certpath2namedtuple(paths.TRUSTED_EXPIRE_41_CERT) + expiry_time = check_cert.get_cert_expiration(cert, IGNORED_CERTS) - now + self.assertEqual(expiry_time.days, 41) @mock.patch('sys.exit') - def test_command_line_parsing(self, SysExitMock): + def test_command_line_parsing(self, SysExitMock, *unused): old_args = sys.argv - #General parsing: + # General parsing: sys.argv = ['./check_cert', '-v', '-s', '-d', '-c', './check_cert.json'] parsed_cmdline = check_cert.parse_command_line() self.assertEqual(parsed_cmdline, {'std_err': True, @@ -323,14 +189,14 @@ def test_command_line_parsing(self, SysExitMock): 'dont_send': True, }) - #Config file should be a mandatory argument: + # Config file should be a mandatory argument: sys.argv = ['./check_cert', ] # Suppres warnings from argparse with mock.patch('sys.stderr'): parsed_cmdline = check_cert.parse_command_line() SysExitMock.assert_called_once_with(2) - #Test default values: + # Test default values: sys.argv = ['./check_cert', '-c', './check_cert.json'] parsed_cmdline = check_cert.parse_command_line() self.assertEqual(parsed_cmdline, {'std_err': False, @@ -341,93 +207,77 @@ def test_command_line_parsing(self, SysExitMock): sys.argv = old_args - @mock.patch('check_cert.sys.exit') - @mock.patch('check_cert.get_cert_expiration') + @mock.patch('sys.exit') @mock.patch('check_cert.CertStore') - @mock.patch('check_cert.ScriptLock', autospec=True) - @mock.patch('check_cert.ScriptStatus', autospec=True) - @mock.patch('check_cert.ScriptConfiguration', autospec=True) - @mock.patch('check_cert.logging', autospec=True) - def test_script_logic(self, LoggingMock, ScriptConfigurationMock, - ScriptStatusMock, ScriptLockMock, CertStoreMock, - CertExpirationMock, SysExitMock): - - #Fake configuration data: - def script_conf_factory(**kwargs): - good_configuration = {"warn_treshold": 30, - "critical_treshold": 15, - "riemann_hosts": { - 'static': ['1.2.3.4:1:udp', - '2.3.4.5:5555:tcp',] - }, - "riemann_tags": ["abc", "def"], - "repo_host": "git.foo.net", - "repo_port": 22, - "repo_url": "/foo-puppet", - "repo_masterbranch": "refs/heads/foo", - "repo_localdir": "/tmp/foo", - "repo_user": "foo", - "repo_pubkey": "./foo", - "lockfile": "./fake_lock.pid", - "ignored_certs": { - '42b270cbd03eaa8c16c386e66f910195f769f8b1': "certificate used during unit-tests" - } - } + def test_script_init(self, CertStoreMock, SysExitMock, + ScriptConfigurationMock, ScriptStatusMock, + ScriptLockMock, *unused): + """ + Test if script initializes its dependencies properly + """ - def func(key): - config = good_configuration.copy() - config.update(kwargs) - self.assertIn(key, config) - return config[key] + ScriptConfigurationMock.get_val.side_effect = self._script_conf_factory() - return func + check_cert.main(config_file='./check_cert.conf') + + proper_init_call = dict(riemann_enabled=True, + riemann_ttl=60, + riemann_service_name='check_cert', + riemann_hosts_config={ + 'static': ['1.2.3.4:1:udp', + '2.3.4.5:5555:tcp', ] + }, + riemann_tags=['abc', 'def'], + nrpe_enabled=True, + debug=False) + ScriptConfigurationMock.load_config.assert_called_once_with('./check_cert.conf') + ScriptLockMock.init.assert_called_once_with("./fake_lock.pid") + ScriptLockMock.aqquire.assert_called_once_with() + ScriptStatusMock.initialize.assert_called_once_with(**proper_init_call) + + proper_init_call = dict(host="git.foo.net", + port=22, + pubkey="./foo", + username="foo", + repo_localdir="/tmp/foo", + repo_url="/foo-puppet", + repo_masterbranch="refs/heads/foo",) + + CertStoreMock.initialize.assert_called_once_with(**proper_init_call) + + @mock.patch('sys.exit') + @mock.patch('check_cert.get_cert_expiration') + @mock.patch('check_cert.CertStore') + def test_sanity_checking(self, CertStoreMock, CertExpirationMock, + SysExitMock, ScriptConfigurationMock, ScriptStatusMock, + *unused): - # A bit of a workaround, but we cannot simply call sys.exit def terminate_script(exit_status): raise SystemExit(exit_status) SysExitMock.side_effect = terminate_script - #Provide fake data for the script: - fake_cert_tuple = namedtuple("FileTuple", ['path', 'content']) - fake_cert_tuple.path = 'some_cert' - fake_cert_tuple.content = 'some content' - - def fake_cert(cert_extensions): - return iter([fake_cert_tuple]) - CertStoreMock.lookup_certs.side_effect = fake_cert - # Test if ScriptStatus gets properly initialized # and whether warn > crit condition is # checked as well - check_cert.ScriptConfiguration.get_val.side_effect = \ - script_conf_factory(warn_treshold=7) + ScriptConfigurationMock.get_val.side_effect = \ + self._script_conf_factory(warn_treshold=7, critical_treshold=15) with self.assertRaises(SystemExit) as e: check_cert.main(config_file='./check_cert.conf') self.assertEqual(e.exception.code, 1) - proper_init_call = dict(riemann_hosts_config= { - 'static': ['1.2.3.4:1:udp', - '2.3.4.5:5555:tcp',] - }, - riemann_tags=['abc', 'def'], - debug=False) - self.assertTrue(ScriptConfigurationMock.load_config.called) - self.assertTrue(ScriptStatusMock.notify_immediate.called) - check_cert.ScriptStatus.initialize.assert_called_once_with(**proper_init_call) - - #this time test only the negative warn threshold: + # this time test only the negative warn threshold: check_cert.ScriptConfiguration.get_val.side_effect = \ - script_conf_factory(warn_treshold=-30) + self._script_conf_factory(warn_treshold=-30) ScriptStatusMock.notify_immediate.reset_mock() with self.assertRaises(SystemExit) as e: check_cert.main(config_file='./check_cert.conf') self.assertTrue(ScriptStatusMock.notify_immediate.called) self.assertEqual(e.exception.code, 1) - #this time test only the crit threshold == 0 condition check: + # this time test only the crit threshold == 0 condition check: check_cert.ScriptConfiguration.get_val.side_effect = \ - script_conf_factory(critical_treshold=-1) + self._script_conf_factory(critical_treshold=-1) ScriptStatusMock.notify_immediate.reset_mock() with self.assertRaises(SystemExit) as e: @@ -435,11 +285,30 @@ def fake_cert(cert_extensions): self.assertTrue(ScriptStatusMock.notify_immediate.called) self.assertEqual(e.exception.code, 1) - #test if an expired cert is properly handled: - ScriptStatusMock.notify_immediate.reset_mock() - check_cert.ScriptConfiguration.get_val.side_effect = \ - script_conf_factory() + @mock.patch('check_cert.sys.exit') + @mock.patch('check_cert.get_cert_expiration') + @mock.patch('check_cert.CertStore') + def test_certificate_testing(self, CertStoreMock, CertExpirationMock, + SysExitMock, ScriptConfigurationMock, + ScriptStatusMock, ScriptLockMock, *unused): + + # A bit of a workaround, but we cannot simply call sys.exit + def terminate_script(exit_status): + raise SystemExit(exit_status) + SysExitMock.side_effect = terminate_script + + # Provide fake data for the script: + fake_cert_tuple = namedtuple("FileTuple", ['path', 'content']) + fake_cert_tuple.path = 'some_cert' + fake_cert_tuple.content = 'some content' + def fake_cert(cert_extensions): + return iter([fake_cert_tuple]) + CertStoreMock.lookup_certs.side_effect = fake_cert + + ScriptConfigurationMock.get_val.side_effect = self._script_conf_factory() + + # test if an expired cert is properly handled: def fake_cert_expiration(cert, ignored_certs): self.assertEqual(cert, fake_cert_tuple) return datetime.utcnow() - timedelta(days=4) @@ -449,16 +318,11 @@ def fake_cert_expiration(cert, ignored_certs): self.assertEqual(e.exception.code, 0) self.assertTrue(ScriptStatusMock.update.called) self.assertEqual(ScriptStatusMock.update.call_args[0][0], 'critical') - self.assertTrue(ScriptLockMock.aqquire.called) - self.assertTrue(ScriptLockMock.release.called) self.assertTrue(ScriptStatusMock.notify_agregated.called) self.assertFalse(ScriptStatusMock.notify_immediate.called) + ScriptStatusMock.reset_mock() - #test if soon to expire ( Date: Fri, 16 May 2014 22:25:20 +0200 Subject: [PATCH 09/44] [test] Add exception handling testing Change-Id: I16dc233f98585e211b573eb0dcc02491f2fbe826 --- check_cert/__init__.py | 4 +- .../moduletests/check_cert/test_check_cert.py | 47 ++++++++++++++++++- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/check_cert/__init__.py b/check_cert/__init__.py index 59b4820..d936ba1 100755 --- a/check_cert/__init__.py +++ b/check_cert/__init__.py @@ -395,7 +395,7 @@ def main(config_file, std_err=False, verbose=True, dont_send=False): except RecoverableException as e: msg = str(e) - logging.critical(msg) + logging.error(msg) ScriptStatus.notify_immediate('unknown', msg) sys.exit(1) except AssertionError as e: @@ -403,5 +403,5 @@ def main(config_file, std_err=False, verbose=True, dont_send=False): raise except Exception as e: msg = "Exception occured: {0}".format(e.__class__.__name__) - logging.exception(msg) + logging.error(msg) sys.exit(1) diff --git a/test/moduletests/check_cert/test_check_cert.py b/test/moduletests/check_cert/test_check_cert.py index 5f00073..a626b3c 100644 --- a/test/moduletests/check_cert/test_check_cert.py +++ b/test/moduletests/check_cert/test_check_cert.py @@ -25,7 +25,9 @@ # Global imports: from collections import namedtuple from datetime import datetime, timedelta +from pymisc.script import RecoverableException, FatalException import fileinput +import mock import os import subprocess import sys @@ -34,7 +36,6 @@ import unittest2 as unittest else: import unittest -import mock # To perform local imports first we need to fix PYTHONPATH: pwd = os.path.abspath(os.path.dirname(__file__)) @@ -373,6 +374,7 @@ def fake_cert_expiration(cert, ignored_certs): self.assertFalse(ScriptStatusMock.notify_immediate.called) self.assertTrue(ScriptStatusMock.notify_agregated.called) self.assertEqual(ScriptStatusMock.update.call_args[0][0], 'critical') + ScriptStatusMock.reset_mock() # test if a certificate that is malformed/invalid is properly handled: def fake_cert_expiration(cert, ignored_certs): @@ -385,6 +387,49 @@ def fake_cert_expiration(cert, ignored_certs): self.assertFalse(ScriptStatusMock.notify_immediate.called) self.assertTrue(ScriptStatusMock.notify_agregated.called) self.assertEqual(ScriptStatusMock.update.call_args[0][0], 'unknown') + ScriptStatusMock.reset_mock() + + @mock.patch('check_cert.sys.exit') + @mock.patch('check_cert.get_cert_expiration') + @mock.patch('check_cert.CertStore') + def test_exception_handling(self, CertStoreMock, CertExpirationMock, + SysExitMock, ScriptConfigurationMock, + ScriptStatusMock, ScriptLockMock, + LoggingErrorMock, LoggingInfoMock, + LoggingWarnMock): + + # A bit of a workaround, but we cannot simply call sys.exit + def terminate_script(exit_status): + raise SystemExit(exit_status) + SysExitMock.side_effect = terminate_script + + #Provide some sane configuration: + ScriptConfigurationMock.get_val.side_effect = self._script_conf_factory() + + # Test an exception from which we can recover: + def throw_test_exception(cert_extensions): + raise RecoverableException("this is just a test exception") + CertStoreMock.lookup_certs.side_effect = throw_test_exception + + with self.assertRaises(SystemExit) as e: + check_cert.main(config_file='./check_cert.conf') + self.assertEqual(e.exception.code, 1) + self.assertTrue(LoggingErrorMock.called) + self.assertTrue(ScriptStatusMock.notify_immediate.called) + self.assertEqual(ScriptStatusMock.notify_immediate.call_args[0][0], 'unknown') + ScriptStatusMock.reset_mock() + + # Test an fatal exception + def throw_test_exception(cert_extensions): + raise FatalException("this is just a test exception") + CertStoreMock.lookup_certs.side_effect = throw_test_exception + + with self.assertRaises(SystemExit) as e: + check_cert.main(config_file='./check_cert.conf') + self.assertEqual(e.exception.code, 1) + self.assertTrue(LoggingErrorMock.called) + self.assertFalse(ScriptStatusMock.notify_immediate.called) + ScriptStatusMock.reset_mock() if __name__ == '__main__': From ddfc047df1db84d81d522453d98a3e76aad04402 Mon Sep 17 00:00:00 2001 From: Vespian Date: Fri, 16 May 2014 22:29:59 +0200 Subject: [PATCH 10/44] Remove unecessary imports, fix formating Change-Id: I5f8b2b598d092940b3c07b462e4d2a6b6244bbbd --- check_cert/__init__.py | 74 +++++++++++++++++++----------------------- 1 file changed, 34 insertions(+), 40 deletions(-) diff --git a/check_cert/__init__.py b/check_cert/__init__.py index d936ba1..4a34ce0 100755 --- a/check_cert/__init__.py +++ b/check_cert/__init__.py @@ -16,13 +16,13 @@ # the License. -#Make it a bit more like python3: +# Make it a bit more like python3: from __future__ import division from __future__ import nested_scopes from __future__ import print_function from __future__ import with_statement -#Imports: +# Imports: from OpenSSL.crypto import FILETYPE_PEM from OpenSSL.crypto import load_certificate from collections import namedtuple @@ -33,20 +33,14 @@ from pymisc.monitoring import ScriptStatus from pymisc.script import RecoverableException, ScriptConfiguration, ScriptLock import argparse -import bernhard -import dns.resolver -import fcntl import hashlib import logging import logging.handlers as lh import os -import re -import socket import subprocess import sys -import yaml -#Constants: +# Constants: LOCKFILE_LOCATION = './'+os.path.basename(__file__)+'.lock' CONFIGFILE_LOCATION = './'+os.path.basename(__file__)+'.conf' SERVICE_NAME = 'check_cert' @@ -68,8 +62,8 @@ def __init__(self, host, pubkey, port=None, username=None, *args, **kwargs): self.alternative_paths = {} def _connect(self, cmd, path): - #FIXME: This has no way to deal with passphrases.. - #FIXME: can we rely on ssh being in PATH here ? + # FIXME: This has no way to deal with passphrases.. + # FIXME: can we rely on ssh being in PATH here ? args = ['ssh', '-x', '-oStrictHostKeyChecking=no'] args.extend(['-i', self.pubkey]) if self.port is not None: @@ -112,8 +106,8 @@ def lookup_files(self, determine_wants, root_sha=None, repo_path=''): root_sha = commit.tree root = self.get_object(root_sha) if repo_path: - #Extreme verbosity - #logging.debug("Scanning repo directory {0}".format(repo_path)) + # Extreme verbosity + # logging.debug("Scanning repo directory {0}".format(repo_path)) pass else: logging.info("Scanning repo root directory") @@ -121,13 +115,13 @@ def lookup_files(self, determine_wants, root_sha=None, repo_path=''): for item in root.iteritems(): full_path = os.path.join(repo_path, item.path) if item.mode & 0b0100000000000000: - #A directory: + # A directory: subentries = self.lookup_files(determine_wants=determine_wants, root_sha=item.sha, repo_path=full_path) file_list.extend(subentries) if item.mode & 0b1000000000000000: - #A file, lets check if user wants it: + # A file, lets check if user wants it: if determine_wants(item.path): logging.info("Matching file found: {0}".format(full_path)) buf = namedtuple("FileTuple", ['path', 'sha']) @@ -163,9 +157,9 @@ def initialize(cls, host, port, pubkey, username, repo_localdir, repo_url, else: cls._local = LocalMirrorRepo(repo_localdir) - #We are only interested in 'production' branch, not the topic branches - #all the commits linked to the master will be downloaded as well of - #course + # We are only interested in 'production' branch, not the topic branches + # all the commits linked to the master will be downloaded as well of + # course def wants_master_only(refs): return [sha for (ref, sha) in refs.iteritems() if ref == repo_masterbranch] @@ -241,14 +235,14 @@ def get_cert_expiration(certificate, ignored_certs): """ if certificate.path[-3:] in ['pem', 'crt', 'cer']: try: - #Many bad things can happen here, but still - we can recover! :) + # Many bad things can happen here, but still - we can recover! :) cert_hash = hashlib.sha1(certificate.content).hexdigest() if cert_hash in ignored_certs: - #This cert should be ignored + # This cert should be ignored logging.info("certificate {0} (sha1sum: {1})".format( certificate.path, cert_hash) + " has been ignored.") return None - #Workaround for -----BEGIN TRUSTED CERTIFICATE----- + # Workaround for -----BEGIN TRUSTED CERTIFICATE----- if certificate.content.find('TRUSTED ') > -1: logging.info("'TRUSTED' string has been removed from " + "certificate {0} (sha1sum: {1})".format( @@ -257,9 +251,9 @@ def get_cert_expiration(certificate, ignored_certs): '') cert_data = load_certificate(FILETYPE_PEM, certificate.content) expiry_date = cert_data.get_notAfter() - #Return datetime object: + # Return datetime object: return datetime.strptime(expiry_date, '%Y%m%d%H%M%SZ') - except Exception as e: + except Exception: return None else: logging.error("Certificate {0} ".format(certificate.path) + @@ -269,7 +263,7 @@ def get_cert_expiration(certificate, ignored_certs): def main(config_file, std_err=False, verbose=True, dont_send=False): try: - #Configure logging: + # Configure logging: fmt = logging.Formatter('%(filename)s[%(process)d] %(levelname)s: ' + '%(message)s') logger = logging.getLogger() @@ -291,27 +285,27 @@ def main(config_file, std_err=False, verbose=True, dont_send=False): "verbose={0}, ".format(verbose) ) - #FIXME - Remember to correctly configure syslog, otherwise rsyslog will - #discard messages + # FIXME - Remember to correctly configure syslog, otherwise rsyslog will + # discard messages ScriptConfiguration.load_config(config_file) logger.debug("Remote repo is is: {0}@{1}:{2}{3}->{4}".format( - ScriptConfiguration.get_val("repo_user"), - ScriptConfiguration.get_val("repo_host"), - ScriptConfiguration.get_val("repo_port"), - ScriptConfiguration.get_val("repo_url"), - ScriptConfiguration.get_val("repo_masterbranch")) + + ScriptConfiguration.get_val("repo_user"), + ScriptConfiguration.get_val("repo_host"), + ScriptConfiguration.get_val("repo_port"), + ScriptConfiguration.get_val("repo_url"), + ScriptConfiguration.get_val("repo_masterbranch")) + ", local repository dir is {0}".format( - ScriptConfiguration.get_val('repo_localdir')) + + ScriptConfiguration.get_val('repo_localdir')) + ", repository key is {0}".format( - ScriptConfiguration.get_val('repo_pubkey')) + + ScriptConfiguration.get_val('repo_pubkey')) + ", warn_thresh is {0}".format( - ScriptConfiguration.get_val('warn_treshold')) + + ScriptConfiguration.get_val('warn_treshold')) + ", crit_thresh is {0}".format( - ScriptConfiguration.get_val('critical_treshold')) + ScriptConfiguration.get_val('critical_treshold')) ) - #Initialize Riemann reporting: + # Initialize Riemann reporting: ScriptStatus.initialize( riemann_enabled=ScriptConfiguration.get_val("riemann_enabled"), riemann_hosts_config=ScriptConfiguration.get_val("riemann_hosts"), @@ -332,18 +326,18 @@ def main(config_file, std_err=False, verbose=True, dont_send=False): ScriptConfiguration.get_val('warn_treshold'): msg.append('warninig threshold should be greater than critical treshold.') - #if there are problems with thresholds then there is no point in continuing: + # if there are problems with thresholds then there is no point in continuing: if msg: ScriptStatus.notify_immediate('unknown', "Configuration file contains errors: " + ','.join(msg)) sys.exit(1) - #Make sure that we are the only ones running on the server: + # Make sure that we are the only ones running on the server: ScriptLock.init(ScriptConfiguration.get_val('lockfile')) ScriptLock.aqquire() - #Initialize our repo mirror: + # Initialize our repo mirror: CertStore.initialize(host=ScriptConfiguration.get_val("repo_host"), port=ScriptConfiguration.get_val("repo_port"), pubkey=ScriptConfiguration.get_val('repo_pubkey'), @@ -399,7 +393,7 @@ def main(config_file, std_err=False, verbose=True, dont_send=False): ScriptStatus.notify_immediate('unknown', msg) sys.exit(1) except AssertionError as e: - #Unittest require it: + # Unittest require it: raise except Exception as e: msg = "Exception occured: {0}".format(e.__class__.__name__) From ec827ee7662dede1e11bc1e3a42495013cf210b7 Mon Sep 17 00:00:00 2001 From: Vespian Date: Sat, 17 May 2014 22:20:20 +0200 Subject: [PATCH 11/44] [doc] Moar! doc. Change-Id: I9e878cc94b0a9a0eec9571c9b039652b5f13c39d --- check_cert/__init__.py | 79 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 69 insertions(+), 10 deletions(-) diff --git a/check_cert/__init__.py b/check_cert/__init__.py index 4a34ce0..5283350 100755 --- a/check_cert/__init__.py +++ b/check_cert/__init__.py @@ -49,11 +49,25 @@ class PubkeySSHGitClient(SSHGitClient): """ - Simple class used to add pubkey authentication to the SSHGitClient class. + Connect to GIT repos using pubkey authentication. + + This simple class extends SSHGitClient class with pubkey authentication. In the base class it is not supported, and using password authentication for a script is insecure. """ def __init__(self, host, pubkey, port=None, username=None, *args, **kwargs): + """ + Initialize the class with authdata and call superclass constructor. + + Please see SSHGitClient's class constructor for a documentation of + arguments not mentioned here. + + Args: + host: host to connect to + pubkey: file path of the publickey to use + port: SSH port to connect to + username: username to use while connecting + """ self.host = host self.port = port self.pubkey = pubkey @@ -62,6 +76,10 @@ def __init__(self, host, pubkey, port=None, username=None, *args, **kwargs): self.alternative_paths = {} def _connect(self, cmd, path): + """ + Override connection establishment in SSHGitClient class so that pubkey + is used. + """ # FIXME: This has no way to deal with passphrases.. # FIXME: can we rely on ssh being in PATH here ? args = ['ssh', '-x', '-oStrictHostKeyChecking=no'] @@ -88,17 +106,27 @@ def _connect(self, cmd, path): class LocalMirrorRepo(Repo): + """ + Common GIT repo object extened with file searching capabilities. + """ def lookup_files(self, determine_wants, root_sha=None, repo_path=''): """ - Search the repo for files described by the determine_wants - function. The function itself operates on the file paths in a repo and - must return True for objects of interest. + Search the repo for files described by the determine_wants function. The search is done recursively, with each iteration scanning just one repo directory. In case a directory is found the root_sha and repo_path parameters are provided for a next iteration of the function. - The result is a list of the filenames accumulated by all iterations. + Args: + determine_wants: the function used to determine whether the file is + of interest. It operates on the file paths in a repo and must + return True for objects that match, False otherwise. + root_sha: sha of the tree object that search should be started from + repo_path: repo path of the tree object pointed by root_sha + + Returns: + The result is a list of the named tuples containing file paths and + their contents, accumulated by all recursive calls: """ file_list = [] if root_sha is None: @@ -133,8 +161,10 @@ def lookup_files(self, determine_wants, root_sha=None, repo_path=''): class CertStore(object): """ - Provides local clone of a remote repo plus some extra functionality to - ease extracting of the certificates from the repository + Provide local clone of a remote repo plus some extra functionality. + + Class is meant to be an abstraction of the GIT repos complexity, allowing + easy extraction of certificates. """ _remote = None _local = None @@ -142,6 +172,18 @@ class CertStore(object): @classmethod def initialize(cls, host, port, pubkey, username, repo_localdir, repo_url, repo_masterbranch): + """ + Initialize CertStore object. + + Args: + host: host to connect to + pubkey: file path of the publickey to use + port: SSH port to connect to + username: username to use while connecting + repo_localdir: path to use for local repo storage + repo_url: url of the repo to fetch + repo_masterbranch: git branch to fetch and scan + """ if cls._remote is None: cls._remote = PubkeySSHGitClient(host=host, pubkey=pubkey, @@ -170,9 +212,13 @@ def wants_master_only(refs): @classmethod def lookup_certs(cls, cert_suffixes): """ - Find all the certificates in the repository. The classification is made - by checking whether file suffix can be found in th list of certificate - suffixes found in cert_suffixes parameter. + Find all the certificates in the locally cached repository. + + The classification whether file is a certificate or not is made basing + on the file suffix. + + Args: + cert_suffixes: list of valid certificate suffixes """ if cls._local is None: raise RecoverableException("Local repo mirror has not been " + @@ -190,6 +236,9 @@ def wants_all_certs(path): def parse_command_line(): + """ + Convert command line arguments into script runtime configuration. + """ parser = argparse.ArgumentParser( description='Certificate checking tool', epilog="Author: vespian a t wp.pl", @@ -262,6 +311,16 @@ def get_cert_expiration(certificate, ignored_certs): def main(config_file, std_err=False, verbose=True, dont_send=False): + """ + Main function of the script + + Args: + config_file: file path of the config file to load + std_err: whether print logging output to stderr + verbose: whether to provide verbose logging messages + dont_send: whether to sent data to monitoring system or just do a dry + run + """ try: # Configure logging: fmt = logging.Formatter('%(filename)s[%(process)d] %(levelname)s: ' + From d28365e43a6b883ffead86b29b728767800b4e8b Mon Sep 17 00:00:00 2001 From: Vespian Date: Sat, 17 May 2014 23:08:59 +0200 Subject: [PATCH 12/44] Move ignored/unsupported cert support out of get_cert_expiration() Change-Id: I14a732c7cbfb3fa935185615b766cb99c2f271eb --- check_cert/__init__.py | 81 ++++++++++-------- .../moduletests/check_cert/test_check_cert.py | 84 +++++++++++++------ 2 files changed, 104 insertions(+), 61 deletions(-) diff --git a/check_cert/__init__.py b/check_cert/__init__.py index 5283350..7646954 100755 --- a/check_cert/__init__.py +++ b/check_cert/__init__.py @@ -276,38 +276,31 @@ def parse_command_line(): } -def get_cert_expiration(certificate, ignored_certs): +def get_cert_expiration(certificate): """ - Extract the certificate expiration date for a certificate blob. Handle - ignored certificates by comparing shasum of the blob with entries in the - ignored_certs list + Extract the certificate expiration date from a certificate blob. + + Args: + certificate: a named tuple object, containing path and content attributes + + Returns: + None if certificate was invalid or expiry date could not be extracted, + datetime object otherwise. """ - if certificate.path[-3:] in ['pem', 'crt', 'cer']: - try: - # Many bad things can happen here, but still - we can recover! :) - cert_hash = hashlib.sha1(certificate.content).hexdigest() - if cert_hash in ignored_certs: - # This cert should be ignored - logging.info("certificate {0} (sha1sum: {1})".format( - certificate.path, cert_hash) + " has been ignored.") - return None - # Workaround for -----BEGIN TRUSTED CERTIFICATE----- - if certificate.content.find('TRUSTED ') > -1: - logging.info("'TRUSTED' string has been removed from " + - "certificate {0} (sha1sum: {1})".format( - certificate.path, cert_hash)) - certificate.content = certificate.content.replace('TRUSTED ', - '') - cert_data = load_certificate(FILETYPE_PEM, certificate.content) - expiry_date = cert_data.get_notAfter() - # Return datetime object: - return datetime.strptime(expiry_date, '%Y%m%d%H%M%SZ') - except Exception: - return None - else: - logging.error("Certificate {0} ".format(certificate.path) + - "is of unsupported type.") - return None + try: + # Many bad things can happen here, but still - we can recover! :) + # Workaround for -----BEGIN TRUSTED CERTIFICATE----- + if certificate.content.find('TRUSTED ') > -1: + logging.info("'TRUSTED' string has been removed from " + + "certificate {0}".format(certificate.path)) + certificate.content = certificate.content.replace('TRUSTED ', + '') + cert_data = load_certificate(FILETYPE_PEM, certificate.content) + expiry_date = cert_data.get_notAfter() + # Return datetime object: + return datetime.strptime(expiry_date, '%Y%m%d%H%M%SZ') + except Exception: + raise RecoverableException() def main(config_file, std_err=False, verbose=True, dont_send=False): @@ -408,15 +401,33 @@ def main(config_file, std_err=False, verbose=True, dont_send=False): "repo_masterbranch"), ) + ignored_certs=ScriptConfiguration.get_val("ignored_certs") for cert in CertStore.lookup_certs(CERTIFICATE_EXTENSIONS): - cert_expiration = get_cert_expiration(cert, - ignored_certs=ScriptConfiguration.get_val( - "ignored_certs") - ) - if cert_expiration is None: + #Check whether the cert needs to be included in checks at all: + cert_hash = hashlib.sha1(cert.content).hexdigest() + if cert_hash in ignored_certs: + # This cert should be ignored + logging.info("certificate {0} (sha1sum: {1})".format( + cert.path, cert_hash) + " has been ignored.") + continue + + #Check if certifice type is supported: + if cert.path[-3:] not in ['pem', 'crt', 'cer']: + ScriptStatus.update('unknown', + "Certificate {0} ".format(cert.path) + + "is not supported by the check script, " + + "please add it to ignore list or upgrade " + + "the script.") + continue + + #Check the expiry date: + try: + cert_expiration = get_cert_expiration(cert) + except RecoverableException: ScriptStatus.update('unknown', "Script cannot parse certificate" + "{0}".format(cert.path)) continue + # -3 days is in fact -4 days, 23:59:58.817181 # so we compensate and round up # additionally, openssl uses utc dates diff --git a/test/moduletests/check_cert/test_check_cert.py b/test/moduletests/check_cert/test_check_cert.py index a626b3c..57c463e 100644 --- a/test/moduletests/check_cert/test_check_cert.py +++ b/test/moduletests/check_cert/test_check_cert.py @@ -102,8 +102,8 @@ def _script_conf_factory(self, **kwargs): "repo_pubkey": "./foo", "lockfile": "./fake_lock.pid", "ignored_certs": { - '42b270cbd03eaa8c16c386e66f910195f769f8b1': - "certificate used during unit-tests" + 'a69d081221a9caf21b1c18907c800528d6f414d2': + "sample/path/ignored_cert.pem" } } @@ -140,8 +140,6 @@ def setUpClass(cls): def test_cert_expiration_parsing(self, ScriptConfigurationMock, ScriptStatusMock, *unused): - IGNORED_CERTS = ['42b270cbd03eaa8c16c386e66f910195f769f8b1'] - # -3 days is in fact -4 days, 23:59:58.817181 # so we compensate and round up # additionally, openssl uses utc dates @@ -149,32 +147,27 @@ def test_cert_expiration_parsing(self, ScriptConfigurationMock, ScriptStatusMock # Test an expired certificate: cert = self._certpath2namedtuple(paths.EXPIRED_3_DAYS) - expiry_time = check_cert.get_cert_expiration(cert, IGNORED_CERTS) - now + expiry_time = check_cert.get_cert_expiration(cert) - now self.assertEqual(expiry_time.days, -3) - # Test an ignored certificate: - cert = self._certpath2namedtuple(paths.IGNORED_CERT) - expiry_time = check_cert.get_cert_expiration(cert, IGNORED_CERTS) - self.assertEqual(expiry_time, None) - # Test a good certificate: cert = self._certpath2namedtuple(paths.EXPIRE_21_DAYS) - expiry_time = check_cert.get_cert_expiration(cert, IGNORED_CERTS) - now + expiry_time = check_cert.get_cert_expiration(cert) - now self.assertEqual(expiry_time.days, 21) # Test a DER certificate: cert = self._certpath2namedtuple(paths.EXPIRE_41_DAYS_DER) - expiry_time = check_cert.get_cert_expiration(cert, IGNORED_CERTS) - self.assertIs(expiry_time, None) + with self.assertRaises(RecoverableException) as e: + expiry_time = check_cert.get_cert_expiration(cert) # Test a broken certificate: cert = self._certpath2namedtuple(paths.BROKEN_CERT) - expiry_time = check_cert.get_cert_expiration(cert, IGNORED_CERTS) - self.assertIs(expiry_time, None) + with self.assertRaises(RecoverableException) as e: + expiry_time = check_cert.get_cert_expiration(cert) # Test a "TRUSTED" certificate: cert = self._certpath2namedtuple(paths.TRUSTED_EXPIRE_41_CERT) - expiry_time = check_cert.get_cert_expiration(cert, IGNORED_CERTS) - now + expiry_time = check_cert.get_cert_expiration(cert) - now self.assertEqual(expiry_time.days, 41) @mock.patch('sys.exit') @@ -298,19 +291,58 @@ def terminate_script(exit_status): raise SystemExit(exit_status) SysExitMock.side_effect = terminate_script + #Fake configuration for the script: + ScriptConfigurationMock.get_val.side_effect = self._script_conf_factory() + # Provide fake data for the script: fake_cert_tuple = namedtuple("FileTuple", ['path', 'content']) - fake_cert_tuple.path = 'some_cert' + fake_cert_tuple.path = 'sample/path/sample_cert.pem' fake_cert_tuple.content = 'some content' + ignored_cert_tuple = namedtuple("FileTuple", ['path', 'content']) + ignored_cert_tuple.path = 'sample/path/ignored_cert.pem' + ignored_cert_tuple.content = 'some ignored content' + + unsupported_cert_tuple = namedtuple("FileTuple", ['path', 'content']) + unsupported_cert_tuple.path = 'sample/path/unsupported_cert.der' + unsupported_cert_tuple.content = 'some unsupported content' + + # simulate a git repo with an unsupported cert: def fake_cert(cert_extensions): - return iter([fake_cert_tuple]) + return iter([unsupported_cert_tuple]) CertStoreMock.lookup_certs.side_effect = fake_cert - ScriptConfigurationMock.get_val.side_effect = self._script_conf_factory() + # test if unsupported certificate is properly handled + with self.assertRaises(SystemExit) as e: + check_cert.main(config_file='./check_cert.conf') + self.assertEqual(e.exception.code, 0) + self.assertFalse(ScriptStatusMock.notify_immediate.called) + self.assertTrue(ScriptStatusMock.notify_agregated.called) + self.assertEqual(ScriptStatusMock.update.call_args[0][0], 'unknown') + ScriptStatusMock.reset_mock() + + # simulate a git repo with an ignored cert: + def fake_cert(cert_extensions): + return iter([ignored_cert_tuple]) + CertStoreMock.lookup_certs.side_effect = fake_cert + + # test if ignored certificate is properly handled: + with self.assertRaises(SystemExit) as e: + check_cert.main(config_file='./check_cert.conf') + self.assertEqual(e.exception.code, 0) + self.assertFalse(ScriptStatusMock.notify_immediate.called) + self.assertTrue(ScriptStatusMock.notify_agregated.called) + # All certs were ok, so a 'default' message should be send to Rieman + self.assertFalse(ScriptStatusMock.update.called) + ScriptStatusMock.reset_mock() + + # simulate a git repo with a valid certificate + def fake_cert(cert_extensions): + return iter([fake_cert_tuple]) + CertStoreMock.lookup_certs.side_effect = fake_cert # test if an expired cert is properly handled: - def fake_cert_expiration(cert, ignored_certs): + def fake_cert_expiration(cert): self.assertEqual(cert, fake_cert_tuple) return datetime.utcnow() - timedelta(days=4) CertExpirationMock.side_effect = fake_cert_expiration @@ -324,7 +356,7 @@ def fake_cert_expiration(cert, ignored_certs): ScriptStatusMock.reset_mock() # test if soon to expire ( Date: Mon, 19 May 2014 11:51:38 +0200 Subject: [PATCH 13/44] Add license preamble to run_tests.py Change-Id: I2ba850b9d09a6f63bc6ca96a791d2ee067e90a00 --- run_tests.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/run_tests.py b/run_tests.py index 8b4d90e..3374ff7 100755 --- a/run_tests.py +++ b/run_tests.py @@ -1,4 +1,20 @@ #!/usr/bin/python -tt +# -*- coding: utf-8 -*- +# Copyright (c) 2014 Pawel Rozlach +# Copyright (c) 2013 Pawel Rozlach +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + #Make it a bit more like python3: from __future__ import absolute_import From 49a2e1ebee9f5982f638cb211a41aee095c6cd8b Mon Sep 17 00:00:00 2001 From: Vespian Date: Mon, 19 May 2014 13:04:20 +0200 Subject: [PATCH 14/44] Fix hasbangs to be compatible with virtualenvs Change-Id: I1e3c661873095fceb57c6051806e169a1cf4acb8 --- run_tests.py | 2 +- test/modules/file_paths.py | 2 +- test/moduletests/check_cert/test_check_cert.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/run_tests.py b/run_tests.py index 3374ff7..aed9aff 100755 --- a/run_tests.py +++ b/run_tests.py @@ -1,4 +1,4 @@ -#!/usr/bin/python -tt +#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (c) 2014 Pawel Rozlach # Copyright (c) 2013 Pawel Rozlach diff --git a/test/modules/file_paths.py b/test/modules/file_paths.py index 02bbc03..e29ca25 100644 --- a/test/modules/file_paths.py +++ b/test/modules/file_paths.py @@ -1,4 +1,4 @@ -#!/usr/bin/python -tt +#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (c) 2013 Spotify AB # diff --git a/test/moduletests/check_cert/test_check_cert.py b/test/moduletests/check_cert/test_check_cert.py index 57c463e..2c9a1a7 100644 --- a/test/moduletests/check_cert/test_check_cert.py +++ b/test/moduletests/check_cert/test_check_cert.py @@ -1,4 +1,4 @@ -#!/usr/bin/python -tt +#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (c) 2013 Spotify AB # From fe59f69bc96076568fcd139b4805256892359f22 Mon Sep 17 00:00:00 2001 From: Pawel Rozlach Date: Mon, 19 May 2014 17:15:59 +0200 Subject: [PATCH 15/44] [doc] NRPE is not supported Change-Id: I78b1e054ceaef51a3404031902668c08b84bd618 --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 1a3b799..cac7f7d 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,7 @@ # _check_cert_ _check_cert is a certificate expiration check capable of scanning GIT repos -and sending data on expiring/expired certificates back to the monitoring system -(currently only Riemann)._ +and sending data on expiring/expired certificates back to the monitoring system._ ## Project Setup From bffbbc5d2fbc78d24524d1d0f0bf5dbcd6e53251 Mon Sep 17 00:00:00 2001 From: Vespian Date: Mon, 19 May 2014 21:12:03 +0200 Subject: [PATCH 16/44] [doc] Fix python dependencies Change-Id: I3921fc55c231c89e1be602031e25850803aa42e2 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cac7f7d..1ca26ba 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ In order to run check_certer you need to following dependencies installed: - argparse library - pyOpenSSL (https://launchpad.net/pyopenssl/) - pymisc (https://github.com/vespian/pymisc) -- python >=2.6 +- python 2.6 or 2.7 You can also use debian packaging rules from debian/ directory to build a deb package. From 427f2997127deabb535894e50b65bcf5e1b40282 Mon Sep 17 00:00:00 2001 From: Vespian Date: Tue, 20 May 2014 21:24:31 +0200 Subject: [PATCH 17/44] Fix licensing info Change-Id: I44a306d575ee0dd72e872309c7d3bfbf5f78939c --- debian/copyright | 2 +- test/moduletests/check_cert/test_check_cert.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/debian/copyright b/debian/copyright index 68e26fd..3ce2fd0 100644 --- a/debian/copyright +++ b/debian/copyright @@ -1,7 +1,7 @@ Format-Specification: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Files: * -Copyright: 2012-2013 Spotify AB +Copyright: 2013 Spotify AB, 2014 Pawel Rozlach License: Apache-2.0 On Debian systems, the full text of the Apache 2.0 license can be found in the file `/usr/share/common-licenses/Apache-2.0'. diff --git a/test/moduletests/check_cert/test_check_cert.py b/test/moduletests/check_cert/test_check_cert.py index 2c9a1a7..2381ede 100644 --- a/test/moduletests/check_cert/test_check_cert.py +++ b/test/moduletests/check_cert/test_check_cert.py @@ -1,5 +1,6 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +# Copyright (c) 2014 Pawel Rozlach # Copyright (c) 2013 Spotify AB # # Licensed under the Apache License, Version 2.0 (the "License"); you may not From d47cf800fd6cddfbe13f090b6636eee94603aabb Mon Sep 17 00:00:00 2001 From: Pawel Rozlach Date: Wed, 24 Sep 2014 15:49:29 +0100 Subject: [PATCH 18/44] Add *.swp and pybuild related stuff to gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index c13b8b0..39160a1 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ /build* check_cert.egg-info/* debian/files +*.swp +debian/check-cert* +.pybuild/ From 5d33ed3116e075869151b3e5377d2b4a097deb2a Mon Sep 17 00:00:00 2001 From: Pawel Rozlach Date: Wed, 24 Sep 2014 15:49:54 +0100 Subject: [PATCH 19/44] Switch to pybuild debian packaging --- debian/changelog | 8 ++++---- debian/control | 23 +++++++++++------------ debian/rules | 11 ++++++++--- 3 files changed, 23 insertions(+), 19 deletions(-) diff --git a/debian/changelog b/debian/changelog index df10399..92c9296 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,19 +1,19 @@ -check_cert (0.3.0) stable; urgency=low +check-cert (0.3.0) stable; urgency=low * Documentation refactoring * Make unittests nosetest compatible * splitting script into modules - -- Vespian Mon, 18 Sep 2013 14:33:43 +0000 + -- Pawel Rozlach Mon, 18 Sep 2013 14:33:43 +0000 -check_cert (0.2.0) stable; urgency=low +check-cert (0.2.0) stable; urgency=low * Git integration. * Drop scanning directories in favour of direct git interfacing. -- Pawel Rozlach Mon, 18 Sep 2013 14:33:43 +0000 -check_cert (0.1.0) unstable; urgency=low +check-cert (0.1.0) unstable; urgency=low * Initial release. diff --git a/debian/control b/debian/control index a09b77e..b68a263 100644 --- a/debian/control +++ b/debian/control @@ -1,18 +1,17 @@ -Source: check_cert +Source: check-cert Section: utils Priority: extra -Maintainer: Vespian -Build-Depends: python (>= 2.6.6-3~), debhelper (>= 8), python-dnspython, - python-coverage, openssl, python-openssl, python-bernhard, python-argparse, - python-protobuf, python-unittest2, python-yaml, python-dulwich (>= 0.8), - openssh-client -Standards-Version: 3.9.3 +Standards-Version: 3.9.4 +Maintainer: Pawel Rozlach +Homepage: https://github.com/vespian/check_cert +Build-Depends: debhelper (>= 8), dh-python, python-all (>= 2.6.6-3~), + python-setuptools, python-coverage, python-mock (>= 1.0), python-openssl, + python-protobuf, python-dulwich (>= 0.8), openssh-client, openssl, + python-pymisc, python-argparse X-Python-Version: >= 2.6 -Package: check_cert -Version: 0.2.0 +Package: check-cert Architecture: any -Depends: ${python:Depends}, python-openssl, python-bernhard, python-argparse, - python-protobuf, python-yaml, python-dulwich (>= 0.8), openssh-client, - python-dnspython +Depends: ${python:Depends}, ${misc:Depends}, python-openssl, + python-dulwich (>= 0.8), openssh-client, python-argparse, python-pymisc Description: Simple certificate check diff --git a/debian/rules b/debian/rules index ffa3e2c..b917a97 100755 --- a/debian/rules +++ b/debian/rules @@ -1,8 +1,13 @@ #!/usr/bin/make -f +#export DH_VERBOSE=1 +export PYBUILD_NAME=pymisc + %: - dh $@ --with python2 + dh $@ --with python2 --buildsystem=pybuild + +override_dh_auto_test: + PYBUILD_SYSTEM=custom \ + PYBUILD_TEST_ARGS="nosetests {dir}/" dh_auto_test -override_dh_fixperms: - dh_fixperms From 8d2fc55421733a2acf8ec62f41014a7e1417b574 Mon Sep 17 00:00:00 2001 From: Pawel Rozlach Date: Mon, 6 Oct 2014 15:19:59 +0200 Subject: [PATCH 20/44] Make PubkeySSHGitClient._connect check if pubkey exists --- check_cert/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/check_cert/__init__.py b/check_cert/__init__.py index 7646954..993484d 100755 --- a/check_cert/__init__.py +++ b/check_cert/__init__.py @@ -28,6 +28,7 @@ from collections import namedtuple from datetime import datetime, timedelta from dulwich.client import SSHGitClient, SubprocessWrapper, TraditionalGitClient +from dulwich.errors import GitProtocolError from dulwich.protocol import Protocol from dulwich.repo import Repo from pymisc.monitoring import ScriptStatus @@ -83,6 +84,8 @@ def _connect(self, cmd, path): # FIXME: This has no way to deal with passphrases.. # FIXME: can we rely on ssh being in PATH here ? args = ['ssh', '-x', '-oStrictHostKeyChecking=no'] + if not (os.path.exists(self.pubkey) and os.access(self.pubkey, os.R_OK)): + raise GitProtocolError("Public key file is missing or incaccesible") args.extend(['-i', self.pubkey]) if self.port is not None: args.extend(['-p', str(self.port)]) From 72c841bc9d72557afc7a1fe3ac9a100d4162d29a Mon Sep 17 00:00:00 2001 From: Pawel Rozlach Date: Mon, 6 Oct 2014 16:31:33 +0200 Subject: [PATCH 21/44] Logging fixes --- check_cert/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/check_cert/__init__.py b/check_cert/__init__.py index 993484d..a17a30b 100755 --- a/check_cert/__init__.py +++ b/check_cert/__init__.py @@ -99,8 +99,8 @@ def _connect(self, cmd, path): stdin=subprocess.PIPE, stdout=subprocess.PIPE) con = SubprocessWrapper(proc) - logging.info("Connected to repo {0}:{1} via ssh".format(self.host, - self.port if self.port else 22)) + logging.info("Connected to repo {0}:{1} via ssh, cmd: {2}".format( + self.host, self.port if self.port else 22, cmd)) return (Protocol(con.read, con.write, report_activity=self._report_activity @@ -427,7 +427,7 @@ def main(config_file, std_err=False, verbose=True, dont_send=False): try: cert_expiration = get_cert_expiration(cert) except RecoverableException: - ScriptStatus.update('unknown', "Script cannot parse certificate" + + ScriptStatus.update('unknown', "Script cannot parse certificate: " + "{0}".format(cert.path)) continue @@ -469,6 +469,6 @@ def main(config_file, std_err=False, verbose=True, dont_send=False): # Unittest require it: raise except Exception as e: - msg = "Exception occured: {0}".format(e.__class__.__name__) + msg = "Exception occured: {0}, msg: {1}".format(e.__class__.__name__, str(e)) logging.error(msg) sys.exit(1) From 6fc77dda4f9ecd1238eab4bb14f1fc20172543ef Mon Sep 17 00:00:00 2001 From: Pawel Rozlach Date: Mon, 6 Oct 2014 16:32:28 +0200 Subject: [PATCH 22/44] Provide some sane defaults for some config params --- check_cert/__init__.py | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/check_cert/__init__.py b/check_cert/__init__.py index a17a30b..7fc33ae 100755 --- a/check_cert/__init__.py +++ b/check_cert/__init__.py @@ -344,10 +344,21 @@ def main(config_file, std_err=False, verbose=True, dont_send=False): # discard messages ScriptConfiguration.load_config(config_file) + # Provide some sane default: + try: + repo_port = ScriptConfiguration.get_val("repo_port") + except KeyError: + repo_port = 22 + + try: + ignored_certs = ScriptConfiguration.get_val("ignored_certs") + except KeyError: + ignored_certs = {} + logger.debug("Remote repo is is: {0}@{1}:{2}{3}->{4}".format( ScriptConfiguration.get_val("repo_user"), ScriptConfiguration.get_val("repo_host"), - ScriptConfiguration.get_val("repo_port"), + repo_port, ScriptConfiguration.get_val("repo_url"), ScriptConfiguration.get_val("repo_masterbranch")) + ", local repository dir is {0}".format( @@ -361,15 +372,16 @@ def main(config_file, std_err=False, verbose=True, dont_send=False): ) # Initialize Riemann reporting: - ScriptStatus.initialize( - riemann_enabled=ScriptConfiguration.get_val("riemann_enabled"), - riemann_hosts_config=ScriptConfiguration.get_val("riemann_hosts"), - riemann_tags=ScriptConfiguration.get_val("riemann_tags"), - riemann_ttl=ScriptConfiguration.get_val("riemann_ttl"), - riemann_service_name=SERVICE_NAME, - nrpe_enabled=ScriptConfiguration.get_val("nrpe_enabled"), - debug=dont_send, - ) + if ScriptConfiguration.get_val("riemann_enabled") is True: + ScriptStatus.initialize( + riemann_enabled=ScriptConfiguration.get_val("riemann_enabled"), + riemann_hosts_config=ScriptConfiguration.get_val("riemann_hosts"), + riemann_tags=ScriptConfiguration.get_val("riemann_tags"), + riemann_ttl=ScriptConfiguration.get_val("riemann_ttl"), + riemann_service_name=SERVICE_NAME, + nrpe_enabled=ScriptConfiguration.get_val("nrpe_enabled"), + debug=dont_send, + ) # verify the configuration msg = [] @@ -394,7 +406,7 @@ def main(config_file, std_err=False, verbose=True, dont_send=False): # Initialize our repo mirror: CertStore.initialize(host=ScriptConfiguration.get_val("repo_host"), - port=ScriptConfiguration.get_val("repo_port"), + port=repo_port, pubkey=ScriptConfiguration.get_val('repo_pubkey'), username=ScriptConfiguration.get_val("repo_user"), repo_localdir=ScriptConfiguration.get_val( @@ -404,7 +416,6 @@ def main(config_file, std_err=False, verbose=True, dont_send=False): "repo_masterbranch"), ) - ignored_certs=ScriptConfiguration.get_val("ignored_certs") for cert in CertStore.lookup_certs(CERTIFICATE_EXTENSIONS): #Check whether the cert needs to be included in checks at all: cert_hash = hashlib.sha1(cert.content).hexdigest() From d3696f94379d126213880bde2aea5f261b2c5fe8 Mon Sep 17 00:00:00 2001 From: Pawel Rozlach Date: Mon, 6 Oct 2014 16:32:43 +0200 Subject: [PATCH 23/44] Formatting fixes --- check_cert/__init__.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/check_cert/__init__.py b/check_cert/__init__.py index 7fc33ae..e1d7658 100755 --- a/check_cert/__init__.py +++ b/check_cert/__init__.py @@ -417,7 +417,7 @@ def main(config_file, std_err=False, verbose=True, dont_send=False): ) for cert in CertStore.lookup_certs(CERTIFICATE_EXTENSIONS): - #Check whether the cert needs to be included in checks at all: + # Check whether the cert needs to be included in checks at all: cert_hash = hashlib.sha1(cert.content).hexdigest() if cert_hash in ignored_certs: # This cert should be ignored @@ -425,7 +425,7 @@ def main(config_file, std_err=False, verbose=True, dont_send=False): cert.path, cert_hash) + " has been ignored.") continue - #Check if certifice type is supported: + # Check if certifice type is supported: if cert.path[-3:] not in ['pem', 'crt', 'cer']: ScriptStatus.update('unknown', "Certificate {0} ".format(cert.path) + @@ -434,7 +434,7 @@ def main(config_file, std_err=False, verbose=True, dont_send=False): "the script.") continue - #Check the expiry date: + # Check the expiry date: try: cert_expiration = get_cert_expiration(cert) except RecoverableException: @@ -450,19 +450,19 @@ def main(config_file, std_err=False, verbose=True, dont_send=False): if time_left.days < 0: ScriptStatus.update('critical', "Certificate {0} expired {1} days ago.".format( - cert.path, abs(time_left.days))) + cert.path, abs(time_left.days))) elif time_left.days == 0: ScriptStatus.update('critical', "Certificate {0} expires today.".format( - cert.path)) + cert.path)) elif time_left.days < ScriptConfiguration.get_val("critical_treshold"): ScriptStatus.update('critical', "Certificate {0} is about to expire in {1} days.".format( - cert.path, time_left.days)) + cert.path, time_left.days)) elif time_left.days < ScriptConfiguration.get_val("warn_treshold"): ScriptStatus.update('warn', "Certificate {0} is about to expire in {1} days.".format( - cert.path, time_left.days)) + cert.path, time_left.days)) else: logger.info("{0} expires in {1} days - OK!".format( cert.path, time_left.days)) From eca77083e823ea67f86d9b1fb1dd41c32999a8ee Mon Sep 17 00:00:00 2001 From: Pawel Rozlach Date: Mon, 6 Oct 2014 16:36:38 +0200 Subject: [PATCH 24/44] Depend on pymisc >= 1.2.0 --- debian/control | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/debian/control b/debian/control index b68a263..f7b4c4f 100644 --- a/debian/control +++ b/debian/control @@ -7,11 +7,12 @@ Homepage: https://github.com/vespian/check_cert Build-Depends: debhelper (>= 8), dh-python, python-all (>= 2.6.6-3~), python-setuptools, python-coverage, python-mock (>= 1.0), python-openssl, python-protobuf, python-dulwich (>= 0.8), openssh-client, openssl, - python-pymisc, python-argparse + python-pymisc (>= 1.2.0), python-argparse X-Python-Version: >= 2.6 Package: check-cert Architecture: any Depends: ${python:Depends}, ${misc:Depends}, python-openssl, - python-dulwich (>= 0.8), openssh-client, python-argparse, python-pymisc + python-dulwich (>= 0.8), openssh-client, python-argparse, + python-pymisc (>= 1.2.0) Description: Simple certificate check From 36ab8029839d9535ec843617c44d6c4f8cf444c0 Mon Sep 17 00:00:00 2001 From: Pawel Rozlach Date: Mon, 6 Oct 2014 16:40:02 +0200 Subject: [PATCH 25/44] Depend on python-dulwich >=0.9.7 --- debian/control | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debian/control b/debian/control index f7b4c4f..23c9c95 100644 --- a/debian/control +++ b/debian/control @@ -6,7 +6,7 @@ Maintainer: Pawel Rozlach Homepage: https://github.com/vespian/check_cert Build-Depends: debhelper (>= 8), dh-python, python-all (>= 2.6.6-3~), python-setuptools, python-coverage, python-mock (>= 1.0), python-openssl, - python-protobuf, python-dulwich (>= 0.8), openssh-client, openssl, + python-protobuf, python-dulwich (>= 0.9.7), openssh-client, openssl, python-pymisc (>= 1.2.0), python-argparse X-Python-Version: >= 2.6 From 0ce5fc74ce9a11d94a09b64e8bc019ef4f51e6df Mon Sep 17 00:00:00 2001 From: Pawel Rozlach Date: Mon, 6 Oct 2014 16:35:08 +0200 Subject: [PATCH 26/44] Version bump --- debian/changelog | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/debian/changelog b/debian/changelog index 92c9296..5320cb5 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,11 @@ +check-cert (0.3.1) stable; urgency=low + + * Small fixes + * formatting + * bump dependency on pymisc + + -- Pawel Rozlach Mon, 06 Oct 2014 16:33:08 +0200 + check-cert (0.3.0) stable; urgency=low * Documentation refactoring From 9ac89c82475093c42ce44ef6628d6618fa257f72 Mon Sep 17 00:00:00 2001 From: Pawel Rozlach Date: Mon, 6 Oct 2014 16:42:13 +0100 Subject: [PATCH 27/44] Fix dulwich dependencies --- debian/changelog | 6 ++++++ debian/control | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/debian/changelog b/debian/changelog index 5320cb5..d5c1983 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +check-cert (0.3.1+1) UNRELEASED; urgency=low + + * Fix dependencies for dulwich. + + -- vespian Mon, 06 Oct 2014 16:31:07 +0100 + check-cert (0.3.1) stable; urgency=low * Small fixes diff --git a/debian/control b/debian/control index 23c9c95..3390617 100644 --- a/debian/control +++ b/debian/control @@ -13,6 +13,6 @@ X-Python-Version: >= 2.6 Package: check-cert Architecture: any Depends: ${python:Depends}, ${misc:Depends}, python-openssl, - python-dulwich (>= 0.8), openssh-client, python-argparse, + python-dulwich (>= 0.9.7), openssh-client, python-argparse, python-pymisc (>= 1.2.0) Description: Simple certificate check From 8ecfa7acf16e4187967d7940dd6fafb762802035 Mon Sep 17 00:00:00 2001 From: Pawel Rozlach Date: Mon, 6 Oct 2014 20:22:51 +0200 Subject: [PATCH 28/44] Provide some info about problems with Dulwich lib --- README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1ca26ba..f46c01f 100644 --- a/README.md +++ b/README.md @@ -5,17 +5,25 @@ and sending data on expiring/expired certificates back to the monitoring system. ## Project Setup -In order to run check_certer you need to following dependencies installed: +In order to run check_cert you need to have following dependencies installed: - Dulwich - python implementation of GIT (https://www.samba.org/~jelmer/dulwich/docs/) - *ssh* command in your PATH - argparse library - pyOpenSSL (https://launchpad.net/pyopenssl/) - pymisc (https://github.com/vespian/pymisc) - python 2.6 or 2.7 +- dulwich library You can also use debian packaging rules from debian/ directory to build a deb package. +Unfortunatelly, dulwich library is broken on wheezy: + +https://bugs.launchpad.net/dulwich/+bug/1326213 + +so the script depends on the newest version (0.9.7) even though 0.8.5 is +sufficient when it comes to functionality. + ## Usage ### Configuration From f21fdbbe83f4e45aed8e7edfba20c185d581920d Mon Sep 17 00:00:00 2001 From: Pawel Rozlach Date: Mon, 6 Oct 2014 20:24:28 +0200 Subject: [PATCH 29/44] Refactor configuration checking --- check_cert/__init__.py | 83 ++++++++++++++---- .../moduletests/check_cert/test_check_cert.py | 87 ++++++++++++------- 2 files changed, 122 insertions(+), 48 deletions(-) diff --git a/check_cert/__init__.py b/check_cert/__init__.py index e1d7658..bd0c0a9 100755 --- a/check_cert/__init__.py +++ b/check_cert/__init__.py @@ -38,6 +38,7 @@ import logging import logging.handlers as lh import os +import re import subprocess import sys @@ -306,6 +307,54 @@ def get_cert_expiration(certificate): raise RecoverableException() +def _verify_conf(conf_hash): + """ + Check if script configuration is sane. + + This function takes care of checking if the script configuration is + logically correct. + + Args: + conf_hash: A hash containing whole configuration, as defined in config + file. + + Returns: + A list of errors/issues found in the configuration, or an empty list + if the configuration is OK. + """ + + msg = [] + + try: + warn_treshold = conf_hash['warn_treshold'] + critical_treshold = conf_hash['critical_treshold'] + repo_host = conf_hash['repo_host'] + repo_url = conf_hash['repo_url'] + repo_masterbranch = conf_hash['repo_masterbranch'] + repo_localdir = conf_hash['repo_localdir'] + repo_user = conf_hash['repo_user'] + repo_pubkey = conf_hash['repo_pubkey'] + lockfile = conf_hash['lockfile'] + except KeyError as e: + msg.append('Mandatory parameter is missing: {0}'.format(str(e))) + + # Verify thresholds: + if warn_treshold <= 0: + msg.append('Certificate expiration warn threshold should be > 0.') + if critical_treshold <= 0: + msg.append('Certificate expiration critical threshold should be > 0.') + if critical_treshold >= warn_treshold: + msg.append('Warninig threshold should be greater than critical treshold.') + + # repo_host + if not re.match(r'^(([a-z0-9]\-*[a-z0-9]*){1,63}\.?){1,255}$', repo_host): + msg.append('Repo host {0} is not a valid domain name.'.format(repo_host)) + + # FIXME - add verification of other command line parameters + + return msg + + def main(config_file, std_err=False, verbose=True, dont_send=False): """ Main function of the script @@ -371,7 +420,7 @@ def main(config_file, std_err=False, verbose=True, dont_send=False): ScriptConfiguration.get_val('critical_treshold')) ) - # Initialize Riemann reporting: + # Initialize Riemann/NRPE reporting: if ScriptConfiguration.get_val("riemann_enabled") is True: ScriptStatus.initialize( riemann_enabled=ScriptConfiguration.get_val("riemann_enabled"), @@ -380,25 +429,23 @@ def main(config_file, std_err=False, verbose=True, dont_send=False): riemann_ttl=ScriptConfiguration.get_val("riemann_ttl"), riemann_service_name=SERVICE_NAME, nrpe_enabled=ScriptConfiguration.get_val("nrpe_enabled"), - debug=dont_send, - ) - - # verify the configuration - msg = [] - if ScriptConfiguration.get_val('warn_treshold') <= 0: - msg.append('certificate expiration warn threshold should be > 0.') - if ScriptConfiguration.get_val('critical_treshold') <= 0: - msg.append('certificate expiration critical threshold should be > 0.') - if ScriptConfiguration.get_val('critical_treshold') >= \ - ScriptConfiguration.get_val('warn_treshold'): - msg.append('warninig threshold should be greater than critical treshold.') - - # if there are problems with thresholds then there is no point in continuing: - if msg: + debug=dont_send,) + else: + ScriptStatus.initialize( + nrpe_enabled=ScriptConfiguration.get_val("nrpe_enabled"), + debug=dont_send,) + + # Now, let's verify the configuration: + # FIXME - ScriptStatus might have been already initialized with + # incorrect config and in effect ScriptStatus.notify_immediate will + # not reach monitoring system + conf_issues = _verify_conf(ScriptConfiguration.get_config()) + if conf_issues: + logging.debug("Configuration problems:\n\t" + + '\n\t'.join(conf_issues)) ScriptStatus.notify_immediate('unknown', "Configuration file contains errors: " + - ','.join(msg)) - sys.exit(1) + ' '.join(conf_issues)) # Make sure that we are the only ones running on the server: ScriptLock.init(ScriptConfiguration.get_val('lockfile')) diff --git a/test/moduletests/check_cert/test_check_cert.py b/test/moduletests/check_cert/test_check_cert.py index 2381ede..489bbd5 100644 --- a/test/moduletests/check_cert/test_check_cert.py +++ b/test/moduletests/check_cert/test_check_cert.py @@ -108,13 +108,17 @@ def _script_conf_factory(self, **kwargs): } } - def func(key): - config = good_configuration.copy() - config.update(kwargs) + config = good_configuration.copy() + config.update(kwargs) + + def get_val(key): self.assertIn(key, config) return config[key] - return func + def get_config(): + return config + + return get_val, get_config @staticmethod def _certpath2namedtuple(path): @@ -158,12 +162,12 @@ def test_cert_expiration_parsing(self, ScriptConfigurationMock, ScriptStatusMock # Test a DER certificate: cert = self._certpath2namedtuple(paths.EXPIRE_41_DAYS_DER) - with self.assertRaises(RecoverableException) as e: + with self.assertRaises(RecoverableException): expiry_time = check_cert.get_cert_expiration(cert) # Test a broken certificate: cert = self._certpath2namedtuple(paths.BROKEN_CERT) - with self.assertRaises(RecoverableException) as e: + with self.assertRaises(RecoverableException): expiry_time = check_cert.get_cert_expiration(cert) # Test a "TRUSTED" certificate: @@ -211,7 +215,9 @@ def test_script_init(self, CertStoreMock, SysExitMock, Test if script initializes its dependencies properly """ - ScriptConfigurationMock.get_val.side_effect = self._script_conf_factory() + ScriptConfigurationMock.get_val.side_effect, \ + ScriptConfigurationMock.get_config.side_effect = \ + self._script_conf_factory() check_cert.main(config_file='./check_cert.conf') @@ -247,38 +253,54 @@ def test_sanity_checking(self, CertStoreMock, CertExpirationMock, SysExitMock, ScriptConfigurationMock, ScriptStatusMock, *unused): - def terminate_script(exit_status): - raise SystemExit(exit_status) - SysExitMock.side_effect = terminate_script + # Hack, hack, hack - we terminate script after a call to + # notify_immediate with a non-standard exit code hoping + # that it will be uniq enough to differentiate other + # errors + def terminate_script(*unused): + raise SystemExit(216) + ScriptStatusMock.notify_immediate.side_effect = terminate_script # Test if ScriptStatus gets properly initialized # and whether warn > crit condition is # checked as well - ScriptConfigurationMock.get_val.side_effect = \ - self._script_conf_factory(warn_treshold=7, critical_treshold=15) - + ScriptConfigurationMock.get_val.side_effect, \ + ScriptConfigurationMock.get_config.side_effect = \ + self._script_conf_factory(warn_treshold=7, + critical_treshold=15) with self.assertRaises(SystemExit) as e: check_cert.main(config_file='./check_cert.conf') - self.assertEqual(e.exception.code, 1) + self.assertEqual(e.exception.code, 216) + self.assertTrue(ScriptStatusMock.notify_immediate.called) + self.assertEqual(ScriptStatusMock.notify_immediate.call_args[0][0], + 'unknown') + ScriptStatusMock.notify_immediate.reset_mock() # this time test only the negative warn threshold: - check_cert.ScriptConfiguration.get_val.side_effect = \ + ScriptConfigurationMock.get_val.side_effect, \ + ScriptConfigurationMock.get_config.side_effect = \ self._script_conf_factory(warn_treshold=-30) - ScriptStatusMock.notify_immediate.reset_mock() + ScriptStatusMock.notify_immediate.side_effect = terminate_script with self.assertRaises(SystemExit) as e: check_cert.main(config_file='./check_cert.conf') + self.assertEqual(e.exception.code, 216) self.assertTrue(ScriptStatusMock.notify_immediate.called) - self.assertEqual(e.exception.code, 1) + self.assertEqual(ScriptStatusMock.notify_immediate.call_args[0][0], + 'unknown') + ScriptStatusMock.notify_immediate.reset_mock() # this time test only the crit threshold == 0 condition check: - check_cert.ScriptConfiguration.get_val.side_effect = \ + ScriptConfigurationMock.get_val.side_effect, \ + ScriptConfigurationMock.get_config.side_effect = \ self._script_conf_factory(critical_treshold=-1) - - ScriptStatusMock.notify_immediate.reset_mock() + ScriptStatusMock.notify_immediate.side_effect = terminate_script with self.assertRaises(SystemExit) as e: check_cert.main(config_file='./check_cert.conf') + self.assertEqual(e.exception.code, 216) self.assertTrue(ScriptStatusMock.notify_immediate.called) - self.assertEqual(e.exception.code, 1) + self.assertEqual(ScriptStatusMock.notify_immediate.call_args[0][0], + 'unknown') + ScriptStatusMock.notify_immediate.reset_mock() @mock.patch('check_cert.sys.exit') @mock.patch('check_cert.get_cert_expiration') @@ -292,8 +314,10 @@ def terminate_script(exit_status): raise SystemExit(exit_status) SysExitMock.side_effect = terminate_script - #Fake configuration for the script: - ScriptConfigurationMock.get_val.side_effect = self._script_conf_factory() + # Fake configuration for the script: + ScriptConfigurationMock.get_val.side_effect, \ + ScriptConfigurationMock.get_config.side_effect = \ + self._script_conf_factory() # Provide fake data for the script: fake_cert_tuple = namedtuple("FileTuple", ['path', 'content']) @@ -426,18 +450,20 @@ def fake_cert_expiration(cert): @mock.patch('check_cert.get_cert_expiration') @mock.patch('check_cert.CertStore') def test_exception_handling(self, CertStoreMock, CertExpirationMock, - SysExitMock, ScriptConfigurationMock, - ScriptStatusMock, ScriptLockMock, - LoggingErrorMock, LoggingInfoMock, - LoggingWarnMock): + SysExitMock, ScriptConfigurationMock, + ScriptStatusMock, ScriptLockMock, + LoggingErrorMock, LoggingInfoMock, + LoggingWarnMock): # A bit of a workaround, but we cannot simply call sys.exit def terminate_script(exit_status): raise SystemExit(exit_status) SysExitMock.side_effect = terminate_script - #Provide some sane configuration: - ScriptConfigurationMock.get_val.side_effect = self._script_conf_factory() + # Provide some sane configuration: + ScriptConfigurationMock.get_val.side_effect, \ + ScriptConfigurationMock.get_config.side_effect = \ + self._script_conf_factory() # Test an exception from which we can recover: def throw_test_exception(cert_extensions): @@ -449,7 +475,8 @@ def throw_test_exception(cert_extensions): self.assertEqual(e.exception.code, 1) self.assertTrue(LoggingErrorMock.called) self.assertTrue(ScriptStatusMock.notify_immediate.called) - self.assertEqual(ScriptStatusMock.notify_immediate.call_args[0][0], 'unknown') + self.assertEqual(ScriptStatusMock.notify_immediate.call_args[0][0], + 'unknown') ScriptStatusMock.reset_mock() # Test an fatal exception From 44061dec870336aefc7af6930d950469a994fd16 Mon Sep 17 00:00:00 2001 From: Pawel Rozlach Date: Mon, 6 Oct 2014 20:24:53 +0200 Subject: [PATCH 30/44] Make script a bit more user-friendly --- check_cert/__init__.py | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/check_cert/__init__.py b/check_cert/__init__.py index bd0c0a9..07c42ee 100755 --- a/check_cert/__init__.py +++ b/check_cert/__init__.py @@ -393,7 +393,11 @@ def main(config_file, std_err=False, verbose=True, dont_send=False): # discard messages ScriptConfiguration.load_config(config_file) - # Provide some sane default: + logger.debug("Loaded configuration: " + + str(ScriptConfiguration.get_config()) + ) + + # Provide some sane defaults: try: repo_port = ScriptConfiguration.get_val("repo_port") except KeyError: @@ -404,7 +408,7 @@ def main(config_file, std_err=False, verbose=True, dont_send=False): except KeyError: ignored_certs = {} - logger.debug("Remote repo is is: {0}@{1}:{2}{3}->{4}".format( + logger.debug("Remote repo is: {0}@{1}{3}->{4}, tcp port {2}".format( ScriptConfiguration.get_val("repo_user"), ScriptConfiguration.get_val("repo_host"), repo_port, @@ -423,7 +427,7 @@ def main(config_file, std_err=False, verbose=True, dont_send=False): # Initialize Riemann/NRPE reporting: if ScriptConfiguration.get_val("riemann_enabled") is True: ScriptStatus.initialize( - riemann_enabled=ScriptConfiguration.get_val("riemann_enabled"), + riemann_enabled=True, riemann_hosts_config=ScriptConfiguration.get_val("riemann_hosts"), riemann_tags=ScriptConfiguration.get_val("riemann_tags"), riemann_ttl=ScriptConfiguration.get_val("riemann_ttl"), @@ -463,6 +467,8 @@ def main(config_file, std_err=False, verbose=True, dont_send=False): "repo_masterbranch"), ) + unparsable_certs = {"number": 0, "paths": []} + for cert in CertStore.lookup_certs(CERTIFICATE_EXTENSIONS): # Check whether the cert needs to be included in checks at all: cert_hash = hashlib.sha1(cert.content).hexdigest() @@ -485,8 +491,8 @@ def main(config_file, std_err=False, verbose=True, dont_send=False): try: cert_expiration = get_cert_expiration(cert) except RecoverableException: - ScriptStatus.update('unknown', "Script cannot parse certificate: " + - "{0}".format(cert.path)) + unparsable_certs["number"] += 1 + unparsable_certs["paths"].append(cert.path) continue # -3 days is in fact -4 days, 23:59:58.817181 @@ -504,16 +510,28 @@ def main(config_file, std_err=False, verbose=True, dont_send=False): cert.path)) elif time_left.days < ScriptConfiguration.get_val("critical_treshold"): ScriptStatus.update('critical', - "Certificate {0} is about to expire in {1} days.".format( - cert.path, time_left.days)) + "Certificate {0} is about to expire in" + "{0} days.".format(cert.path, time_left.days)) elif time_left.days < ScriptConfiguration.get_val("warn_treshold"): ScriptStatus.update('warn', - "Certificate {0} is about to expire in {1} days.".format( - cert.path, time_left.days)) + "Certificate {0} is about to expire in" + "{0} days.".format(cert.path, time_left.days)) else: logger.info("{0} expires in {1} days - OK!".format( cert.path, time_left.days)) + # We do not want to pollute output in case when there are too many broken + # certs in the report. + if unparsable_certs["number"] > 0: + if unparsable_certs["number"] <= 2: + ScriptStatus.update('unknown', + 'Script cannot parse certificates: ' + ','.join(unparsable_certs["paths"])) + else: + ScriptStatus.update('unknown', 'Script cannot parse {0} '.format( + unparsable_certs["number"]) + + "certificates, please check with verbose out on") + ScriptStatus.notify_agregated() ScriptLock.release() sys.exit(0) From 0f26d92631c68977050912cdd1171a892ae034af Mon Sep 17 00:00:00 2001 From: Pawel Rozlach Date: Mon, 6 Oct 2014 20:27:00 +0200 Subject: [PATCH 31/44] Changelog bump --- debian/changelog | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/debian/changelog b/debian/changelog index d5c1983..330ee2e 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,14 @@ +check-cert (0.3.2) UNRELEASED; urgency=low + + * Fixes, config checking refactoring + + -- vespian Mon, 06 Oct 2014 21:31:07 +0100 + check-cert (0.3.1+1) UNRELEASED; urgency=low * Fix dependencies for dulwich. - -- vespian Mon, 06 Oct 2014 16:31:07 +0100 + -- vespian Mon, 06 Oct 2014 16:31:07 +0100 check-cert (0.3.1) stable; urgency=low From f0a729005501e0aabb1cadd24128b2104e8e839a Mon Sep 17 00:00:00 2001 From: Pawel Rozlach Date: Mon, 6 Oct 2014 19:32:44 +0100 Subject: [PATCH 32/44] Bump dependencies on pymisc --- debian/control | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/debian/control b/debian/control index 3390617..3bdc664 100644 --- a/debian/control +++ b/debian/control @@ -7,12 +7,12 @@ Homepage: https://github.com/vespian/check_cert Build-Depends: debhelper (>= 8), dh-python, python-all (>= 2.6.6-3~), python-setuptools, python-coverage, python-mock (>= 1.0), python-openssl, python-protobuf, python-dulwich (>= 0.9.7), openssh-client, openssl, - python-pymisc (>= 1.2.0), python-argparse + python-pymisc (>= 1.2.1), python-argparse X-Python-Version: >= 2.6 Package: check-cert Architecture: any Depends: ${python:Depends}, ${misc:Depends}, python-openssl, python-dulwich (>= 0.9.7), openssh-client, python-argparse, - python-pymisc (>= 1.2.0) + python-pymisc (>= 1.2.1) Description: Simple certificate check From 91a554f8d06164b37196073e959198b93c539923 Mon Sep 17 00:00:00 2001 From: Vespian Date: Thu, 1 Jan 2015 23:52:44 +0100 Subject: [PATCH 33/44] Unittests refactoring Change-Id: I8faa367dd03c5871acb346d15ed7604bb1bf4f65 --- .../moduletests/check_cert/test_check_cert.py | 524 +++++++++--------- 1 file changed, 275 insertions(+), 249 deletions(-) diff --git a/test/moduletests/check_cert/test_check_cert.py b/test/moduletests/check_cert/test_check_cert.py index 489bbd5..76711f0 100644 --- a/test/moduletests/check_cert/test_check_cert.py +++ b/test/moduletests/check_cert/test_check_cert.py @@ -50,10 +50,7 @@ @mock.patch('logging.warn') @mock.patch('logging.info') @mock.patch('logging.error') -@mock.patch('check_cert.ScriptLock', autospec=True) -@mock.patch('check_cert.ScriptStatus', autospec=True) -@mock.patch('check_cert.ScriptConfiguration', autospec=True) -class TestCheckCert(unittest.TestCase): +class TestCertificateParsing(unittest.TestCase): @staticmethod def _create_test_cert(days, path, is_der=False): openssl_cmd = ["/usr/bin/openssl", "req", "-x509", "-nodes", @@ -80,46 +77,6 @@ def _create_test_cert(days, path, is_der=False): else: print("Created test certificate {0}".format(os.path.basename(path))) - def _script_conf_factory(self, **kwargs): - """ - Provide fake configuration data objects. - """ - good_configuration = {"warn_treshold": 30, - "critical_treshold": 15, - "nrpe_enabled": True, - "riemann_enabled": True, - "riemann_hosts": { - 'static': ['1.2.3.4:1:udp', - '2.3.4.5:5555:tcp', ] - }, - "riemann_tags": ["abc", "def"], - "riemann_ttl": 60, - "repo_host": "git.foo.net", - "repo_port": 22, - "repo_url": "/foo-puppet", - "repo_masterbranch": "refs/heads/foo", - "repo_localdir": "/tmp/foo", - "repo_user": "foo", - "repo_pubkey": "./foo", - "lockfile": "./fake_lock.pid", - "ignored_certs": { - 'a69d081221a9caf21b1c18907c800528d6f414d2': - "sample/path/ignored_cert.pem" - } - } - - config = good_configuration.copy() - config.update(kwargs) - - def get_val(key): - self.assertIn(key, config) - return config[key] - - def get_config(): - return config - - return get_val, get_config - @staticmethod def _certpath2namedtuple(path): with open(path, 'rb') as fh: @@ -143,42 +100,52 @@ def setUpClass(cls): print(line.replace('-----BEGIN CERTIFICATE-----', '-----BEGIN TRUSTED CERTIFICATE-----'), end="") - def test_cert_expiration_parsing(self, ScriptConfigurationMock, ScriptStatusMock, - *unused): + def setUp(self): # -3 days is in fact -4 days, 23:59:58.817181 # so we compensate and round up # additionally, openssl uses utc dates - now = datetime.utcnow() - timedelta(days=1) + self.now = datetime.utcnow() - timedelta(days=1) + def test_expired_cert(self, *unused): # Test an expired certificate: cert = self._certpath2namedtuple(paths.EXPIRED_3_DAYS) - expiry_time = check_cert.get_cert_expiration(cert) - now + expiry_time = check_cert.get_cert_expiration(cert) - self.now self.assertEqual(expiry_time.days, -3) + def test_ok_cert(self, *unused): # Test a good certificate: cert = self._certpath2namedtuple(paths.EXPIRE_21_DAYS) - expiry_time = check_cert.get_cert_expiration(cert) - now + expiry_time = check_cert.get_cert_expiration(cert) - self.now self.assertEqual(expiry_time.days, 21) + def test_der_cert(self, *unused): # Test a DER certificate: cert = self._certpath2namedtuple(paths.EXPIRE_41_DAYS_DER) with self.assertRaises(RecoverableException): - expiry_time = check_cert.get_cert_expiration(cert) + check_cert.get_cert_expiration(cert) + def test_broken_cert(self, *unused): # Test a broken certificate: cert = self._certpath2namedtuple(paths.BROKEN_CERT) with self.assertRaises(RecoverableException): - expiry_time = check_cert.get_cert_expiration(cert) + check_cert.get_cert_expiration(cert) + def test_trusted_cert(self, *unused): # Test a "TRUSTED" certificate: cert = self._certpath2namedtuple(paths.TRUSTED_EXPIRE_41_CERT) - expiry_time = check_cert.get_cert_expiration(cert) - now + expiry_time = check_cert.get_cert_expiration(cert) - self.now self.assertEqual(expiry_time.days, 41) - @mock.patch('sys.exit') - def test_command_line_parsing(self, SysExitMock, *unused): - old_args = sys.argv +@mock.patch('sys.exit') +class TestCommandLineParsing(unittest.TestCase): + def setUp(self): + self._old_args = sys.argv + + def tearDown(self): + sys.argv = self._old_args + + def test_proper_command_line_parsing(self, *unused): # General parsing: sys.argv = ['./check_cert', '-v', '-s', '-d', '-c', './check_cert.json'] parsed_cmdline = check_cert.parse_command_line() @@ -188,13 +155,14 @@ def test_command_line_parsing(self, SysExitMock, *unused): 'dont_send': True, }) - # Config file should be a mandatory argument: + def test_config_file_missing_from_commandline(self, SysExitMock): sys.argv = ['./check_cert', ] # Suppres warnings from argparse with mock.patch('sys.stderr'): - parsed_cmdline = check_cert.parse_command_line() + check_cert.parse_command_line() SysExitMock.assert_called_once_with(2) + def test_default_command_line_args(self, *unused): # Test default values: sys.argv = ['./check_cert', '-c', './check_cert.json'] parsed_cmdline = check_cert.parse_command_line() @@ -204,22 +172,114 @@ def test_command_line_parsing(self, SysExitMock, *unused): 'dont_send': False, }) - sys.argv = old_args - @mock.patch('sys.exit') - @mock.patch('check_cert.CertStore') - def test_script_init(self, CertStoreMock, SysExitMock, - ScriptConfigurationMock, ScriptStatusMock, - ScriptLockMock, *unused): +class TestCheckCert(unittest.TestCase): + def _script_conf_factory(self, **kwargs): """ - Test if script initializes its dependencies properly + Provide fake configuration data objects. """ + good_configuration = {"warn_treshold": 30, + "critical_treshold": 15, + "nrpe_enabled": True, + "riemann_enabled": True, + "riemann_hosts": { + 'static': ['1.2.3.4:1:udp', + '2.3.4.5:5555:tcp', ] + }, + "riemann_tags": ["abc", "def"], + "riemann_ttl": 60, + "repo_host": "git.foo.net", + "repo_port": 22, + "repo_url": "/foo-puppet", + "repo_masterbranch": "refs/heads/foo", + "repo_localdir": "/tmp/foo", + "repo_user": "foo", + "repo_pubkey": "./foo", + "lockfile": "./fake_lock.pid", + "ignored_certs": { + 'a69d081221a9caf21b1c18907c800528d6f414d2': + "sample/path/ignored_cert.pem" + } + } + + config = good_configuration.copy() + config.update(kwargs) + + def get_val(key): + self.assertIn(key, config) + return config[key] + + def get_config(): + return config + + return get_val, get_config + + @staticmethod + def _fake_cert_git_repo(cert_extensions): + fake_cert_tuple = namedtuple("FileTuple", ['path', 'content']) + fake_cert_tuple.path = 'sample/path/sample_cert.pem' + fake_cert_tuple.content = 'some content' + return iter([fake_cert_tuple]) - ScriptConfigurationMock.get_val.side_effect, \ - ScriptConfigurationMock.get_config.side_effect = \ + @staticmethod + def _ignored_cert_git_repo(cert_extensions): + ignored_cert_tuple = namedtuple("FileTuple", ['path', 'content']) + ignored_cert_tuple.path = 'sample/path/ignored_cert.pem' + ignored_cert_tuple.content = 'some ignored content' + return iter([ignored_cert_tuple]) + + @staticmethod + def _unsupported_cert_git_repo(cert_extensions): + unsupported_cert_tuple = namedtuple("FileTuple", ['path', 'content']) + unsupported_cert_tuple.path = 'sample/path/unsupported_cert.der' + unsupported_cert_tuple.content = 'some unsupported content' + return iter([unsupported_cert_tuple]) + + def setUp(self): + self.mocks = {} + for patched in ['check_cert.ScriptConfiguration', + 'check_cert.ScriptStatus', + 'check_cert.ScriptLock', + 'logging.error', + 'logging.info', + 'logging.warn', + 'sys.exit', + 'check_cert.CertStore', + 'check_cert.get_cert_expiration']: + patcher = mock.patch(patched) + self.mocks[patched] = patcher.start() + self.addCleanup(patcher.stop) + + # Hack, hack, hack - we terminate script after a call to + # notify_immediate with a non-standard exit code hoping + # that it will be uniq enough to differentiate other + # errors + def terminate_script(*unused): + raise SystemExit(-216) + self.mocks['check_cert.ScriptStatus'].notify_immediate.side_effect = \ + terminate_script + + def terminate_script(exit_status): + raise SystemExit(exit_status) + self.mocks['sys.exit'].side_effect = terminate_script + + # Fake configuration for the script: + self.mocks['check_cert.ScriptConfiguration'].get_val.side_effect, \ + self.mocks['check_cert.ScriptConfiguration'].get_config.side_effect = \ self._script_conf_factory() - check_cert.main(config_file='./check_cert.conf') + self.mocks['check_cert.CertStore'].lookup_certs.side_effect = \ + self._fake_cert_git_repo + + def test_script_init(self): + """ + Test if script initializes its dependencies properly + """ + + with self.assertRaises(SystemExit) as e: + check_cert.main(config_file='./check_cert.conf') + + self.assertEqual(e.exception.code, 0) proper_init_call = dict(riemann_enabled=True, riemann_ttl=60, @@ -231,10 +291,13 @@ def test_script_init(self, CertStoreMock, SysExitMock, riemann_tags=['abc', 'def'], nrpe_enabled=True, debug=False) - ScriptConfigurationMock.load_config.assert_called_once_with('./check_cert.conf') - ScriptLockMock.init.assert_called_once_with("./fake_lock.pid") - ScriptLockMock.aqquire.assert_called_once_with() - ScriptStatusMock.initialize.assert_called_once_with(**proper_init_call) + self.mocks['check_cert.ScriptConfiguration'].load_config.assert_called_once_with( + './check_cert.conf') + self.mocks['check_cert.ScriptLock'].init.assert_called_once_with( + "./fake_lock.pid") + self.mocks['check_cert.ScriptLock'].aqquire.assert_called_once_with() + self.mocks['check_cert.ScriptStatus'].initialize.assert_called_once_with( + **proper_init_call) proper_init_call = dict(host="git.foo.net", port=22, @@ -244,253 +307,216 @@ def test_script_init(self, CertStoreMock, SysExitMock, repo_url="/foo-puppet", repo_masterbranch="refs/heads/foo",) - CertStoreMock.initialize.assert_called_once_with(**proper_init_call) - - @mock.patch('sys.exit') - @mock.patch('check_cert.get_cert_expiration') - @mock.patch('check_cert.CertStore') - def test_sanity_checking(self, CertStoreMock, CertExpirationMock, - SysExitMock, ScriptConfigurationMock, ScriptStatusMock, - *unused): + self.mocks['check_cert.CertStore'].initialize.assert_called_once_with( + **proper_init_call) - # Hack, hack, hack - we terminate script after a call to - # notify_immediate with a non-standard exit code hoping - # that it will be uniq enough to differentiate other - # errors - def terminate_script(*unused): - raise SystemExit(216) - ScriptStatusMock.notify_immediate.side_effect = terminate_script - - # Test if ScriptStatus gets properly initialized - # and whether warn > crit condition is - # checked as well - ScriptConfigurationMock.get_val.side_effect, \ - ScriptConfigurationMock.get_config.side_effect = \ + def test_warn_gt_crit(self): + self.mocks['check_cert.ScriptConfiguration'].get_val.side_effect, \ + self.mocks['check_cert.ScriptConfiguration'].get_config.side_effect = \ self._script_conf_factory(warn_treshold=7, critical_treshold=15) + with self.assertRaises(SystemExit) as e: check_cert.main(config_file='./check_cert.conf') - self.assertEqual(e.exception.code, 216) - self.assertTrue(ScriptStatusMock.notify_immediate.called) - self.assertEqual(ScriptStatusMock.notify_immediate.call_args[0][0], - 'unknown') - ScriptStatusMock.notify_immediate.reset_mock() - - # this time test only the negative warn threshold: - ScriptConfigurationMock.get_val.side_effect, \ - ScriptConfigurationMock.get_config.side_effect = \ + + self.assertEqual(e.exception.code, -216) + self.assertTrue( + self.mocks['check_cert.ScriptStatus'].notify_immediate.called) + self.assertEqual( + self.mocks['check_cert.ScriptStatus'].notify_immediate.call_args[0][0], + 'unknown') + + def test_negative_warn_thresh(self): + self.mocks['check_cert.ScriptConfiguration'].get_val.side_effect, \ + self.mocks['check_cert.ScriptConfiguration'].get_config.side_effect = \ self._script_conf_factory(warn_treshold=-30) - ScriptStatusMock.notify_immediate.side_effect = terminate_script - with self.assertRaises(SystemExit) as e: - check_cert.main(config_file='./check_cert.conf') - self.assertEqual(e.exception.code, 216) - self.assertTrue(ScriptStatusMock.notify_immediate.called) - self.assertEqual(ScriptStatusMock.notify_immediate.call_args[0][0], - 'unknown') - ScriptStatusMock.notify_immediate.reset_mock() - - # this time test only the crit threshold == 0 condition check: - ScriptConfigurationMock.get_val.side_effect, \ - ScriptConfigurationMock.get_config.side_effect = \ - self._script_conf_factory(critical_treshold=-1) - ScriptStatusMock.notify_immediate.side_effect = terminate_script + with self.assertRaises(SystemExit) as e: check_cert.main(config_file='./check_cert.conf') - self.assertEqual(e.exception.code, 216) - self.assertTrue(ScriptStatusMock.notify_immediate.called) - self.assertEqual(ScriptStatusMock.notify_immediate.call_args[0][0], - 'unknown') - ScriptStatusMock.notify_immediate.reset_mock() - - @mock.patch('check_cert.sys.exit') - @mock.patch('check_cert.get_cert_expiration') - @mock.patch('check_cert.CertStore') - def test_certificate_testing(self, CertStoreMock, CertExpirationMock, - SysExitMock, ScriptConfigurationMock, - ScriptStatusMock, ScriptLockMock, *unused): - - # A bit of a workaround, but we cannot simply call sys.exit - def terminate_script(exit_status): - raise SystemExit(exit_status) - SysExitMock.side_effect = terminate_script - # Fake configuration for the script: - ScriptConfigurationMock.get_val.side_effect, \ - ScriptConfigurationMock.get_config.side_effect = \ - self._script_conf_factory() + self.assertEqual(e.exception.code, -216) + self.assertTrue( + self.mocks['check_cert.ScriptStatus'].notify_immediate.called) + self.assertEqual( + self.mocks['check_cert.ScriptStatus'].notify_immediate.call_args[0][0], + 'unknown') - # Provide fake data for the script: - fake_cert_tuple = namedtuple("FileTuple", ['path', 'content']) - fake_cert_tuple.path = 'sample/path/sample_cert.pem' - fake_cert_tuple.content = 'some content' + def test_crit_is_zero(self): + self.mocks['check_cert.ScriptConfiguration'].get_val.side_effect, \ + self.mocks['check_cert.ScriptConfiguration'].get_config.side_effect = \ + self._script_conf_factory(critical_treshold=-1) - ignored_cert_tuple = namedtuple("FileTuple", ['path', 'content']) - ignored_cert_tuple.path = 'sample/path/ignored_cert.pem' - ignored_cert_tuple.content = 'some ignored content' + with self.assertRaises(SystemExit) as e: + check_cert.main(config_file='./check_cert.conf') - unsupported_cert_tuple = namedtuple("FileTuple", ['path', 'content']) - unsupported_cert_tuple.path = 'sample/path/unsupported_cert.der' - unsupported_cert_tuple.content = 'some unsupported content' + self.assertEqual(e.exception.code, -216) + self.assertTrue( + self.mocks['check_cert.ScriptStatus'].notify_immediate.called) + self.assertEqual( + self.mocks['check_cert.ScriptStatus'].notify_immediate.call_args[0][0], + 'unknown') - # simulate a git repo with an unsupported cert: - def fake_cert(cert_extensions): - return iter([unsupported_cert_tuple]) - CertStoreMock.lookup_certs.side_effect = fake_cert + def test_unsuported_cert_detection(self): + self.mocks['check_cert.CertStore'].lookup_certs.side_effect = \ + self._unsupported_cert_git_repo - # test if unsupported certificate is properly handled with self.assertRaises(SystemExit) as e: check_cert.main(config_file='./check_cert.conf') - self.assertEqual(e.exception.code, 0) - self.assertFalse(ScriptStatusMock.notify_immediate.called) - self.assertTrue(ScriptStatusMock.notify_agregated.called) - self.assertEqual(ScriptStatusMock.update.call_args[0][0], 'unknown') - ScriptStatusMock.reset_mock() + self.assertEqual(e.exception.code, 0) + self.assertFalse( + self.mocks['check_cert.ScriptStatus'].notify_immediate.called) + self.assertTrue( + self.mocks['check_cert.ScriptStatus'].notify_agregated.called) + self.assertEqual( + self.mocks['check_cert.ScriptStatus'].update.call_args[0][0], 'unknown') + + def test_ignored_cert_detection(self): # simulate a git repo with an ignored cert: - def fake_cert(cert_extensions): - return iter([ignored_cert_tuple]) - CertStoreMock.lookup_certs.side_effect = fake_cert + self.mocks['check_cert.CertStore'].lookup_certs.side_effect = \ + self._ignored_cert_git_repo - # test if ignored certificate is properly handled: with self.assertRaises(SystemExit) as e: check_cert.main(config_file='./check_cert.conf') + self.assertEqual(e.exception.code, 0) - self.assertFalse(ScriptStatusMock.notify_immediate.called) - self.assertTrue(ScriptStatusMock.notify_agregated.called) - # All certs were ok, so a 'default' message should be send to Rieman - self.assertFalse(ScriptStatusMock.update.called) - ScriptStatusMock.reset_mock() + self.assertFalse( + self.mocks['check_cert.ScriptStatus'].notify_immediate.called) + self.assertTrue( + self.mocks['check_cert.ScriptStatus'].notify_agregated.called) + # All certs were ok, so a 'default' message should be send to + # monitoring + self.assertFalse(self.mocks['check_cert.ScriptStatus'].update.called) - # simulate a git repo with a valid certificate - def fake_cert(cert_extensions): - return iter([fake_cert_tuple]) - CertStoreMock.lookup_certs.side_effect = fake_cert + def test_expired_cert_detection(self): - # test if an expired cert is properly handled: def fake_cert_expiration(cert): - self.assertEqual(cert, fake_cert_tuple) return datetime.utcnow() - timedelta(days=4) - CertExpirationMock.side_effect = fake_cert_expiration + self.mocks['check_cert.get_cert_expiration'].side_effect = \ + fake_cert_expiration + with self.assertRaises(SystemExit) as e: check_cert.main(config_file='./check_cert.conf') - self.assertEqual(e.exception.code, 0) - self.assertTrue(ScriptStatusMock.update.called) - self.assertEqual(ScriptStatusMock.update.call_args[0][0], 'critical') - self.assertTrue(ScriptStatusMock.notify_agregated.called) - self.assertFalse(ScriptStatusMock.notify_immediate.called) - ScriptStatusMock.reset_mock() - # test if soon to expire ( Date: Thu, 1 Jan 2015 23:56:11 +0100 Subject: [PATCH 34/44] Travis-CI integration Change-Id: I3a1e2fcc4435befa9728df4a59195d7fc78a997c --- .travis.yml | 12 ++++++++++++ requirements.txt | 3 +++ 2 files changed, 15 insertions(+) create mode 100644 .travis.yml create mode 100644 requirements.txt diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..1d895d5 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,12 @@ +language: python +python: + - "2.6" + - "2.7" +install: "pip install -r requirements.txt --use-mirrors" +script: "./run_tests.py" +before_script: + - wget https://github.com/vespian/pymisc/archive/1.2.0.tar.gz -O /tmp/pymisc-1.2.0.tar.gz + - tar -xvf /tmp/pymisc-1.2.0.tar.gz -C /tmp/ + - cd /tmp/pymisc-1.2.0/ && /tmp/pymisc-1.2.0/setup.py install + - echo $TRAVIS_BUILD_DIR + - cd $TRAVIS_BUILD_DIR diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..a09f3da --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +dulwich==0.9.6 +mock==1.0.1 +pyOpenSSL==0.14 From 9d904362903b2fb7b2cea8a7cbfe5c5a60bf2f6d Mon Sep 17 00:00:00 2001 From: Vespian Date: Fri, 2 Jan 2015 00:00:28 +0100 Subject: [PATCH 35/44] Make coverage measurements optional Change-Id: I13cf8d7cbfabc3a90b75fd82294e8eeab0d635a1 --- run_tests.py | 36 ++++++++++++------------------------ 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/run_tests.py b/run_tests.py index aed9aff..2c1c50e 100755 --- a/run_tests.py +++ b/run_tests.py @@ -15,29 +15,15 @@ # License for the specific language governing permissions and limitations under # the License. - -#Make it a bit more like python3: -from __future__ import absolute_import -from __future__ import print_function - -import coverage -import os -import shutil +try: + import coverage +except ImportError: + pass import sys import unittest - +import os def main(): - major, minor, micro, releaselevel, serial = sys.version_info - - if major == 2 and minor < 7: - print("In order to run tests you need at least Python 2.7") - sys.exit(1) - - if major == 3: - print("Tests were not tested on Python 3.X, use at your own risk") - sys.exit(1) - #Cleanup old html report: for root, dirs, files in os.walk('test/output_coverage_html/'): for f in files: @@ -48,17 +34,19 @@ def main(): shutil.rmtree(os.path.join(root, d)) #Perform coverage analisys: - cov = coverage.coverage() + if "coverage" in sys.modules: + cov = coverage.coverage() + cov.start() - cov.start() - #Discover the test and execute them: + #Discover the tests and execute them: loader = unittest.TestLoader() tests = loader.discover('./test/') testRunner = unittest.runner.TextTestRunner(descriptions=True, verbosity=1) testRunner.run(tests) - cov.stop() - cov.html_report() + if "coverage" in sys.modules: + cov.stop() + cov.html_report() if __name__ == '__main__': main() From 68d2121b5ab4848e570a3e33cdf59e656abb2545 Mon Sep 17 00:00:00 2001 From: Vespian Date: Fri, 2 Jan 2015 00:03:58 +0100 Subject: [PATCH 36/44] Drop 2.6 from travis Change-Id: Ia4fb967c6eabc607f142b75790aa5682edd501a4 --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 1d895d5..7734935 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,5 @@ language: python python: - - "2.6" - "2.7" install: "pip install -r requirements.txt --use-mirrors" script: "./run_tests.py" From 28b7bb97fff6eba5e7a0c23c9896f2e9662d8232 Mon Sep 17 00:00:00 2001 From: Vespian Date: Fri, 2 Jan 2015 00:05:46 +0100 Subject: [PATCH 37/44] Fix Travis badge Change-Id: I03ca84e561fa8e7353d023126881090b85aee51c --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index f46c01f..e8f1aab 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ # _check_cert_ +[![Build +Status](https://travis-ci.org/vespian/check-cert.svg?branch=master)](https://travis-ci.org/vespian/check-cert) + _check_cert is a certificate expiration check capable of scanning GIT repos and sending data on expiring/expired certificates back to the monitoring system._ From d442b6fcd99f8dbf9c5576d82ed402d370d8b9ad Mon Sep 17 00:00:00 2001 From: Vespian Date: Fri, 2 Jan 2015 00:30:08 +0100 Subject: [PATCH 38/44] Include pymisc dependencies Change-Id: I45b6f20a7570d7803969ff28617bdba448273d29 --- .travis.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7734935..d98f46e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,7 @@ script: "./run_tests.py" before_script: - wget https://github.com/vespian/pymisc/archive/1.2.0.tar.gz -O /tmp/pymisc-1.2.0.tar.gz - tar -xvf /tmp/pymisc-1.2.0.tar.gz -C /tmp/ - - cd /tmp/pymisc-1.2.0/ && /tmp/pymisc-1.2.0/setup.py install - - echo $TRAVIS_BUILD_DIR + - cd /tmp/pymisc-1.2.0/ + - pip install -r ./requirements.txt --use-mirrors + - ./setup.py install - cd $TRAVIS_BUILD_DIR From 06773560f585f4eae345a0f0135f47d56761810b Mon Sep 17 00:00:00 2001 From: Vespian Date: Fri, 2 Jan 2015 00:51:03 +0100 Subject: [PATCH 39/44] Fix exit status of the test run script Change-Id: I145bbc869a5387872ee366cc496a06dfc72aee57 --- run_tests.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/run_tests.py b/run_tests.py index 2c1c50e..9fa986a 100755 --- a/run_tests.py +++ b/run_tests.py @@ -1,7 +1,5 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- -# Copyright (c) 2014 Pawel Rozlach -# Copyright (c) 2013 Pawel Rozlach +# Copyright (c) 2013 Spotify AB # # Licensed under the Apache License, Version 2.0 (the "License"); you may not # use this file except in compliance with the License. You may obtain a copy of @@ -42,11 +40,16 @@ def main(): loader = unittest.TestLoader() tests = loader.discover('./test/') testRunner = unittest.runner.TextTestRunner(descriptions=True, verbosity=1) - testRunner.run(tests) + res = testRunner.run(tests) if "coverage" in sys.modules: cov.stop() cov.html_report() + if res.wasSuccessful(): + sys.exit(0) + else: + sys.exit(1) + if __name__ == '__main__': main() From ea0f210c63cb79110cda58e2c515b7aa2329b88a Mon Sep 17 00:00:00 2001 From: Vespian Date: Sun, 4 Jan 2015 22:49:00 +0100 Subject: [PATCH 40/44] Add icingaexchange.yml file Change-Id: I073e335a536c5aac927a9e9568c510187849fdfe --- icingaexchange.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 icingaexchange.yml diff --git a/icingaexchange.yml b/icingaexchange.yml new file mode 100644 index 0000000..c738787 --- /dev/null +++ b/icingaexchange.yml @@ -0,0 +1,16 @@ +name: check-cert +description: "file:///README.md" +url: "https://github.com/vespian/check-cert" +tags: certificate,git,ssl,tls +vendor: Linux +target: Website,Service +type: Plugin +license: Apache 2.0 +releases: + - name: 0.3.2 + description: "0.3.2 release" + files: + - name: 0.3.2.tar.gz + description: plugin tarbal + url: "https://github.com/vespian/check-cert/archive/0.3.2.tar.gz" + checksum: 95985b6bca06fa410af7a0e8ca842f0b From 3e863825f668c6ade24bbea3998dca5664629488 Mon Sep 17 00:00:00 2001 From: Vespian Date: Sun, 4 Jan 2015 22:55:51 +0100 Subject: [PATCH 41/44] Small fixes related to documenting/icingaexchange Change-Id: I7ca58167002b0dda19b4952d985f9fc1dadcef51 --- README.md | 14 +++++++------- icingaexchange.yml | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index e8f1aab..0622787 100644 --- a/README.md +++ b/README.md @@ -9,13 +9,13 @@ and sending data on expiring/expired certificates back to the monitoring system. ## Project Setup In order to run check_cert you need to have following dependencies installed: -- Dulwich - python implementation of GIT (https://www.samba.org/~jelmer/dulwich/docs/) -- *ssh* command in your PATH -- argparse library -- pyOpenSSL (https://launchpad.net/pyopenssl/) -- pymisc (https://github.com/vespian/pymisc) -- python 2.6 or 2.7 -- dulwich library +* Dulwich - python implementation of GIT (https://www.samba.org/~jelmer/dulwich/docs/) +* *ssh* command in your PATH +* argparse library +* pyOpenSSL (https://launchpad.net/pyopenssl/) +* pymisc (https://github.com/vespian/pymisc) +* python 2.6 or 2.7 +* dulwich library You can also use debian packaging rules from debian/ directory to build a deb package. diff --git a/icingaexchange.yml b/icingaexchange.yml index c738787..fcb0001 100644 --- a/icingaexchange.yml +++ b/icingaexchange.yml @@ -8,7 +8,7 @@ type: Plugin license: Apache 2.0 releases: - name: 0.3.2 - description: "0.3.2 release" + description: "All the features implemented and unittested." files: - name: 0.3.2.tar.gz description: plugin tarbal From 823b0cfe79cc1b61d87458956cdec80bc1784cf2 Mon Sep 17 00:00:00 2001 From: Vespian Date: Mon, 19 Jan 2015 14:47:03 +0100 Subject: [PATCH 42/44] Add submodule support --- check_cert/__init__.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/check_cert/__init__.py b/check_cert/__init__.py index 07c42ee..f1f21fb 100755 --- a/check_cert/__init__.py +++ b/check_cert/__init__.py @@ -136,11 +136,16 @@ def lookup_files(self, determine_wants, root_sha=None, repo_path=''): if root_sha is None: commit = self.get_object(self.head()) root_sha = commit.tree - root = self.get_object(root_sha) + logging.debug("Root sha is {0}".format(root_sha)) + try: + root = self.get_object(root_sha) + except KeyError: + msg = "Skipping object from submodule: {0}, dir: {1}".format(root_sha, repo_path) + logging.warning(msg) + return file_list if repo_path: # Extreme verbosity - # logging.debug("Scanning repo directory {0}".format(repo_path)) - pass + logging.debug("Scanning repo directory {0}".format(repo_path)) else: logging.info("Scanning repo root directory") From 1875db1e0512486b6291c5a91b37b0388aaa8a93 Mon Sep 17 00:00:00 2001 From: Vespian Date: Mon, 19 Jan 2015 14:47:21 +0100 Subject: [PATCH 43/44] Print stacktrace in case of problems --- check_cert/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/check_cert/__init__.py b/check_cert/__init__.py index f1f21fb..0a3726c 100755 --- a/check_cert/__init__.py +++ b/check_cert/__init__.py @@ -552,4 +552,5 @@ def main(config_file, std_err=False, verbose=True, dont_send=False): except Exception as e: msg = "Exception occured: {0}, msg: {1}".format(e.__class__.__name__, str(e)) logging.error(msg) + logging.exception(e) sys.exit(1) From 678a0dadb5815aeebe73a2916dc11d366d5aaa61 Mon Sep 17 00:00:00 2001 From: Vespian Date: Mon, 19 Jan 2015 14:52:03 +0100 Subject: [PATCH 44/44] Version bump to 0.3.3 --- debian/changelog | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/debian/changelog b/debian/changelog index 330ee2e..fc00905 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,10 +1,16 @@ -check-cert (0.3.2) UNRELEASED; urgency=low +check-cert (0.3.3) stable; urgency=low + + * Submodules support + + -- vespian Mon, 19 Jan 2015 14:50:19 +0100 + +check-cert (0.3.2) stable; urgency=low * Fixes, config checking refactoring -- vespian Mon, 06 Oct 2014 21:31:07 +0100 -check-cert (0.3.1+1) UNRELEASED; urgency=low +check-cert (0.3.1+1) stable; urgency=low * Fix dependencies for dulwich.