Merge lp:~hopem/charm-helpers/hardeningfixes into lp:charm-helpers
- hardeningfixes
- Merge into devel
Proposed by
Edward Hope-Morley
Status: | Merged |
---|---|
Merged at revision: | 553 |
Proposed branch: | lp:~hopem/charm-helpers/hardeningfixes |
Merge into: | lp:charm-helpers |
Diff against target: |
736 lines (+192/-101) 16 files modified
charmhelpers/contrib/hardening/apache/checks/config.py (+10/-8) charmhelpers/contrib/hardening/audits/apache.py (+7/-4) charmhelpers/contrib/hardening/audits/file.py (+4/-3) charmhelpers/contrib/hardening/defaults/mysql.yaml (+4/-4) charmhelpers/contrib/hardening/defaults/mysql.yaml.schema (+3/-3) charmhelpers/contrib/hardening/harden.py (+3/-1) charmhelpers/contrib/hardening/host/checks/sysctl.py (+4/-1) charmhelpers/contrib/hardening/host/templates/99-juju-hardening.conf (+1/-1) charmhelpers/contrib/hardening/mysql/checks/config.py (+4/-1) charmhelpers/contrib/hardening/mysql/templates/hardening.cnf (+1/-1) charmhelpers/contrib/hardening/utils.py (+2/-1) tests/contrib/hardening/audits/test_apache_audits.py (+3/-1) tests/contrib/hardening/audits/test_file_audits.py (+94/-42) tests/contrib/hardening/mysql/checks/test_config.py (+1/-1) tests/contrib/hardening/test_templating.py (+47/-27) tests/contrib/hardening/test_utils.py (+4/-2) |
To merge this branch: | bzr merge lp:~hopem/charm-helpers/hardeningfixes |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Liam Young (community) | Approve | ||
Review via email: mp+289624@code.launchpad.net |
Commit message
Description of the change
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'charmhelpers/contrib/hardening/apache/checks/config.py' |
2 | --- charmhelpers/contrib/hardening/apache/checks/config.py 2016-03-18 05:54:42 +0000 |
3 | +++ charmhelpers/contrib/hardening/apache/checks/config.py 2016-03-21 10:39:40 +0000 |
4 | @@ -19,15 +19,17 @@ |
5 | import subprocess |
6 | |
7 | |
8 | -from charmhelpers.core.hookenv import log |
9 | -from charmhelpers.core.hookenv import INFO |
10 | - |
11 | -from charmhelpers.contrib.hardening.audits.file import FilePermissionAudit |
12 | -from charmhelpers.contrib.hardening.audits.file import DirectoryPermissionAudit |
13 | -from charmhelpers.contrib.hardening.audits.file import NoReadWriteForOther |
14 | -from charmhelpers.contrib.hardening.audits.file import TemplatedFile |
15 | +from charmhelpers.core.hookenv import ( |
16 | + log, |
17 | + INFO, |
18 | +) |
19 | +from charmhelpers.contrib.hardening.audits.file import ( |
20 | + FilePermissionAudit, |
21 | + DirectoryPermissionAudit, |
22 | + NoReadWriteForOther, |
23 | + TemplatedFile, |
24 | +) |
25 | from charmhelpers.contrib.hardening.audits.apache import DisabledModuleAudit |
26 | - |
27 | from charmhelpers.contrib.hardening.apache import TEMPLATES_DIR |
28 | from charmhelpers.contrib.hardening import utils |
29 | |
30 | |
31 | === modified file 'charmhelpers/contrib/hardening/audits/apache.py' |
32 | --- charmhelpers/contrib/hardening/audits/apache.py 2016-03-18 06:49:19 +0000 |
33 | +++ charmhelpers/contrib/hardening/audits/apache.py 2016-03-21 10:39:40 +0000 |
34 | @@ -19,9 +19,11 @@ |
35 | |
36 | from six import string_types |
37 | |
38 | -from charmhelpers.core.hookenv import log |
39 | -from charmhelpers.core.hookenv import INFO |
40 | -from charmhelpers.core.hookenv import ERROR |
41 | +from charmhelpers.core.hookenv import ( |
42 | + log, |
43 | + INFO, |
44 | + ERROR, |
45 | +) |
46 | |
47 | from charmhelpers.contrib.hardening.audits import BaseAudit |
48 | |
49 | @@ -50,7 +52,8 @@ |
50 | non_compliant_modules = [] |
51 | for module in self.modules: |
52 | if module in loaded_modules: |
53 | - log('Module %s is enabled and should not be.', level=INFO) |
54 | + log("Module '%s' is enabled but should not be." % |
55 | + (module), level=INFO) |
56 | non_compliant_modules.append(module) |
57 | |
58 | if len(non_compliant_modules) == 0: |
59 | |
60 | === modified file 'charmhelpers/contrib/hardening/audits/file.py' |
61 | --- charmhelpers/contrib/hardening/audits/file.py 2016-03-18 05:54:42 +0000 |
62 | +++ charmhelpers/contrib/hardening/audits/file.py 2016-03-21 10:39:40 +0000 |
63 | @@ -509,9 +509,10 @@ |
64 | |
65 | def is_compliant(self, path): |
66 | """ |
67 | - Given a set of content matching cases, check that all cases match |
68 | - as expected with the contents of the file. Cases can be expected to |
69 | - pass of fail. |
70 | + Given a set of content matching cases i.e. tuple(regex, bool) where |
71 | + bool value denotes whether or not regex is expected to match, check that |
72 | + all cases match as expected with the contents of the file. Cases can be |
73 | + expected to pass of fail. |
74 | |
75 | :param path: Path of file to check. |
76 | :returns: Boolean value representing whether or not all cases are |
77 | |
78 | === modified file 'charmhelpers/contrib/hardening/defaults/mysql.yaml' |
79 | --- charmhelpers/contrib/hardening/defaults/mysql.yaml 2016-03-14 13:57:17 +0000 |
80 | +++ charmhelpers/contrib/hardening/defaults/mysql.yaml 2016-03-21 10:39:40 +0000 |
81 | @@ -4,6 +4,10 @@ |
82 | # name 'mysql' as the root key followed by any of the following with new |
83 | # values. |
84 | |
85 | +hardening: |
86 | + mysql-conf: /etc/mysql/my.cnf |
87 | + hardening-conf: /etc/mysql/conf.d/hardening.cnf |
88 | + |
89 | security: |
90 | # @see http://www.symantec.com/connect/articles/securing-mysql-step-step |
91 | # @see http://dev.mysql.com/doc/refman/5.7/en/server-options.html#option_mysqld_chroot |
92 | @@ -32,7 +36,3 @@ |
93 | |
94 | # @see https://dev.mysql.com/doc/refman/5.7/en/server-options.html#option_mysqld_secure-file-priv |
95 | secure-file-priv: /tmp |
96 | - |
97 | -hardening: |
98 | - mysql-conf: /etc/mysql/my.cnf |
99 | - hardening-conf: /etc/mysql/conf.d/hardening.cnf |
100 | |
101 | === modified file 'charmhelpers/contrib/hardening/defaults/mysql.yaml.schema' |
102 | --- charmhelpers/contrib/hardening/defaults/mysql.yaml.schema 2016-03-14 13:57:17 +0000 |
103 | +++ charmhelpers/contrib/hardening/defaults/mysql.yaml.schema 2016-03-21 10:39:40 +0000 |
104 | @@ -1,5 +1,8 @@ |
105 | # NOTE: this schema must contain all valid keys from it's associated defaults |
106 | # file. It is used to validate user-provided overrides. |
107 | +hardening: |
108 | + mysql-conf: |
109 | + hardening-conf: |
110 | security: |
111 | chroot: |
112 | safe-user-create: |
113 | @@ -10,6 +13,3 @@ |
114 | allow-suspicious-udfs: |
115 | automatic-sp-privileges: |
116 | secure-file-priv: |
117 | -hardening: |
118 | - mysql-conf: |
119 | - hardening-conf: |
120 | \ No newline at end of file |
121 | |
122 | === modified file 'charmhelpers/contrib/hardening/harden.py' |
123 | --- charmhelpers/contrib/hardening/harden.py 2016-03-14 14:18:58 +0000 |
124 | +++ charmhelpers/contrib/hardening/harden.py 2016-03-21 10:39:40 +0000 |
125 | @@ -14,6 +14,8 @@ |
126 | # You should have received a copy of the GNU Lesser General Public License |
127 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
128 | |
129 | +import six |
130 | + |
131 | from collections import OrderedDict |
132 | |
133 | from charmhelpers.core.hookenv import ( |
134 | @@ -60,7 +62,7 @@ |
135 | if enabled: |
136 | modules_to_run = [] |
137 | # modules will always be performed in the following order |
138 | - for module, func in RUN_CATALOG.iteritems(): |
139 | + for module, func in six.iteritems(RUN_CATALOG): |
140 | if module in enabled: |
141 | enabled.remove(module) |
142 | modules_to_run.append(func) |
143 | |
144 | === modified file 'charmhelpers/contrib/hardening/host/checks/sysctl.py' |
145 | --- charmhelpers/contrib/hardening/host/checks/sysctl.py 2016-03-14 20:24:18 +0000 |
146 | +++ charmhelpers/contrib/hardening/host/checks/sysctl.py 2016-03-21 10:39:40 +0000 |
147 | @@ -17,6 +17,7 @@ |
148 | import os |
149 | import platform |
150 | import re |
151 | +import six |
152 | import subprocess |
153 | |
154 | from charmhelpers.core.hookenv import ( |
155 | @@ -184,7 +185,9 @@ |
156 | |
157 | ctxt['sysctl'][key] = d[2] or None |
158 | |
159 | - return ctxt |
160 | + # Translate for python3 |
161 | + return {'sysctl_settings': |
162 | + [(k, v) for k, v in six.iteritems(ctxt['sysctl'])]} |
163 | |
164 | |
165 | class SysctlConf(TemplatedFile): |
166 | |
167 | === modified file 'charmhelpers/contrib/hardening/host/templates/99-juju-hardening.conf' |
168 | --- charmhelpers/contrib/hardening/host/templates/99-juju-hardening.conf 2016-03-14 20:24:18 +0000 |
169 | +++ charmhelpers/contrib/hardening/host/templates/99-juju-hardening.conf 2016-03-21 10:39:40 +0000 |
170 | @@ -2,6 +2,6 @@ |
171 | # WARNING: This configuration file is maintained by Juju. Local changes may |
172 | # be overwritten. |
173 | ############################################################################### |
174 | -{% for key, value in sysctl.iteritems() -%} |
175 | +{% for key, value in sysctl_settings -%} |
176 | {{ key }}={{ value }} |
177 | {% endfor -%} |
178 | |
179 | === modified file 'charmhelpers/contrib/hardening/mysql/checks/config.py' |
180 | --- charmhelpers/contrib/hardening/mysql/checks/config.py 2016-03-16 01:44:06 +0000 |
181 | +++ charmhelpers/contrib/hardening/mysql/checks/config.py 2016-03-21 10:39:40 +0000 |
182 | @@ -14,6 +14,7 @@ |
183 | # You should have received a copy of the GNU Lesser General Public License |
184 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
185 | |
186 | +import six |
187 | import subprocess |
188 | |
189 | from charmhelpers.core.hookenv import ( |
190 | @@ -83,4 +84,6 @@ |
191 | """ |
192 | def __call__(self): |
193 | settings = utils.get_settings('mysql') |
194 | - return {'mysql_settings': settings['security']} |
195 | + # Translate for python3 |
196 | + return {'mysql_settings': |
197 | + [(k, v) for k, v in six.iteritems(settings['security'])]} |
198 | |
199 | === modified file 'charmhelpers/contrib/hardening/mysql/templates/hardening.cnf' |
200 | --- charmhelpers/contrib/hardening/mysql/templates/hardening.cnf 2016-03-16 01:44:06 +0000 |
201 | +++ charmhelpers/contrib/hardening/mysql/templates/hardening.cnf 2016-03-21 10:39:40 +0000 |
202 | @@ -3,7 +3,7 @@ |
203 | # be overwritten. |
204 | ############################################################################### |
205 | [mysqld] |
206 | -{% for setting, value in mysql_settings.iteritems() -%} |
207 | +{% for setting, value in mysql_settings -%} |
208 | {% if value == 'True' -%} |
209 | {{ setting }} |
210 | {% elif value != 'None' and value != None -%} |
211 | |
212 | === modified file 'charmhelpers/contrib/hardening/utils.py' |
213 | --- charmhelpers/contrib/hardening/utils.py 2016-03-14 14:18:58 +0000 |
214 | +++ charmhelpers/contrib/hardening/utils.py 2016-03-21 10:39:40 +0000 |
215 | @@ -18,6 +18,7 @@ |
216 | import grp |
217 | import os |
218 | import pwd |
219 | +import six |
220 | import yaml |
221 | |
222 | from charmhelpers.core.hookenv import ( |
223 | @@ -92,7 +93,7 @@ |
224 | :returns: dictionary of modules config with user overrides applied. |
225 | """ |
226 | if overrides: |
227 | - for k, v in overrides.iteritems(): |
228 | + for k, v in six.iteritems(overrides): |
229 | if k in schema: |
230 | if schema[k] is None: |
231 | settings[k] = v |
232 | |
233 | === added file 'tests/contrib/hardening/__init__.py' |
234 | === modified file 'tests/contrib/hardening/audits/test_apache_audits.py' |
235 | --- tests/contrib/hardening/audits/test_apache_audits.py 2016-03-18 05:54:42 +0000 |
236 | +++ tests/contrib/hardening/audits/test_apache_audits.py 2016-03-21 10:39:40 +0000 |
237 | @@ -49,15 +49,16 @@ |
238 | self.assertFalse(mock_loaded_modules.called) |
239 | |
240 | @patch.object(apache.DisabledModuleAudit, '_get_loaded_modules') |
241 | + @patch.object(apache, 'log', lambda *args, **kwargs: None) |
242 | def test_ensure_compliance_loaded_modules_raises_ex(self, |
243 | mock_loaded_modules): |
244 | mock_loaded_modules.side_effect = CalledProcessError(1, 'test', 'err') |
245 | audit = apache.DisabledModuleAudit('foo') |
246 | audit.ensure_compliance() |
247 | - self.assertTrue(self.log.called) |
248 | |
249 | @patch.object(apache.DisabledModuleAudit, '_get_loaded_modules') |
250 | @patch.object(apache.DisabledModuleAudit, '_disable_module') |
251 | + @patch.object(apache, 'log', lambda *args, **kwargs: None) |
252 | def test_disabled_modules_not_loaded(self, mock_disable_module, |
253 | mock_loaded_modules): |
254 | mock_loaded_modules.return_value = ['foo'] |
255 | @@ -68,6 +69,7 @@ |
256 | @patch.object(apache.DisabledModuleAudit, '_get_loaded_modules') |
257 | @patch.object(apache.DisabledModuleAudit, '_disable_module') |
258 | @patch.object(apache.DisabledModuleAudit, '_restart_apache') |
259 | + @patch.object(apache, 'log', lambda *args, **kwargs: None) |
260 | def test_disabled_modules_loaded(self, mock_restart_apache, |
261 | mock_disable_module, mock_loaded_modules): |
262 | mock_loaded_modules.return_value = ['foo', 'bar'] |
263 | |
264 | === modified file 'tests/contrib/hardening/audits/test_file_audits.py' |
265 | --- tests/contrib/hardening/audits/test_file_audits.py 2016-03-14 13:49:57 +0000 |
266 | +++ tests/contrib/hardening/audits/test_file_audits.py 2016-03-21 10:39:40 +0000 |
267 | @@ -38,7 +38,7 @@ |
268 | def _patch_obj(self, obj, method): |
269 | _m = patch.object(obj, method) |
270 | mock = _m.start() |
271 | - self.addCleanup(mock.stop) |
272 | + self.addCleanup(_m.stop) |
273 | setattr(self, method, mock) |
274 | |
275 | def test_ensure_compliance(self, mock_exists): |
276 | @@ -79,9 +79,9 @@ |
277 | self._patch_obj(file.grp, 'getgrnam') |
278 | self._patch_obj(file.pwd, 'getpwnam') |
279 | self._patch_obj(file.FilePermissionAudit, '_get_stat') |
280 | - self.getpwnam.return_value = EasyMock({'pw_name': 'ubuntu', |
281 | + self.getpwnam.return_value = EasyMock({'pw_name': 'testuser', |
282 | 'pw_uid': 1000}) |
283 | - self.getgrnam.return_value = EasyMock({'gr_name': 'ubuntu', |
284 | + self.getgrnam.return_value = EasyMock({'gr_name': 'testgroup', |
285 | 'gr_gid': 1000}) |
286 | self._get_stat.return_value = EasyMock({'st_mode': 0o644, |
287 | 'st_uid': 1000, |
288 | @@ -90,47 +90,51 @@ |
289 | def _patch_obj(self, obj, method): |
290 | _m = patch.object(obj, method) |
291 | mock = _m.start() |
292 | - self.addCleanup(mock.stop) |
293 | + self.addCleanup(_m.stop) |
294 | setattr(self, method, mock) |
295 | |
296 | def test_is_compliant(self): |
297 | check = file.FilePermissionAudit(paths=['/foo/bar'], |
298 | - user='ubuntu', |
299 | - group='ubuntu', mode=0o644) |
300 | + user='testuser', |
301 | + group='testgroup', mode=0o644) |
302 | compliant = check.is_compliant('/foo/bar') |
303 | self.assertTrue(compliant) |
304 | |
305 | + @patch.object(file, 'log', lambda *args, **kwargs: None) |
306 | def test_not_compliant_wrong_group(self): |
307 | - self.getgrnam.return_value = EasyMock({'gr_name': 'admin', |
308 | + self.getgrnam.return_value = EasyMock({'gr_name': 'testgroup', |
309 | 'gr_gid': 222}) |
310 | - check = file.FilePermissionAudit(paths=['/foo/bar'], user='ubuntu', |
311 | - group='ubuntu', mode=0o644) |
312 | + check = file.FilePermissionAudit(paths=['/foo/bar'], user='testuser', |
313 | + group='testgroup', mode=0o644) |
314 | compliant = check.is_compliant('/foo/bar') |
315 | self.assertFalse(compliant) |
316 | |
317 | + @patch.object(file, 'log', lambda *args, **kwargs: None) |
318 | def test_not_compliant_wrong_user(self): |
319 | self.getpwnam.return_value = EasyMock({'pw_name': 'fred', |
320 | 'pw_uid': 123}) |
321 | - check = file.FilePermissionAudit(paths=['/foo/bar'], user='ubuntu', |
322 | - group='ubuntu', mode=0o644) |
323 | + check = file.FilePermissionAudit(paths=['/foo/bar'], user='testuser', |
324 | + group='testgroup', mode=0o644) |
325 | compliant = check.is_compliant('/foo/bar') |
326 | self.assertFalse(compliant) |
327 | |
328 | + @patch.object(file, 'log', lambda *args, **kwargs: None) |
329 | def test_not_compliant_wrong_permissions(self): |
330 | self._get_stat.return_value = EasyMock({'st_mode': 0o777, |
331 | 'st_uid': 1000, |
332 | 'st_gid': 1000}) |
333 | - check = file.FilePermissionAudit(paths=['/foo/bar'], user='ubuntu', |
334 | - group='ubuntu', mode=0o644) |
335 | + check = file.FilePermissionAudit(paths=['/foo/bar'], user='testuser', |
336 | + group='testgroup', mode=0o644) |
337 | compliant = check.is_compliant('/foo/bar') |
338 | self.assertFalse(compliant) |
339 | |
340 | @patch('charmhelpers.contrib.hardening.utils.ensure_permissions') |
341 | + @patch.object(file, 'log', lambda *args, **kwargs: None) |
342 | def test_comply(self, _ensure_permissions): |
343 | - check = file.FilePermissionAudit(paths=['/foo/bar'], user='ubuntu', |
344 | - group='ubuntu', mode=0o644) |
345 | + check = file.FilePermissionAudit(paths=['/foo/bar'], user='testuser', |
346 | + group='testgroup', mode=0o644) |
347 | check.comply('/foo/bar') |
348 | - c = call('/foo/bar', 'ubuntu', 'ubuntu', 0o644) |
349 | + c = call('/foo/bar', 'testuser', 'testgroup', 0o644) |
350 | _ensure_permissions.assert_has_calls(c) |
351 | |
352 | |
353 | @@ -139,19 +143,24 @@ |
354 | super(DirectoryPermissionAuditTestCase, self).setUp() |
355 | |
356 | @patch('charmhelpers.contrib.hardening.audits.file.os.path.isdir') |
357 | + @patch.object(file, 'log', lambda *args, **kwargs: None) |
358 | def test_is_compliant_not_directory(self, mock_isdir): |
359 | mock_isdir.return_value = False |
360 | - check = file.DirectoryPermissionAudit(paths=['/foo/bar'], user='ubuntu', |
361 | - group='ubuntu', mode=0o0700) |
362 | + check = file.DirectoryPermissionAudit(paths=['/foo/bar'], |
363 | + user='testuser', |
364 | + group='testgroup', mode=0o0700) |
365 | self.assertRaises(ValueError, check.is_compliant, '/foo/bar') |
366 | |
367 | @patch.object(file.FilePermissionAudit, 'is_compliant') |
368 | + @patch.object(file, 'log', lambda *args, **kwargs: None) |
369 | def test_is_compliant_file_not_compliant(self, mock_is_compliant): |
370 | mock_is_compliant.return_value = False |
371 | tmpdir = tempfile.mkdtemp() |
372 | try: |
373 | - check = file.DirectoryPermissionAudit(paths=[tmpdir], user='ubuntu', |
374 | - group='ubuntu', mode=0o0700) |
375 | + check = file.DirectoryPermissionAudit(paths=[tmpdir], |
376 | + user='testuser', |
377 | + group='testgroup', |
378 | + mode=0o0700) |
379 | compliant = check.is_compliant(tmpdir) |
380 | self.assertFalse(compliant) |
381 | finally: |
382 | @@ -163,6 +172,7 @@ |
383 | super(NoSUIDGUIDAuditTestCase, self).setUp() |
384 | |
385 | @patch.object(file.NoSUIDSGIDAudit, '_get_stat') |
386 | + @patch.object(file, 'log', lambda *args, **kwargs: None) |
387 | def test_is_compliant(self, mock_get_stat): |
388 | mock_get_stat.return_value = EasyMock({'st_mode': 0o0644, |
389 | 'st_uid': 0, |
390 | @@ -172,6 +182,7 @@ |
391 | self.assertTrue(compliant) |
392 | |
393 | @patch.object(file.NoSUIDSGIDAudit, '_get_stat') |
394 | + @patch.object(file, 'log', lambda *args, **kwargs: None) |
395 | def test_is_noncompliant(self, mock_get_stat): |
396 | mock_get_stat.return_value = EasyMock({'st_mode': 0o6644, |
397 | 'st_uid': 0, |
398 | @@ -180,23 +191,26 @@ |
399 | compliant = audit.is_compliant('/foo/bar') |
400 | self.assertFalse(compliant) |
401 | |
402 | - @patch('charmhelpers.contrib.hardening.audits.file.log') |
403 | - @patch('charmhelpers.contrib.hardening.audits.file.check_output') |
404 | + @patch.object(file, 'log') |
405 | + @patch.object(file, 'check_output') |
406 | def test_comply(self, mock_check_output, mock_log): |
407 | audit = file.NoSUIDSGIDAudit('/foo/bar') |
408 | audit.comply('/foo/bar') |
409 | mock_check_output.assert_has_calls(call(['chmod', '-s', '/foo/bar'])) |
410 | + self.assertTrue(mock_log.called) |
411 | |
412 | |
413 | class TemplatedFileTestCase(TestCase): |
414 | def setUp(self): |
415 | super(TemplatedFileTestCase, self).setUp() |
416 | - self.kv = patch.object(unitdata, 'kv').start() |
417 | + self.kv = patch.object(unitdata, 'kv') |
418 | + self.kv.start() |
419 | self.addCleanup(self.kv.stop) |
420 | |
421 | @patch.object(file.TemplatedFile, 'templates_match') |
422 | @patch.object(file.TemplatedFile, 'contents_match') |
423 | @patch.object(file.TemplatedFile, 'permissions_match') |
424 | + @patch.object(file, 'log', lambda *args, **kwargs: None) |
425 | def test_is_not_compliant(self, contents_match_, permissions_match_, |
426 | templates_match_): |
427 | contents_match_.return_value = False |
428 | @@ -210,6 +224,7 @@ |
429 | @patch.object(file.TemplatedFile, 'templates_match') |
430 | @patch.object(file.TemplatedFile, 'contents_match') |
431 | @patch.object(file.TemplatedFile, 'permissions_match') |
432 | + @patch.object(file, 'log', lambda *args, **kwargs: None) |
433 | def test_is_compliant(self, contents_match_, permissions_match_, |
434 | templates_match_): |
435 | contents_match_.return_value = True |
436 | @@ -223,6 +238,7 @@ |
437 | @patch.object(file.TemplatedFile, 'templates_match') |
438 | @patch.object(file.TemplatedFile, 'contents_match') |
439 | @patch.object(file.TemplatedFile, 'permissions_match') |
440 | + @patch.object(file, 'log', lambda *args, **kwargs: None) |
441 | def test_template_changes(self, contents_match_, permissions_match_, |
442 | templates_match_): |
443 | contents_match_.return_value = True |
444 | @@ -235,6 +251,7 @@ |
445 | |
446 | @patch.object(file, 'render_and_write') |
447 | @patch.object(file.utils, 'ensure_permissions') |
448 | + @patch.object(file, 'log', lambda *args, **kwargs: None) |
449 | def test_comply(self, mock_ensure_permissions, mock_render_and_write): |
450 | class Context(object): |
451 | def __call__(self): |
452 | @@ -263,22 +280,57 @@ |
453 | |
454 | class FileContentAuditTestCase(TestCase): |
455 | |
456 | - def test_audit_contents_pass(self): |
457 | - conditions = {'pass': [r'^KexAlgorithms\s+diffie-hellman-group-exchange-sha256$'], |
458 | - 'fail': [r'^KexAlgorithms\s+diffie-hellman-group-exchange-sha256.+$']} |
459 | - with tempfile.NamedTemporaryFile() as ftmp: |
460 | - with open(ftmp.name, 'w') as fd: |
461 | - fd.write(CONTENTS_PASS) |
462 | - |
463 | - audit = file.FileContentAudit(ftmp.name, conditions) |
464 | - self.assertTrue(audit.is_compliant(ftmp.name)) |
465 | - |
466 | - def test_audit_contents_fail(self): |
467 | - conditions = {'pass': [r'^KexAlgorithms\s+diffie-hellman-group-exchange-sha256$'], |
468 | - 'fail': [r'^KexAlgorithms\s+diffie-hellman-group-exchange-sha256.+$']} |
469 | - with tempfile.NamedTemporaryFile() as ftmp: |
470 | - with open(ftmp.name, 'w') as fd: |
471 | - fd.write(CONTENTS_FAIL) |
472 | - |
473 | - audit = file.FileContentAudit(ftmp.name, conditions) |
474 | - self.assertFalse(audit.is_compliant(ftmp.name)) |
475 | + @patch.object(file, 'log') |
476 | + def test_audit_contents_pass(self, mock_log): |
477 | + conditions = {'pass': |
478 | + [r'^KexAlgorithms\s+diffie-hellman-group-exchange-' |
479 | + 'sha256$'], |
480 | + 'fail': [r'^KexAlgorithms\s+diffie-hellman-group-' |
481 | + 'exchange-sha256.+$']} |
482 | + with tempfile.NamedTemporaryFile() as ftmp: |
483 | + filename = ftmp.name |
484 | + with open(filename, 'w') as fd: |
485 | + fd.write(CONTENTS_FAIL) |
486 | + |
487 | + audit = file.FileContentAudit(filename, conditions) |
488 | + self.assertFalse(audit.is_compliant(filename)) |
489 | + |
490 | + calls = [call("Auditing contents of file '%s'" % filename, |
491 | + level='DEBUG'), |
492 | + call("Pattern '^KexAlgorithms\\s+diffie-hellman-group-" |
493 | + "exchange-sha256$' was expected to pass but instead it " |
494 | + "failed", level='WARNING'), |
495 | + call("Pattern '^KexAlgorithms\\s+diffie-hellman-group-" |
496 | + "exchange-sha256.+$' was expected to fail but instead " |
497 | + "it passed", level='WARNING'), |
498 | + call('Checked 2 cases and 0 passed', level='DEBUG')] |
499 | + mock_log.assert_has_calls(calls) |
500 | + |
501 | + @patch.object(file, 'log') |
502 | + def test_audit_contents_fail(self, mock_log): |
503 | + conditions = {'pass': |
504 | + [r'^KexAlgorithms\s+diffie-hellman-group-exchange-' |
505 | + 'sha256$'], |
506 | + 'fail': |
507 | + [r'^KexAlgorithms\s+diffie-hellman-group-exchange-' |
508 | + 'sha256.+$']} |
509 | + with tempfile.NamedTemporaryFile() as ftmp: |
510 | + filename = ftmp.name |
511 | + with open(filename, 'w') as fd: |
512 | + fd.write(CONTENTS_FAIL) |
513 | + |
514 | + audit = file.FileContentAudit(filename, conditions) |
515 | + self.assertFalse(audit.is_compliant(filename)) |
516 | + |
517 | + calls = [call("Auditing contents of file '%s'" % filename, |
518 | + level='DEBUG'), |
519 | + call("Pattern '^KexAlgorithms\\s+diffie-hellman-group-" |
520 | + "exchange-sha256$' was expected to pass but instead " |
521 | + "it failed", |
522 | + level='WARNING'), |
523 | + call("Pattern '^KexAlgorithms\\s+diffie-hellman-group-" |
524 | + "exchange-sha256.+$' was expected to fail but instead " |
525 | + "it passed", |
526 | + level='WARNING'), |
527 | + call('Checked 2 cases and 0 passed', level='DEBUG')] |
528 | + mock_log.assert_has_calls(calls) |
529 | |
530 | === modified file 'tests/contrib/hardening/mysql/checks/test_config.py' |
531 | --- tests/contrib/hardening/mysql/checks/test_config.py 2016-03-14 13:49:57 +0000 |
532 | +++ tests/contrib/hardening/mysql/checks/test_config.py 2016-03-21 10:39:40 +0000 |
533 | @@ -32,4 +32,4 @@ |
534 | }) |
535 | def test_get_audits(self): |
536 | audits = config.get_audits() |
537 | - self.assertEqual(3, len(audits)) |
538 | + self.assertEqual(4, len(audits)) |
539 | |
540 | === modified file 'tests/contrib/hardening/test_templating.py' |
541 | --- tests/contrib/hardening/test_templating.py 2016-03-14 20:24:18 +0000 |
542 | +++ tests/contrib/hardening/test_templating.py 2016-03-21 10:39:40 +0000 |
543 | @@ -16,8 +16,9 @@ |
544 | |
545 | import tempfile |
546 | import os |
547 | +import six |
548 | |
549 | -from mock import patch, MagicMock |
550 | +from mock import call, patch |
551 | from unittest import TestCase |
552 | |
553 | os.environ['JUJU_CHARM_DIR'] = '/tmp' |
554 | @@ -85,22 +86,29 @@ |
555 | lambda: {'DISTRIB_CODENAME': 'precise'}) |
556 | @patch.object(utils, 'ensure_permissions') |
557 | @patch.object(templating, 'write') |
558 | + @patch('charmhelpers.contrib.hardening.audits.file.log') |
559 | @patch.object(templating, 'log', lambda *args, **kwargs: None) |
560 | @patch.object(utils, 'log', lambda *args, **kwargs: None) |
561 | @patch.object(ssh_config_check, 'log', lambda *args, **kwargs: None) |
562 | - def test_ssh_config_render_and_check_lt_trusty(self, mock_write, |
563 | + def test_ssh_config_render_and_check_lt_trusty(self, mock_log, mock_write, |
564 | mock_ensure_permissions): |
565 | audits = ssh_config_check.get_audits() |
566 | contentcheckers = self.get_contentcheckers(audits) |
567 | renderers = self.get_renderers(audits) |
568 | + configs = {} |
569 | |
570 | def write(path, data): |
571 | - with tempfile.NamedTemporaryFile(delete=False) as FTMP: |
572 | + with tempfile.NamedTemporaryFile(delete=False) as ftmp: |
573 | + if os.path.basename(path) == "ssh_config": |
574 | + configs['ssh'] = ftmp.name |
575 | + elif os.path.basename(path) == "sshd_config": |
576 | + configs['sshd'] = ftmp.name |
577 | + |
578 | if path in self.pathindex: |
579 | raise Exception("File already rendered '%s'" % path) |
580 | |
581 | - self.pathindex[path] = FTMP.name |
582 | - with open(FTMP.name, 'w') as fd: |
583 | + self.pathindex[path] = ftmp.name |
584 | + with open(ftmp.name, 'wb') as fd: |
585 | fd.write(data) |
586 | |
587 | mock_write.side_effect = write |
588 | @@ -112,26 +120,35 @@ |
589 | self.assertEqual('/etc/ssh/sshd_config', args_list[1][0][0]) |
590 | self.assertEqual(mock_write.call_count, 2) |
591 | |
592 | + calls = [call("Auditing contents of file '%s'" % configs['ssh'], |
593 | + level='DEBUG'), |
594 | + call('Checked 10 cases and 10 passed', level='DEBUG'), |
595 | + call("Auditing contents of file '%s'" % configs['sshd'], |
596 | + level='DEBUG'), |
597 | + call('Checked 10 cases and 10 passed', level='DEBUG')] |
598 | + mock_log.assert_has_calls(calls) |
599 | + |
600 | @patch.object(ssh_config_check, 'lsb_release', |
601 | lambda: {'DISTRIB_CODENAME': 'trusty'}) |
602 | @patch.object(utils, 'ensure_permissions') |
603 | @patch.object(templating, 'write') |
604 | + @patch('charmhelpers.contrib.hardening.audits.file.log') |
605 | @patch.object(templating, 'log', lambda *args, **kwargs: None) |
606 | @patch.object(utils, 'log', lambda *args, **kwargs: None) |
607 | @patch.object(ssh_config_check, 'log', lambda *args, **kwargs: None) |
608 | - def test_ssh_config_render_and_check_gte_trusty(self, mock_write, |
609 | + def test_ssh_config_render_and_check_gte_trusty(self, mock_log, mock_write, |
610 | mock_ensure_permissions): |
611 | audits = ssh_config_check.get_audits() |
612 | contentcheckers = self.get_contentcheckers(audits) |
613 | renderers = self.get_renderers(audits) |
614 | |
615 | def write(path, data): |
616 | - with tempfile.NamedTemporaryFile(delete=False) as FTMP: |
617 | + with tempfile.NamedTemporaryFile(delete=False) as ftmp: |
618 | if path in self.pathindex: |
619 | raise Exception("File already rendered '%s'" % path) |
620 | |
621 | - self.pathindex[path] = FTMP.name |
622 | - with open(FTMP.name, 'w') as fd: |
623 | + self.pathindex[path] = ftmp.name |
624 | + with open(ftmp.name, 'wb') as fd: |
625 | fd.write(data) |
626 | |
627 | mock_write.side_effect = write |
628 | @@ -143,6 +160,9 @@ |
629 | self.assertEqual('/etc/ssh/sshd_config', args_list[1][0][0]) |
630 | self.assertEqual(mock_write.call_count, 2) |
631 | |
632 | + mock_log.assert_has_calls([call('Checked 9 cases and 9 passed', |
633 | + level='DEBUG')]) |
634 | + |
635 | @patch.object(utils, 'ensure_permissions') |
636 | @patch.object(templating, 'write') |
637 | @patch.object(sysctl, 'log', lambda *args, **kwargs: None) |
638 | @@ -157,9 +177,9 @@ |
639 | if path in self.pathindex: |
640 | raise Exception("File already rendered '%s'" % path) |
641 | |
642 | - with tempfile.NamedTemporaryFile(delete=False) as FTMP: |
643 | - self.pathindex[path] = FTMP.name |
644 | - with open(FTMP.name, 'w') as fd: |
645 | + with tempfile.NamedTemporaryFile(delete=False) as ftmp: |
646 | + self.pathindex[path] = ftmp.name |
647 | + with open(ftmp.name, 'wb') as fd: |
648 | fd.write(data) |
649 | |
650 | mock_write.side_effect = write |
651 | @@ -185,9 +205,9 @@ |
652 | if path in self.pathindex: |
653 | raise Exception("File already rendered '%s'" % path) |
654 | |
655 | - with tempfile.NamedTemporaryFile(delete=False) as FTMP: |
656 | - self.pathindex[path] = FTMP.name |
657 | - with open(FTMP.name, 'w') as fd: |
658 | + with tempfile.NamedTemporaryFile(delete=False) as ftmp: |
659 | + self.pathindex[path] = ftmp.name |
660 | + with open(ftmp.name, 'wb') as fd: |
661 | fd.write(data) |
662 | |
663 | mock_write.side_effect = write |
664 | @@ -209,17 +229,17 @@ |
665 | @patch('charmhelpers.contrib.hardening.audits.file.os.path.exists', |
666 | lambda *a, **kwa: True) |
667 | @patch.object(apache_config_check, 'subprocess') |
668 | - @patch.object(apache_config_check, 're') |
669 | @patch.object(utils, 'ensure_permissions') |
670 | @patch.object(templating, 'write') |
671 | @patch.object(templating, 'log', lambda *args, **kwargs: None) |
672 | @patch.object(utils, 'log', lambda *args, **kwargs: None) |
673 | def test_apache_conf_and_check(self, mock_write, mock_ensure_permissions, |
674 | - mock_re, mock_subprocess): |
675 | - mock_group = MagicMock() |
676 | - mock_group.side_effect = "foo" |
677 | - mock_re.search.return_value = mock_group |
678 | + mock_subprocess): |
679 | mock_subprocess.call.return_value = 0 |
680 | + apache_version = """Server version: Apache/2.4.7 (Ubuntu) |
681 | + Server built: Jan 14 2016 17:45:23 |
682 | + """ |
683 | + mock_subprocess.check_output.return_value = apache_version |
684 | audits = apache_config_check.get_audits() |
685 | contentcheckers = self.get_contentcheckers(audits) |
686 | renderers = self.get_renderers(audits) |
687 | @@ -228,9 +248,9 @@ |
688 | if path in self.pathindex: |
689 | raise Exception("File already rendered '%s'" % path) |
690 | |
691 | - with tempfile.NamedTemporaryFile(delete=False) as FTMP: |
692 | - self.pathindex[path] = FTMP.name |
693 | - with open(FTMP.name, 'w') as fd: |
694 | + with tempfile.NamedTemporaryFile(delete=False) as ftmp: |
695 | + self.pathindex[path] = ftmp.name |
696 | + with open(ftmp.name, 'wb') as fd: |
697 | fd.write(data) |
698 | |
699 | mock_write.side_effect = write |
700 | @@ -266,9 +286,9 @@ |
701 | if path in self.pathindex: |
702 | raise Exception("File already rendered '%s'" % path) |
703 | |
704 | - with tempfile.NamedTemporaryFile(delete=False) as FTMP: |
705 | - self.pathindex[path] = FTMP.name |
706 | - with open(FTMP.name, 'w') as fd: |
707 | + with tempfile.NamedTemporaryFile(delete=False) as ftmp: |
708 | + self.pathindex[path] = ftmp.name |
709 | + with open(ftmp.name, 'wb') as fd: |
710 | fd.write(data) |
711 | |
712 | mock_write.side_effect = write |
713 | @@ -282,7 +302,7 @@ |
714 | |
715 | def tearDown(self): |
716 | # Cleanup |
717 | - for path in self.pathindex.itervalues(): |
718 | + for path in six.itervalues(self.pathindex): |
719 | os.remove(path) |
720 | |
721 | super(TemplatingTestCase, self).tearDown() |
722 | |
723 | === modified file 'tests/contrib/hardening/test_utils.py' |
724 | --- tests/contrib/hardening/test_utils.py 2016-03-14 13:49:57 +0000 |
725 | +++ tests/contrib/hardening/test_utils.py 2016-03-21 10:39:40 +0000 |
726 | @@ -58,6 +58,8 @@ |
727 | mock_get_user_provided_overrides.return_value = {} |
728 | self.assertEqual(utils.__SETTINGS__, {}) |
729 | self.assertTrue('sysctl' in utils.get_settings('os')) |
730 | - self.assertEqual(list(six.iterkeys(utils.__SETTINGS__)), ['os']) |
731 | + self.assertEqual(sorted(list(six.iterkeys(utils.__SETTINGS__))), |
732 | + ['os']) |
733 | self.assertTrue('server' in utils.get_settings('ssh')) |
734 | - self.assertEqual(list(six.iterkeys(utils.__SETTINGS__)), ['os', 'ssh']) |
735 | + self.assertEqual(sorted(list(six.iterkeys(utils.__SETTINGS__))), |
736 | + ['os', 'ssh']) |
lgtm