Support dynamic loading of tests

This adds config file handling, module import, and function addition
to test set.
This commit is contained in:
Jamie Finnigan
2014-07-17 10:31:46 -07:00
parent ae9e98ed9e
commit 613ea2dc51
11 changed files with 195 additions and 114 deletions

View File

@@ -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
View 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

View File

@@ -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

View File

@@ -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)

View 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
View 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)

View File

@@ -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
View File

@@ -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
View File

74
plugins/test_calls.py Normal file
View 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
View 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.")