Support dynamic loading of tests
This adds config file handling, module import, and function addition to test set.
This commit is contained in:
15
README.md
15
README.md
@@ -20,17 +20,20 @@ Example usage across a code tree:
|
||||
Usage:
|
||||
|
||||
$ ./main.py -h
|
||||
usage: main.py [-h] [-C CONTEXT] [-l] [-d] file [file ...]
|
||||
|
||||
usage: main.py [-h] [-C CONTEXT_LINES] [-t TEST_CONFIG] [-l] [-d]
|
||||
file [file ...]
|
||||
|
||||
Bandit - a Python source code analyzer.
|
||||
|
||||
|
||||
positional arguments:
|
||||
file source file/s to be tested
|
||||
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
-C CONTEXT, --context CONTEXT
|
||||
-C CONTEXT_LINES, --context CONTEXT_LINES
|
||||
number of context lines to print
|
||||
-t TEST_CONFIG, --testconfig TEST_CONFIG
|
||||
test config file (default: bandit.ini)
|
||||
-l, --level results level filter
|
||||
-d, --debug turn on debug mode
|
||||
|
||||
|
||||
|
||||
12
bandit.ini
Normal file
12
bandit.ini
Normal file
@@ -0,0 +1,12 @@
|
||||
[Import]
|
||||
import_name_match = test_imports
|
||||
import_name_telnetlib = test_imports
|
||||
|
||||
[ImportFrom]
|
||||
import_name_match = test_imports
|
||||
|
||||
[Call]
|
||||
call_bad_names = test_calls
|
||||
call_subprocess_popen = test_calls
|
||||
call_no_cert_validation = test_calls
|
||||
call_bad_permissions = test_calls
|
||||
@@ -12,11 +12,11 @@ class BanditManager():
|
||||
scope = []
|
||||
progress = 50
|
||||
|
||||
def __init__(self, debug=False):
|
||||
def __init__(self, test_config, debug=False):
|
||||
self.logger = self._init_logger(debug)
|
||||
self.b_ma = b_meta_ast.BanditMetaAst(self.logger)
|
||||
self.b_rs = b_result_store.BanditResultStore(self.logger)
|
||||
self.b_ts = b_test_set.BanditTestSet(self.logger)
|
||||
self.b_ts = b_test_set.BanditTestSet(self.logger, test_config)
|
||||
|
||||
def get_logger(self):
|
||||
return self.logger
|
||||
|
||||
@@ -1,111 +1,52 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import sys
|
||||
from collections import OrderedDict
|
||||
import utils
|
||||
import _ast
|
||||
import stat
|
||||
|
||||
import ConfigParser
|
||||
|
||||
class BanditTestSet():
|
||||
|
||||
tests = OrderedDict()
|
||||
|
||||
# stubbed tests
|
||||
def _test_import_name_match(self, context):
|
||||
info_on_import = ['pickle', 'subprocess', 'Crypto']
|
||||
for module in info_on_import:
|
||||
if context['module'] == module:
|
||||
return('INFO',
|
||||
"Consider possible security implications"
|
||||
" associated with '%s' module" % module)
|
||||
|
||||
def _test_call_bad_names(self, context):
|
||||
bad_name_sets = [
|
||||
(['pickle.loads', 'pickle.dumps', ],
|
||||
'Pickle library appears to be in use, possible security issue.'),
|
||||
(['hashlib.md5', ],
|
||||
'Use of insecure MD5 hash function.'),
|
||||
(['subprocess.Popen', ],
|
||||
'Use of possibly-insecure system call function '
|
||||
'(subprocess.Popen).'),
|
||||
(['subprocess.call', ],
|
||||
'Use of possibly-insecure system call function '
|
||||
'(subprocess.call).'),
|
||||
(['mktemp', ],
|
||||
'Use of insecure and deprecated function (mktemp).'),
|
||||
(['eval', ],
|
||||
'Use of possibly-insecure function - consider using the safer '
|
||||
'ast.literal_eval().'),
|
||||
]
|
||||
|
||||
# test for 'bad' names defined above
|
||||
for bad_name_set in bad_name_sets:
|
||||
for bad_name in bad_name_set[0]:
|
||||
# if name.startswith(bad_name) or name.endswith(bad_name):
|
||||
if context['qualname'] == bad_name:
|
||||
return('WARN', "%s %s" %
|
||||
(bad_name_set[1],
|
||||
utils.ast_args_to_str(context['call'].args)))
|
||||
|
||||
def _test_call_subprocess_popen(self, context):
|
||||
if context['qualname'] == 'subprocess.Popen':
|
||||
if hasattr(context['call'], 'keywords'):
|
||||
for k in context['call'].keywords:
|
||||
if k.arg == 'shell' and isinstance(k.value, _ast.Name):
|
||||
if k.value.id == 'True':
|
||||
return('ERROR', 'Popen call with shell=True '
|
||||
'identified, security issue. %s' %
|
||||
utils.ast_args_to_str(context['call'].args))
|
||||
|
||||
def _test_call_no_cert_validation(self, context):
|
||||
if 'requests' in context['qualname'] and ('get' in context['name'] or
|
||||
'post' in context['name']):
|
||||
if hasattr(context['call'], 'keywords'):
|
||||
for k in context['call'].keywords:
|
||||
if k.arg == 'verify' and isinstance(k.value, _ast.Name):
|
||||
if k.value.id == 'False':
|
||||
return('ERROR',
|
||||
'Requests call with verify=False '
|
||||
'disabling SSL certificate checks, '
|
||||
'security issue. %s' %
|
||||
utils.ast_args_to_str(context['call'].args))
|
||||
|
||||
def _test_call_bad_permissions(self, context):
|
||||
if 'chmod' in context['name']:
|
||||
if (hasattr(context['call'], 'args')):
|
||||
args = context['call'].args
|
||||
if len(args) == 2 and isinstance(args[1], _ast.Num):
|
||||
if ((args[1].n & stat.S_IWOTH) or
|
||||
(args[1].n & stat.S_IXGRP)):
|
||||
return('ERROR',
|
||||
'Chmod setting a permissive mask '
|
||||
'%s on file (%s).' %
|
||||
(oct(args[1].n), args[0].s))
|
||||
|
||||
# end stubbed tests
|
||||
|
||||
def __init__(self, logger):
|
||||
def __init__(self, logger, test_config):
|
||||
self.logger = logger
|
||||
self.load_tests()
|
||||
self.load_tests(test_config)
|
||||
|
||||
def load_tests(self):
|
||||
# each test should have a name, target node/s, and function...
|
||||
# for now, stub in some tests
|
||||
self.tests['import_name_match'] = {
|
||||
'targets': ['Import', 'ImportFrom'],
|
||||
'function': self._test_import_name_match}
|
||||
self.tests['call_bad_names'] = {
|
||||
'targets': ['Call', ],
|
||||
'function': self._test_call_bad_names}
|
||||
self.tests['call_subprocess_popen'] = {
|
||||
'targets': ['Call', ],
|
||||
'function': self._test_call_subprocess_popen}
|
||||
self.tests['call_no_cert_validation'] = {
|
||||
'targets': ['Call', ],
|
||||
'function': self._test_call_no_cert_validation}
|
||||
self.tests['call_bad_permissions'] = {
|
||||
'targets': ['Call', ],
|
||||
'function': self._test_call_bad_permissions}
|
||||
def load_tests(self, test_config):
|
||||
#each test should be keyed with name and have targets and function...
|
||||
config = ConfigParser.RawConfigParser()
|
||||
config.read(test_config)
|
||||
self.tests = OrderedDict()
|
||||
directory = 'plugins' #TODO - parametize this at runtime
|
||||
for target in config.sections():
|
||||
for (test_name_func, test_name_mod) in config.items(target):
|
||||
if test_name_func not in self.tests:
|
||||
self.tests[test_name_func] = {'targets':[]}
|
||||
test_mod = None
|
||||
try:
|
||||
test_mod = __import__('%s.%s' % (directory, test_name_mod),
|
||||
fromlist=[directory,])
|
||||
except ImportError as e:
|
||||
self.logger.error("could not import test module '%s.%s'" %
|
||||
(directory, test_name_mod))
|
||||
self.logger.error("\tdetail: '%s'" % (str(e)))
|
||||
del(self.tests[test_name_func])
|
||||
#continue
|
||||
sys.exit(2)
|
||||
else:
|
||||
try:
|
||||
test_func = getattr(test_mod, test_name_func)
|
||||
except AttributeError as e:
|
||||
self.logger.error("could not locate test function"
|
||||
" '%s' in module '%s.%s'" %
|
||||
(test_name_func, directory,
|
||||
test_name_mod))
|
||||
del(self.tests[test_name_func])
|
||||
#continue
|
||||
sys.exit(2)
|
||||
else:
|
||||
self.tests[test_name_func]['function'] = test_func
|
||||
self.tests[test_name_func]['targets'].append(target)
|
||||
|
||||
def get_tests(self, nodetype):
|
||||
self.logger.debug('get_tests called with nodetype: %s' % nodetype)
|
||||
|
||||
21
examples/imports-telnetlib.py
Normal file
21
examples/imports-telnetlib.py
Normal file
@@ -0,0 +1,21 @@
|
||||
import telnetlib
|
||||
import getpass
|
||||
|
||||
host = sys.argv[1]
|
||||
|
||||
username = raw_input('Username:')
|
||||
password = getpass.getpass()
|
||||
tn = telnetlib.Telnet(host)
|
||||
|
||||
tn.read_until("login: ")
|
||||
tn.write(username + "\n")
|
||||
if password:
|
||||
tn.read_until("Password: ")
|
||||
tn.write(password + "\n")
|
||||
|
||||
tn.write("ls\n")
|
||||
tn.write("exit\n")
|
||||
|
||||
print tn.read_all()
|
||||
|
||||
|
||||
10
examples/mktemp.py
Normal file
10
examples/mktemp.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from tempfile import mktemp
|
||||
import tempfile.mktemp as mt
|
||||
import tempfile as tmp
|
||||
|
||||
foo = 'hi'
|
||||
|
||||
mktemp(foo)
|
||||
tempfile.mktemp('foo')
|
||||
mt(foo)
|
||||
tmp.mktemp(foo)
|
||||
@@ -1,16 +1,15 @@
|
||||
import os
|
||||
import stat
|
||||
|
||||
keyfile = 'foo'
|
||||
|
||||
os.chmod('/etc/passwd', 0777)
|
||||
os.chmod('/etc/passwd', 0227)
|
||||
os.chmod('/etc/passwd', 07)
|
||||
os.chmod('/etc/passwd', 0664)
|
||||
os.chmod('/etc/passwd', 0777)
|
||||
os.chmod('/etc/passwd', 0777)
|
||||
os.chmod('/etc/passwd', 0777)
|
||||
os.chmod('/etc/passwd', 0777)
|
||||
os.chmod('~/.bashrc', 511)
|
||||
os.chmod('/etc/hosts', 0o777)
|
||||
os.chmod('/etc/hosts', 0o777)
|
||||
os.chmod('/tmp/oh_hai', 0x1ff)
|
||||
os.chmod('/etc/passwd', stat.S_IRWXU)
|
||||
os.chmod(key_file, 0o777)
|
||||
|
||||
12
main.py
12
main.py
@@ -3,14 +3,18 @@
|
||||
import sys, argparse
|
||||
from bandit import manager as b_manager
|
||||
|
||||
default_test_config = 'bandit.ini'
|
||||
|
||||
if __name__=='__main__':
|
||||
parser = argparse.ArgumentParser(description='Bandit - a Python source code analyzer.')
|
||||
parser.add_argument('files', metavar='file', type=str, nargs='+',
|
||||
help='source file/s to be tested')
|
||||
parser.add_argument('-C', '--context', dest='context', action='store',
|
||||
default=0, type=int,
|
||||
parser.add_argument('-C', '--context', dest='context_lines',
|
||||
action='store', default=0, type=int,
|
||||
help='number of context lines to print')
|
||||
parser.add_argument('-t', '--testconfig', dest='test_config',
|
||||
action='store', default=default_test_config, type=str,
|
||||
help='test config file (default: %s)' % default_test_config)
|
||||
parser.add_argument('-l', '--level', dest='level', action='count',
|
||||
default=1, help='results level filter')
|
||||
parser.add_argument('-d', '--debug', dest='debug', action='store_true',
|
||||
@@ -19,9 +23,9 @@ if __name__=='__main__':
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
b_mgr = b_manager.BanditManager(args.debug)
|
||||
b_mgr = b_manager.BanditManager(args.test_config, args.debug)
|
||||
b_mgr.run_scope(args.files)
|
||||
b_mgr.output_results(args.context, args.level - 1)
|
||||
b_mgr.output_results(args.context_lines, args.level - 1)
|
||||
#b_mgr.output_metaast()
|
||||
|
||||
|
||||
|
||||
0
plugins/__init__.py
Normal file
0
plugins/__init__.py
Normal file
74
plugins/test_calls.py
Normal file
74
plugins/test_calls.py
Normal file
@@ -0,0 +1,74 @@
|
||||
##
|
||||
# generic tests against Call nodes
|
||||
##
|
||||
|
||||
from bandit import utils
|
||||
import _ast
|
||||
import stat
|
||||
|
||||
def call_bad_names(context):
|
||||
bad_name_sets = [
|
||||
(['pickle.loads', 'pickle.dumps', ],
|
||||
'Pickle library appears to be in use, possible security issue.'),
|
||||
(['hashlib.md5', ],
|
||||
'Use of insecure MD5 hash function.'),
|
||||
(['subprocess.Popen', ],
|
||||
'Use of possibly-insecure system call function '
|
||||
'(subprocess.Popen).'),
|
||||
(['subprocess.call', ],
|
||||
'Use of possibly-insecure system call function '
|
||||
'(subprocess.call).'),
|
||||
(['tempfile.mktemp', 'mktemp' ],
|
||||
'Use of insecure and deprecated function (mktemp).'),
|
||||
(['eval', ],
|
||||
'Use of possibly-insecure function - consider using the safer '
|
||||
'ast.literal_eval().'),
|
||||
]
|
||||
|
||||
# test for 'bad' names defined above
|
||||
for bad_name_set in bad_name_sets:
|
||||
for bad_name in bad_name_set[0]:
|
||||
# if name.startswith(bad_name) or name.endswith(bad_name):
|
||||
if context['qualname'] == bad_name:
|
||||
return('WARN', "%s %s" %
|
||||
(bad_name_set[1],
|
||||
utils.ast_args_to_str(context['call'].args)))
|
||||
|
||||
|
||||
def call_subprocess_popen(context):
|
||||
if context['qualname'] == 'subprocess.Popen':
|
||||
if hasattr(context['call'], 'keywords'):
|
||||
for k in context['call'].keywords:
|
||||
if k.arg == 'shell' and isinstance(k.value, _ast.Name):
|
||||
if k.value.id == 'True':
|
||||
return('ERROR', 'Popen call with shell=True '
|
||||
'identified, security issue. %s' %
|
||||
utils.ast_args_to_str(context['call'].args))
|
||||
|
||||
|
||||
def call_no_cert_validation(context):
|
||||
if 'requests' in context['qualname'] and ('get' in context['name'] or
|
||||
'post' in context['name']):
|
||||
if hasattr(context['call'], 'keywords'):
|
||||
for k in context['call'].keywords:
|
||||
if k.arg == 'verify' and isinstance(k.value, _ast.Name):
|
||||
if k.value.id == 'False':
|
||||
return('ERROR',
|
||||
'Requests call with verify=False '
|
||||
'disabling SSL certificate checks, '
|
||||
'security issue. %s' %
|
||||
utils.ast_args_to_str(context['call'].args))
|
||||
|
||||
def call_bad_permissions(context):
|
||||
if 'chmod' in context['name']:
|
||||
if (hasattr(context['call'], 'args')):
|
||||
args = context['call'].args
|
||||
if len(args) == 2 and isinstance(args[1], _ast.Num):
|
||||
if ((args[1].n & stat.S_IWOTH) or
|
||||
(args[1].n & stat.S_IXGRP)):
|
||||
filename = args[0].s if hasattr(args[0], 's') else 'NOT PARSED'
|
||||
return('ERROR',
|
||||
'Chmod setting a permissive mask '
|
||||
'%s on file (%s).' %
|
||||
(oct(args[1].n), filename))
|
||||
|
||||
17
plugins/test_imports.py
Normal file
17
plugins/test_imports.py
Normal file
@@ -0,0 +1,17 @@
|
||||
##
|
||||
# tests targeting Import and ImportFrom nodes in the AST
|
||||
##
|
||||
|
||||
def import_name_match(context):
|
||||
info_on_import = ['pickle', 'subprocess', 'Crypto']
|
||||
for module in info_on_import:
|
||||
if context['module'] == module:
|
||||
return('INFO',
|
||||
"Consider possible security implications"
|
||||
" associated with '%s' module" % module)
|
||||
|
||||
def import_name_telnetlib(context):
|
||||
if context['module'] == 'telnetlib':
|
||||
return('ERROR', "Telnet is considered insecure. Use SSH or some"
|
||||
" other encrypted protocol.")
|
||||
|
||||
Reference in New Issue
Block a user