diff --git a/bandit/blacklists/__init__.py b/bandit/blacklists/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bandit/blacklists/calls.py b/bandit/blacklists/calls.py new file mode 100644 index 00000000..8cfbe9de --- /dev/null +++ b/bandit/blacklists/calls.py @@ -0,0 +1,225 @@ +# -*- coding:utf-8 -*- +# +# Copyright 2016 Hewlett-Packard Development Company, L.P. +# +# 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. + +from bandit.blacklists import utils + + +def gen_blacklist(): + """Generate a list of items to blacklist. + + Methods of this type, "bandit.blacklist" plugins, are used to build a list + of items that bandit's built in blacklisting tests will use to trigger + issues. They replace the older blacklist* test plugins and allow + blacklisted items to have a unique bandit ID for filtering and profile + usage. + + :return: a dictionary mapping node types to a list of blacklist data + """ + + sets = [] + sets.append(utils.build_conf_dict( + 'pickle', 'B301', + ['pickle.loads', + 'pickle.load', + 'pickle.Unpickler', + 'cPickle.loads', + 'cPickle.load', + 'cPickle.Unpickler'], + 'Pickle library appears to be in use, possible security issue.' + )) + + sets.append(utils.build_conf_dict( + 'marshal', 'B302', ['marshal.load', 'marshal.loads'], + 'Deserialization with the marshal module is possibly dangerous.' + )) + + sets.append(utils.build_conf_dict( + 'md5', 'B303', + ['hashlib.md5', + 'Crypto.Hash.MD2.new', + 'Crypto.Hash.MD4.new', + 'Crypto.Hash.MD5.new', + 'cryptography.hazmat.primitives.hashes.MD5'], + 'Use of insecure MD2, MD4, or MD5 hash function.' + )) + + sets.append(utils.build_conf_dict( + 'ciphers', 'B304', + ['Crypto.Cipher.ARC2.new', + 'Crypto.Cipher.ARC4.new', + 'Crypto.Cipher.Blowfish.new', + 'Crypto.Cipher.DES.new', + 'Crypto.Cipher.XOR.new', + 'cryptography.hazmat.primitives.ciphers.algorithms.ARC4', + 'cryptography.hazmat.primitives.ciphers.algorithms.Blowfish', + 'cryptography.hazmat.primitives.ciphers.algorithms.IDEA'], + 'Use of insecure cipher {name}. Replace with a known secure' + ' cipher such as AES.', + 'HIGH' + )) + + sets.append(utils.build_conf_dict( + 'cipher_modes', 'B305', + ['cryptography.hazmat.primitives.ciphers.modes.ECB'], + 'Use of insecure cipher mode {name}.' + )) + + sets.append(utils.build_conf_dict( + 'mktemp_q', 'B306', ['tempfile.mktemp'], + 'Use of insecure and deprecated function (mktemp).' + )) + + sets.append(utils.build_conf_dict( + 'eval', 'B307', ['eval'], + 'Use of possibly insecure function - consider using safer ' + 'ast.literal_eval.' + )) + + sets.append(utils.build_conf_dict( + 'mark_safe', 'B308', ['mark_safe'], + 'Use of mark_safe() may expose cross-site scripting ' + 'vulnerabilities and should be reviewed.' + )) + + sets.append(utils.build_conf_dict( + 'httpsconnection', 'B309', + ['httplib.HTTPSConnection', + 'http.client.HTTPSConnection', + 'six.moves.http_client.HTTPSConnection'], + 'Use of HTTPSConnection does not provide security, see ' + 'https://wiki.openstack.org/wiki/OSSN/OSSN-0033' + )) + + sets.append(utils.build_conf_dict( + 'urllib_urlopen', 'B310', + ['urllib.urlopen', + 'urllib.request.urlopen', + 'urllib.urlretrieve', + 'urllib.request.urlretrieve', + 'urllib.URLopener', + 'urllib.request.URLopener', + 'urllib.FancyURLopener', + 'urllib.request.FancyURLopener', + 'urllib2.urlopen', + 'urllib2.Request', + 'six.moves.urllib.request.urlopen', + 'six.moves.urllib.request.urlretrieve', + 'six.moves.urllib.request.URLopener', + 'six.moves.urllib.request.FancyURLopener'], + 'Audit url open for permitted schemes. Allowing use of file:/ or ' + 'custom schemes is often unexpected.' + )) + + sets.append(utils.build_conf_dict( + 'random', 'B311', + ['random.random', + 'random.randrange', + 'random.randint', + 'random.choice', + 'random.uniform', + 'random.triangular'], + 'Standard pseudo-random generators are not suitable for ' + 'security/cryptographic purposes.', + 'LOW' + )) + + sets.append(utils.build_conf_dict( + 'telnetlib', 'B312', ['telnetlib.*'], + 'Telnet-related funtions are being called. Telnet is considered ' + 'insecure. Use SSH or some other encrypted protocol.', + 'HIGH' + )) + + # Most of this is based off of Christian Heimes' work on defusedxml: + # https://pypi.python.org/pypi/defusedxml/#defusedxml-sax + + xml_msg = ('Using {name} to parse untrusted XML data is known to be ' + 'vulnerable to XML attacks. Replace {name} with its ' + 'defusedxml equivalent function.') + + sets.append(utils.build_conf_dict( + 'xml_bad_cElementTree', 'B313', + ['xml.etree.cElementTree.parse', + 'xml.etree.cElementTree.iterparse', + 'xml.etree.cElementTree.fromstring', + 'xml.etree.cElementTree.XMLParser'], + xml_msg + )) + + sets.append(utils.build_conf_dict( + 'xml_bad_ElementTree', 'B314', + ['xml.etree.ElementTree.parse', + 'xml.etree.ElementTree.iterparse', + 'xml.etree.ElementTree.fromstring', + 'xml.etree.ElementTree.XMLParser'], + xml_msg + )) + + sets.append(utils.build_conf_dict( + 'xml_bad_expatreader', 'B315', ['xml.sax.expatreader.create_parser'], + xml_msg + )) + + sets.append(utils.build_conf_dict( + 'xml_bad_expatbuilder', 'B316', + ['xml.dom.expatbuilder.parse', + 'xml.dom.expatbuilder.parseString'], + xml_msg + )) + + sets.append(utils.build_conf_dict( + 'xml_bad_sax', 'B317', + ['xml.sax.parse', + 'xml.sax.parseString', + 'xml.sax.make_parser'], + xml_msg + )) + + sets.append(utils.build_conf_dict( + 'xml_bad_minidom', 'B318', + ['xml.dom.minidom.parse', + 'xml.dom.minidom.parseString'], + xml_msg + )) + + sets.append(utils.build_conf_dict( + 'xml_bad_pulldom', 'B319', + ['xml.dom.pulldom.parse', + 'xml.dom.pulldom.parseString'], + xml_msg + )) + + sets.append(utils.build_conf_dict( + 'xml_bad_etree', 'B320', + ['lxml.etree.parse', + 'lxml.etree.fromstring', + 'lxml.etree.RestrictedElement', + 'lxml.etree.GlobalParserTLS', + 'lxml.etree.getDefaultParser', + 'lxml.etree.check_docinfo'], + xml_msg + )) + + # end of XML tests + + sets.append(utils.build_conf_dict( + 'ftplib', 'B321', ['ftplib.*'], + 'FTP-related funtions are being called. FTP is considered ' + 'insecure. Use SSH/SFTP/SCP or some other encrypted protocol.', + 'HIGH' + )) + + return {'Call': sets} diff --git a/bandit/blacklists/imports.py b/bandit/blacklists/imports.py new file mode 100644 index 00000000..d0a5ae06 --- /dev/null +++ b/bandit/blacklists/imports.py @@ -0,0 +1,81 @@ +# -*- coding:utf-8 -*- +# +# Copyright 2016 Hewlett-Packard Development Company, L.P. +# +# 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. + + +from bandit.blacklists import utils + + +def gen_blacklist(): + """Generate a list of items to blacklist. + + Methods of this type, "bandit.blacklist" plugins, are used to build a list + of items that bandit's built in blacklisting tests will use to trigger + issues. They replace the older blacklist* test plugins and allow + blacklisted items to have a unique bandit ID for filtering and profile + usage. + + :return: a dictionary mapping node types to a list of blacklist data + """ + + sets = [] + sets.append(utils.build_conf_dict( + 'telnet', 'B401', ['telnetlib'], + 'A telnet-related module is being imported. Telnet is ' + 'considered insecure. Use SSH or some other encrypted protocol.', + 'HIGH' + )) + + sets.append(utils.build_conf_dict( + 'ftp', 'B402', ['ftplib'], + 'A FTP-related module is being imported. FTP is considered ' + 'insecure. Use SSH/SFTP/SCP or some other encrypted protocol.', + 'HIGH' + )) + + sets.append(utils.build_conf_dict( + 'info_libs', 'B403', ['pickle', 'cPickle', 'subprocess', 'Crypto'], + 'Consider possible security implications associated with ' + '{name} module.', 'LOW' + )) + + # Most of this is based off of Christian Heimes' work on defusedxml: + # https://pypi.python.org/pypi/defusedxml/#defusedxml-sax + + sets.append(utils.build_conf_dict( + 'xml_libs', 'B404', + ['xml.etree.cElementTree', + 'xml.etree.ElementTree', + 'xml.sax.expatreader', + 'xml.sax', + 'xml.dom.expatbuilder', + 'xml.dom.minidom', + 'xml.dom.pulldom', + 'lxml.etree', + 'lxml'], + 'Using {name} to parse untrusted XML data is known to be ' + 'vulnerable to XML attacks. Replace {name} with the equivalent ' + 'defusedxml package.', 'LOW' + )) + + sets.append(utils.build_conf_dict( + 'xml_libs_high', 'B405', ['xmlrpclib'], + 'Using {name} to parse untrusted XML data is known to be ' + 'vulnerable to XML attacks. Use defused.xmlrpc.monkey_patch() ' + 'function to monkey-patch xmlrpclib and mitigate XML ' + 'vulnerabilities.', 'HIGH' + )) + + return {'Import': sets, 'ImportFrom': sets, 'Call': sets} diff --git a/bandit/blacklists/utils.py b/bandit/blacklists/utils.py new file mode 100644 index 00000000..f14e4872 --- /dev/null +++ b/bandit/blacklists/utils.py @@ -0,0 +1,22 @@ +# -*- coding:utf-8 -*- +# +# Copyright 2016 Hewlett-Packard Development Company, L.P. +# +# 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. + + +def build_conf_dict(name, bid, qualnames, message, level='MEDIUM'): + """Build and return a blacklist configuration dict.""" + + return {'name': name, 'id': bid, 'message': message, + 'qualnames': qualnames, 'level': level} diff --git a/bandit/core/blacklisting.py b/bandit/core/blacklisting.py new file mode 100644 index 00000000..973e7aed --- /dev/null +++ b/bandit/core/blacklisting.py @@ -0,0 +1,61 @@ +# -*- coding:utf-8 -*- +# +# Copyright 2016 Hewlett-Packard Development Company, L.P. +# +# 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. + +import ast +import fnmatch + +from bandit.core import extension_loader +from bandit.core import issue + + +def report_issue(check, name): + return issue.Issue( + severity=check['level'], confidence='HIGH', + text=check['message'].replace('{name}', name), + ident=name, test_id=check["id"]) + + +def blacklist(context): + blacklists = extension_loader.MANAGER.blacklist + node_type = context.node.__class__.__name__ + if node_type not in blacklists: + return + + if node_type == 'Call': + func = context.node.func + if isinstance(func, ast.Name) and func.id == '__import__': + if len(context.node.args): + name = context.node.args[0].s + else: + name = "" # handle '__import__()' + else: + name = context.call_function_name_qual + for check in blacklists[node_type]: + for qn in check['qualnames']: + if fnmatch.fnmatch(name, qn): + return report_issue(check, name) + + if node_type.startswith('Import'): + prefix = "" + if node_type == "ImportFrom": + if context.node.module is not None: + prefix = context.node.module + "." + + for check in blacklists[node_type]: + for name in context.node.names: + for qn in check['qualnames']: + if (prefix + name.name).startswith(qn): + return report_issue(check, name.name) diff --git a/bandit/core/extension_loader.py b/bandit/core/extension_loader.py index ac4c6036..aac79453 100644 --- a/bandit/core/extension_loader.py +++ b/bandit/core/extension_loader.py @@ -16,15 +16,18 @@ from __future__ import print_function import sys +import six from stevedore import extension class Manager(object): def __init__(self, formatters_namespace='bandit.formatters', - plugins_namespace='bandit.plugins'): + plugins_namespace='bandit.plugins', + blacklists_namespace='bandit.blacklists'): # Cache the extension managers, loaded extensions, and extension names self.load_formatters(formatters_namespace) self.load_plugins(plugins_namespace) + self.load_blacklists(blacklists_namespace) def load_formatters(self, formatters_namespace): self.formatters_mgr = extension.ExtensionManager( @@ -63,6 +66,18 @@ class Manager(object): def get_plugin_id(self, plugin_name): return self.plugin_name_to_id.get(plugin_name) + def load_blacklists(self, blacklist_namespace): + self.blacklists_mgr = extension.ExtensionManager( + namespace=blacklist_namespace, + invoke_on_load=False, + verify_requirements=False, + ) + self.blacklist = {} + blacklist = list(self.blacklists_mgr) + for item in blacklist: + for key, val in six.iteritems(item.plugin()): + self.blacklist.setdefault(key, []).extend(val) + # Using entry-points and pkg_resources *can* be expensive. So let's load these # once, store them on the object, and have a module global object for # accessing them. After the first time this module is imported, it should save diff --git a/bandit/core/issue.py b/bandit/core/issue.py index 390a014a..ea3bde07 100644 --- a/bandit/core/issue.py +++ b/bandit/core/issue.py @@ -25,14 +25,14 @@ import linecache class Issue(object): def __init__(self, severity, confidence=constants.CONFIDENCE_DEFAULT, - text="", ident=None, lineno=None): + text="", ident=None, lineno=None, test_id=""): self.severity = severity self.confidence = confidence self.text = text self.ident = ident self.fname = "" self.test = "" - self.test_id = "" + self.test_id = test_id self.lineno = lineno self.linerange = [] diff --git a/bandit/core/test_set.py b/bandit/core/test_set.py index 15e982df..de1831b9 100644 --- a/bandit/core/test_set.py +++ b/bandit/core/test_set.py @@ -22,6 +22,8 @@ import logging import sys import warnings +from bandit.core import blacklisting +from bandit.core import extension_loader from bandit.core import utils @@ -37,6 +39,11 @@ class BanditTestSet(): filter_list = self._filter_list_from_config(profile=profile) self.load_tests(filter=filter_list) + # load blacklists + for key in extension_loader.MANAGER.blacklist.keys(): + value = self.tests.setdefault(key, {}) + value["blacklist"] = blacklisting.blacklist + def _filter_list_from_config(self, profile=None): # will create an (include,exclude) list tuple from a specified name # config section diff --git a/bandit/core/tester.py b/bandit/core/tester.py index 499b8929..ac63346c 100644 --- a/bandit/core/tester.py +++ b/bandit/core/tester.py @@ -74,7 +74,8 @@ class BanditTester(): result.lineno = temp_context['lineno'] result.linerange = temp_context['linerange'] result.test = test.__name__ - result.test_id = test._test_id + if result.test_id == "": + result.test_id = test._test_id self.results.append(result) diff --git a/bandit/plugins/blacklist_calls.py b/bandit/plugins/blacklist_calls.py deleted file mode 100644 index ef1ff160..00000000 --- a/bandit/plugins/blacklist_calls.py +++ /dev/null @@ -1,379 +0,0 @@ -# -*- coding:utf-8 -*- -# -# Copyright 2014 Hewlett-Packard Development Company, L.P. -# -# 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. - -r""" -================================= -B301: Test for black listed calls -================================= - -A number of Python methods and functions are known to have potential security -implications. The blacklist calls plugin test is designed to detect the use of -these methods by scanning code for method calls and checking for their presence -in a configurable blacklist. The scanned calls are fully qualified and -de-aliased prior to checking. To illustrate this, imagine a check for -"evil.thing()" running on the following example code: - -.. code-block:: python - - import evil as good - - good.thing() - thing() - -This would generate a warning about calling `evil.thing()` despite the module -being aliased as `good`. It would also not generate a warning on the call to -`thing()` in the local module, as it's fully qualified name will not match. - -Each of the provided blacklisted calls can be grouped such that they generate -appropriate warnings (message, severity) and a token `{func}` may be used -in the provided output message, to be replaced with the actual method name. - -Due to the nature of the test, confidence is always reported as HIGH - -**Config Options:** - -.. code-block:: yaml - - blacklist_calls: - bad_name_sets: - - pickle: - qualnames: - - pickle.loads - - pickle.load - - pickle.Unpickler - - cPickle.loads - - cPickle.load - - cPickle.Unpickler - message: > - Pickle library appears to be in use, possible security - issue. - - marshal: - qualnames: [marshal.load, marshal.loads] - message: > - Deserialization with the {func} is possibly dangerous. - level: LOW - -:Example: - -.. code-block:: none - - >> Issue: Pickle library appears to be in use, possible security issue. - - Severity: Medium Confidence: High - Location: ./examples/pickle_deserialize.py:20 - 19 serialized = cPickle.dumps({(): []}) - 20 print(cPickle.loads(serialized)) - 21 - -.. seealso:: - - - https://security.openstack.org - -.. versionadded:: 0.9.0 - -""" - -import fnmatch - -import bandit -from bandit.core import test_properties as test - - -_cached_blacklist_checks = [] -_cached_blacklist_config = None # FIXME(tkelsey): there is no point in this .. - - -def _build_conf_dict(name, qualnames, message, level='MEDIUM'): - return {name: {'message': message, 'qualnames': qualnames, 'level': level}} - - -def gen_config(name): - if 'blacklist_calls' == name: - sets = [] - - sets.append(_build_conf_dict( - 'pickle', - ['pickle.loads', - 'pickle.load', - 'pickle.Unpickler', - 'cPickle.loads', - 'cPickle.load', - 'cPickle.Unpickler'], - 'Pickle library appears to be in use, possible security issue.' - )) - - sets.append(_build_conf_dict( - 'marshal', ['marshal.load', 'marshal.loads'], - 'Deserialization with the marshal module is possibly dangerous.' - )) - - sets.append(_build_conf_dict( - 'md5', - ['hashlib.md5', - 'Crypto.Hash.MD2.new', - 'Crypto.Hash.MD4.new', - 'Crypto.Hash.MD5.new', - 'cryptography.hazmat.primitives.hashes.MD5'], - 'Use of insecure MD2, MD4, or MD5 hash function.' - )) - - sets.append(_build_conf_dict( - 'ciphers', - ['Crypto.Cipher.ARC2.new', - 'Crypto.Cipher.ARC4.new', - 'Crypto.Cipher.Blowfish.new', - 'Crypto.Cipher.DES.new', - 'Crypto.Cipher.XOR.new', - 'cryptography.hazmat.primitives.ciphers.algorithms.ARC4', - 'cryptography.hazmat.primitives.ciphers.algorithms.Blowfish', - 'cryptography.hazmat.primitives.ciphers.algorithms.IDEA'], - 'Use of insecure cipher {func}. Replace with a known secure' - ' cipher such as AES.', - 'HIGH' - )) - - sets.append(_build_conf_dict( - 'cipher_modes', - ['cryptography.hazmat.primitives.ciphers.modes.ECB'], - 'Use of insecure cipher mode {func}.' - )) - - sets.append(_build_conf_dict( - 'mktemp_q', ['tempfile.mktemp'], - 'Use of insecure and deprecated function (mktemp).' - )) - - sets.append(_build_conf_dict( - 'eval', ['eval'], - 'Use of possibly insecure function - consider using safer ' - 'ast.literal_eval.' - )) - - sets.append(_build_conf_dict( - 'mark_safe', ['mark_safe'], - 'Use of mark_safe() may expose cross-site scripting ' - 'vulnerabilities and should be reviewed.' - )) - - sets.append(_build_conf_dict( - 'httpsconnection', - ['httplib.HTTPSConnection', - 'http.client.HTTPSConnection', - 'six.moves.http_client.HTTPSConnection'], - 'Use of HTTPSConnection does not provide security, see ' - 'https://wiki.openstack.org/wiki/OSSN/OSSN-0033' - )) - - sets.append(_build_conf_dict( - 'urllib_urlopen', - ['urllib.urlopen', - 'urllib.request.urlopen', - 'urllib.urlretrieve', - 'urllib.request.urlretrieve', - 'urllib.URLopener', - 'urllib.request.URLopener', - 'urllib.FancyURLopener', - 'urllib.request.FancyURLopener', - 'urllib2.urlopen', - 'urllib2.Request', - 'six.moves.urllib.request.urlopen', - 'six.moves.urllib.request.urlretrieve', - 'six.moves.urllib.request.URLopener', - 'six.moves.urllib.request.FancyURLopener'], - 'Audit url open for permitted schemes. Allowing use of file:/ or ' - 'custom schemes is often unexpected.' - )) - - sets.append(_build_conf_dict( - 'random', - ['random.random', - 'random.randrange', - 'random.randint', - 'random.choice', - 'random.uniform', - 'random.triangular'], - 'Standard pseudo-random generators are not suitable for ' - 'security/cryptographic purposes.', - 'LOW' - )) - - sets.append(_build_conf_dict( - 'telnetlib', ['telnetlib.*'], - 'Telnet-related funtions are being called. Telnet is considered ' - 'insecure. Use SSH or some other encrypted protocol.', - 'HIGH' - )) - - sets.append(_build_conf_dict( - 'ftplib', ['ftplib.*'], - 'FTP-related funtions are being called. FTP is considered ' - 'insecure. Use SSH/SFTP/SCP or some other encrypted protocol.', - 'HIGH' - )) - - # Most of this is based off of Christian Heimes' work on defusedxml: - # https://pypi.python.org/pypi/defusedxml/#defusedxml-sax - - xml_msg = ('Using {func} to parse untrusted XML data is known to be ' - 'vulnerable to XML attacks. Replace {func} with its ' - 'defusedxml equivalent function.') - - sets.append(_build_conf_dict( - 'xml_bad_cElementTree', - ['xml.etree.cElementTree.parse', - 'xml.etree.cElementTree.iterparse', - 'xml.etree.cElementTree.fromstring', - 'xml.etree.cElementTree.XMLParser'], - xml_msg - )) - - sets.append(_build_conf_dict( - 'xml_bad_ElementTree', - ['xml.etree.ElementTree.parse', - 'xml.etree.ElementTree.iterparse', - 'xml.etree.ElementTree.fromstring', - 'xml.etree.ElementTree.XMLParser'], - xml_msg - )) - - sets.append(_build_conf_dict( - 'xml_bad_expatreader', ['xml.sax.expatreader.create_parser'], - xml_msg - )) - - sets.append(_build_conf_dict( - 'xml_bad_expatbuilder', - ['xml.dom.expatbuilder.parse', - 'xml.dom.expatbuilder.parseString'], - xml_msg - )) - - sets.append(_build_conf_dict( - 'xml_bad_sax', - ['xml.sax.parse', - 'xml.sax.parseString', - 'xml.sax.make_parser'], - xml_msg - )) - - sets.append(_build_conf_dict( - 'xml_bad_minidom', - ['xml.dom.minidom.parse', - 'xml.dom.minidom.parseString'], - xml_msg - )) - - sets.append(_build_conf_dict( - 'xml_bad_pulldom', - ['xml.dom.pulldom.parse', - 'xml.dom.pulldom.parseString'], - xml_msg - )) - - sets.append(_build_conf_dict( - 'xml_bad_etree', - ['lxml.etree.parse', - 'lxml.etree.fromstring', - 'lxml.etree.RestrictedElement', - 'lxml.etree.GlobalParserTLS', - 'lxml.etree.getDefaultParser', - 'lxml.etree.check_docinfo'], - xml_msg - )) - - return {'bad_name_sets': sets} - - -@test.takes_config -@test.checks('Call') -@test.test_id('B301') -def blacklist_calls(context, config): - _ensure_cache(config) - checks = _cached_blacklist_checks - - # for each check, go through and see if it matches all qualifications - for qualnames, names, message_tpl, level, params in checks: - confidence = 'HIGH' - does_match = True - # item 0=qualnames, 1=names, 2=message, 3=level, 4=params - if does_match and qualnames: - # match the qualname - respect wildcards if present - does_match = any( - fnmatch.fnmatch(context.call_function_name_qual, qn) - for qn in qualnames) - - if does_match and names: - does_match = any(context.call_function_name == n for n in names) - - if does_match and params: - matched_p = False - for p in params: - for arg_num in range(0, context.call_args_count - 1): - if p == context.get_call_arg_at_position(arg_num): - matched_p = True - if not matched_p: - does_match = False - - if does_match: - message = message_tpl.replace("{func}", - context.call_function_name_qual) - - return bandit.Issue( - severity=level, confidence=confidence, - text=message, - ident=context.call_function_name_qual - ) - - -def _ensure_cache(config): - global _cached_blacklist_config - if _cached_blacklist_checks and config is _cached_blacklist_config: - return - - _cached_blacklist_config = config - if config is not None and 'bad_name_sets' in config: - sets = config['bad_name_sets'] - else: - sets = [] - - # load all the checks from the config file - for cur_item in sets: - for blacklist_item in cur_item: - blacklist_object = cur_item[blacklist_item] - cur_check = _get_tuple_for_item(blacklist_object) - # skip bogus checks - if cur_check: - _cached_blacklist_checks.append(cur_check) - - -def _get_tuple_for_item(blacklist_object): - level_map = {'LOW': bandit.LOW, 'MEDIUM': bandit.MEDIUM, - 'HIGH': bandit.HIGH} - - # if the item we got passed isn't a dictionary, do nothing with this object - if not isinstance(blacklist_object, dict): - return None - - # not all of the fields will be set, so all have default fallbacks - qualnames = blacklist_object.get('qualnames') - names = blacklist_object.get('names') - message = blacklist_object.get('message', '') - params = blacklist_object.get('params') - - level_name = blacklist_object.get('level', 'MEDIUM').upper() - level = level_map.get(level_name, 'MEDIUM') - - return (qualnames, names, message, level, params) diff --git a/bandit/plugins/blacklist_imports.py b/bandit/plugins/blacklist_imports.py deleted file mode 100644 index 8bed9356..00000000 --- a/bandit/plugins/blacklist_imports.py +++ /dev/null @@ -1,296 +0,0 @@ -# -*- coding:utf-8 -*- -# -# Copyright 2014 Hewlett-Packard Development Company, L.P. -# -# 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. - -import bandit -from bandit.core import test_properties as test - - -def _build_conf_dict(name, imports, message, level='MEDIUM'): - return {name: {'message': message, 'imports': imports, 'level': level}} - - -def gen_config(name): - if 'blacklist_imports' == name: - sets = [] - - sets.append(_build_conf_dict( - 'telnet', ['telnetlib'], - 'A telnet-related module is being imported. Telnet is ' - 'considered insecure. Use SSH or some other encrypted protocol.', - 'HIGH' - )) - - sets.append(_build_conf_dict( - 'ftp', ['ftplib'], - 'A FTP-related module is being imported. FTP is considered ' - 'insecure. Use SSH/SFTP/SCP or some other encrypted protocol.', - 'HIGH' - )) - - sets.append(_build_conf_dict( - 'info_libs', ['pickle', 'cPickle', 'subprocess', 'Crypto'], - 'Consider possible security implications associated with ' - '{module} module.', 'LOW' - )) - - # Most of this is based off of Christian Heimes' work on defusedxml: - # https://pypi.python.org/pypi/defusedxml/#defusedxml-sax - - sets.append(_build_conf_dict( - 'xml_libs', - ['xml.etree.cElementTree', - 'xml.etree.ElementTree', - 'xml.sax.expatreader', - 'xml.sax', - 'xml.dom.expatbuilder', - 'xml.dom.minidom', - 'xml.dom.pulldom', - 'lxml.etree', - 'lxml'], - 'Using {module} to parse untrusted XML data is known to be ' - 'vulnerable to XML attacks. Replace {module} with the equivalent ' - 'defusedxml package.', 'LOW' - )) - - sets.append(_build_conf_dict( - 'xml_libs_high', ['xmlrpclib'], - 'Using {module} to parse untrusted XML data is known to be ' - 'vulnerable to XML attacks. Use defused.xmlrpc.monkey_patch() ' - 'function to monkey-patch xmlrpclib and mitigate XML ' - 'vulnerabilities.', 'HIGH' - )) - - return {'bad_import_sets': sets} - - -@test.takes_config -@test.checks('Import', 'ImportFrom') -@test.test_id('B401') -def blacklist_imports(context, config): - """**B401: Test for blacklisted imports** - - A number of Python modules are known to provide collections of - functionality with potential security implications. The blacklist imports - plugin test is designed to detect the use of these modules by scanning code - for `import` statements and checking for the imported modules presence in a - configurable blacklist. The imported modules are fully qualified and - de-aliased prior to checking. To illustrate this, imagine a check for - "module.evil" running on the following example code: - - .. code-block:: python - - import module # no warning - import module.evil # warning - from module import evil # warning - from module import evil as good # warning - - This would generate a warning about importing `module.evil` in each of the - last three cases, despite the module being aliased as `good` in one of - them. It would also not generate a warning on the first import - (of `module`) as it's fully qualified name will not match. - - Each of the provided blacklisted modules can be grouped such that they - generate appropriate warnings (message, severity) and a token `{module}` - may be used in the provided output message, to be replaced with the actual - module name. - - Due to the nature of the test, confidence is always reported as HIGH - - **Config Options:** - - .. code-block:: yaml - - blacklist_imports: - bad_import_sets: - - xml_libs: - imports: - - xml.etree.cElementTree - - xml.etree.ElementTree - - xml.sax.expatreader - - xml.sax - - xml.dom.expatbuilder - - xml.dom.minidom - - xml.dom.pulldom - - lxml.etree - - lxml - message: > - Using {module} to parse untrusted XML data is known to - be vulnerable to XML attacks. Replace {module} with the - equivalent defusedxml package. - level: LOW - - - :Example: - - .. code-block:: none - - >> Issue: Using xml.sax to parse untrusted XML data is known to be - vulnerable to XML attacks. Replace xml.sax with the equivalent - defusedxml package. - - Severity: Low Confidence: High - Location: ./examples/xml_sax.py:1 - 1 import xml.sax - 2 from xml import sax - - >> Issue: Using xml.sax.parseString to parse untrusted XML data is - known to be vulnerable to XML attacks. Replace xml.sax.parseString with - its defusedxml equivalent function. - - Severity: Medium Confidence: High - Location: ./examples/xml_sax.py:21 - 20 # bad - 21 xml.sax.parseString(xmlString, ExampleContentHandler()) - 22 xml.sax.parse('notaxmlfilethatexists.xml', ExampleContentHandler()) - - .. seealso:: - - - https://security.openstack.org - - .. versionadded:: 0.9.0 - """ - - checks = _load_checks(config) - - # for each check, go through and see if it matches all qualifications - for check in checks: - # item 0=import, 1=message, 2=level - if check[0]: - for im in check[0]: - if context.is_module_being_imported(im): - return _get_result(check, im) - - -@test.takes_config('blacklist_imports') -@test.checks('Call') -@test.test_id('B402') -def blacklist_import_func(context, config): - """**B402: Test for blacklisted import functions** - - This test is in all ways identical blacklist_imports. However, it - is designed to catch modules that have been imported using Python's special - builtin import function, `__import__()`. For example, running a test on the - following code for `module.evil` would warn as shown: - - .. code-block:: python - - __import__('module') # no warning - __import__('module.evil') # warning - - This test shares the configuration provided for the standard - blacklist_imports test. - - :Example: - - .. code-block:: none - - >> Issue: Using xml.sax to parse untrusted XML data is known to be - vulnerable to XML attacks. Replace xml.sax with the equivalent - defusedxml package. - - Severity: Low Confidence: High - Location: ./examples/xml_sax.py:1 - 1 import xml.sax - 2 from xml import sax - - >> Issue: Using xml.sax.parseString to parse untrusted XML data is - known to be vulnerable to XML attacks. Replace xml.sax.parseString with - its defusedxml equivalent function. - - Severity: Medium Confidence: High - Location: ./examples/xml_sax.py:21 - 20 # bad - 21 xml.sax.parseString(xmlString, ExampleContentHandler()) - 22 xml.sax.parse('notaxmlfilethatexists.xml', ExampleContentHandler()) - - - .. seealso:: - - - https://security.openstack.org - - .. versionadded:: 0.9.0 - """ - checks = _load_checks(config) - if context.call_function_name_qual == '__import__': - for check in checks: - # item 0=import, 1=message, 2=level - if check[0]: - for im in check[0]: - if len(context.call_args) and im == context.call_args[0]: - return _get_result(check, im) - - -def _load_checks(config): - # load all the checks from the config file - if config is not None and 'bad_import_sets' in config: - sets = config['bad_import_sets'] - else: - sets = [] - - checks = [] - for cur_item in sets: - for blacklist_item in cur_item: - blacklist_object = cur_item[blacklist_item] - cur_check = _get_tuple_for_item(blacklist_object) - # skip bogus checks - if cur_check: - checks.append(cur_check) - return checks - - -def _get_tuple_for_item(blacklist_object): - # default values - imports = None - message = "" - level = 'MEDIUM' - - # if the item we got passed isn't a dictionary, do nothing with the object; - # if the item we got passed doesn't have an imports field, we can't do - # anything with this. Return None - if (not isinstance(blacklist_object, dict) or - 'imports' not in blacklist_object): - return None - - imports = blacklist_object['imports'] - - if 'message' in blacklist_object: - message = blacklist_object['message'] - - if 'level' in blacklist_object: - if blacklist_object['level'] == 'HIGH': - level = 'HIGH' - elif blacklist_object['level'] == 'MEDIUM': - level = 'MEDIUM' - elif blacklist_object['level'] == 'LOW': - level = 'LOW' - - return_tuple = (imports, message, level) - return return_tuple - - -def _get_result(check, im): - # substitute '{module}' for the imported module name - message = check[1].replace('{module}', im) - - level = None - if check[2] == 'HIGH': - level = bandit.HIGH - elif check[2] == 'MEDIUM': - level = bandit.MEDIUM - elif check[2] == 'LOW': - level = bandit.LOW - - return bandit.Issue(severity=level, confidence=bandit.HIGH, text=message) diff --git a/setup.cfg b/setup.cfg index fc4d0b27..336d2402 100644 --- a/setup.cfg +++ b/setup.cfg @@ -26,6 +26,9 @@ console_scripts = bandit = bandit.cli.main:main bandit-config-generator = bandit.cli.config_generator:main bandit-baseline = bandit.cli.baseline:main +bandit.blacklists = + calls = bandit.blacklists.calls:gen_blacklist + imports = bandit.blacklists.imports:gen_blacklist bandit.formatters = csv = bandit.formatters.csv:report json = bandit.formatters.json:report @@ -40,13 +43,6 @@ bandit.plugins = # bandit/plugins/asserts.py assert_used = bandit.plugins.asserts:assert_used - # bandit/plugins/blacklist_calls.py - blacklist_calls = bandit.plugins.blacklist_calls:blacklist_calls - - # bandit/plugins/blacklist_imports.py - blacklist_imports = bandit.plugins.blacklist_imports:blacklist_imports - blacklist_import_func = bandit.plugins.blacklist_imports:blacklist_import_func - # bandit/plugins/crypto_request_no_cert_validation.py request_with_no_cert_validation = bandit.plugins.crypto_request_no_cert_validation:request_with_no_cert_validation diff --git a/tests/functional/test_functional.py b/tests/functional/test_functional.py index f6e6961b..2ee534de 100644 --- a/tests/functional/test_functional.py +++ b/tests/functional/test_functional.py @@ -116,12 +116,14 @@ class FunctionalTests(testtools.TestCase): def test_crypto_md5(self): '''Test the `hashlib.md5` example.''' - expect = {'SEVERITY': {'MEDIUM': 8}, 'CONFIDENCE': {'HIGH': 8}} + expect = {'SEVERITY': {'MEDIUM': 8, 'LOW': 3}, + 'CONFIDENCE': {'HIGH': 11}} self.check_example('crypto-md5.py', expect) def test_ciphers(self): '''Test the `Crypto.Cipher` example.''' - expect = {'SEVERITY': {'LOW': 1, 'HIGH': 8}, 'CONFIDENCE': {'HIGH': 9}} + expect = {'SEVERITY': {'LOW': 8, 'HIGH': 8}, + 'CONFIDENCE': {'HIGH': 16}} self.check_example('ciphers.py', expect) def test_cipher_modes(self): @@ -384,8 +386,8 @@ class FunctionalTests(testtools.TestCase): 'CONFIDENCE': {'HIGH': 2, 'MEDIUM': 2}} self.check_example('xml_minidom.py', expect) - expect = {'SEVERITY': {'LOW': 1, 'HIGH': 6}, - 'CONFIDENCE': {'HIGH': 1, 'MEDIUM': 6}} + expect = {'SEVERITY': {'LOW': 2, 'HIGH': 6}, + 'CONFIDENCE': {'HIGH': 2, 'MEDIUM': 6}} self.check_example('xml_sax.py', expect) def test_asserts(self): @@ -436,8 +438,8 @@ class FunctionalTests(testtools.TestCase): def test_weak_cryptographic_key(self): '''Test for weak key sizes.''' expect = { - 'SEVERITY': {'MEDIUM': 5, 'HIGH': 4}, - 'CONFIDENCE': {'HIGH': 9} + 'SEVERITY': {'LOW': 2, 'MEDIUM': 5, 'HIGH': 4}, + 'CONFIDENCE': {'HIGH': 11} } self.check_example('weak_cryptographic_key_sizes.py', expect) diff --git a/tests/functional/test_runtime.py b/tests/functional/test_runtime.py index 57b45b7f..c545ecec 100644 --- a/tests/functional/test_runtime.py +++ b/tests/functional/test_runtime.py @@ -96,7 +96,7 @@ class RuntimeTests(testtools.TestCase): self.assertIn("Low: 2", output) self.assertIn("High: 2", output) self.assertIn("Files skipped (0):", output) - self.assertIn("Issue: [B401:blacklist_imports] Consider possible", + self.assertIn("Issue: [B403:blacklist] Consider possible", output) self.assertIn("imports.py:2", output) self.assertIn("imports.py:4", output) diff --git a/tests/unit/core/test_manager.py b/tests/unit/core/test_manager.py index 2ba9cb69..dbe0fc95 100644 --- a/tests/unit/core/test_manager.py +++ b/tests/unit/core/test_manager.py @@ -131,7 +131,6 @@ class ManagerTests(testtools.TestCase): self.assertEqual(m.debug, False) self.assertEqual(m.verbose, False) self.assertEqual(m.agg_type, 'file') - self.assertFalse(m.has_tests) def test_matches_globlist(self): self.assertTrue(manager._matches_glob_list('test', ['*tes*']))