Skip to content

Commit 6e2e387

Browse files
author
Jamie Finnigan
committed
Shift in result types & ranking scales
This commit completes work that was discussed during the OSSG mid-cycle. Summarizing the major changes: - Result 'severity' has been moved from INFO/WARN/ERROR to a LOW/MEDIUM/HIGH scale. This, as previously, is intended to indicate the risk associated with the issue being reported. - Result 'confidence' has been introduced, also with a LOW/MEDIUM/HIGH scale. This is intended to allow a plugin writer to indicate their level of confidence that the issue raised is truly a security problem. - A Bandit plugin may now return a result as a 2-tuple (issue_severity, issue_text) or - optionally - a 3-tuple (issue_severity, issue_confidence, issue_text). Confidence will default to UNDEFINED if the plugin does not return a value. The commit includes updates across Bandit and plugins to utilize the new scales, and updates to the associated functional test suite to maintain test coverage. Additionally, this commit: - Modifies the labels on fields included in JSON and CSV output formats to align with Bandit-internal terminology (likely a breaking change for consumers of these formats). - Removes color output codes from Bandit configuration when not running in a TTY, as they are not likely to be supported. - Adds severity and confidence to the standard Bandit output for each issue. - Adds a few more debug statements and includes a traceback when a plugin fails, to aid in developing plugins. - Cleans up the command line arguments used with argparse a little. - Fixes PEP8 niggles. It does not implement filtering by confidence, that will be a future todo. Change-Id: Ia0f76b96d1e756ce605ce6b0b29eee8bd2830574
1 parent eec0d75 commit 6e2e387

29 files changed

Lines changed: 495 additions & 255 deletions

bandit.yaml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ plugin_name_pattern: '*.py'
1111
#output_colors:
1212
# DEFAULT: '\033[0m'
1313
# HEADER: '\033[95m'
14-
# INFO: '\033[94m'
15-
# WARN: '\033[93m'
16-
# ERROR: '\033[91m'
14+
# LOW: '\033[94m'
15+
# MEDIUM: '\033[93m'
16+
# HIGH: '\033[91m'
1717

1818
# optional: log format string
1919
#log_format: "[%(module)s]\t%(levelname)s\t%(message)s"
@@ -46,7 +46,7 @@ profiles:
4646
include:
4747
- hardcoded_sql_expressions
4848

49-
blacklist_functions:
49+
blacklist_calls:
5050
bad_name_sets:
5151
- pickle:
5252
qualnames: [pickle.loads, pickle.load, pickle.Unpickler,
@@ -95,11 +95,11 @@ blacklist_imports:
9595
bad_import_sets:
9696
- telnet:
9797
imports: [telnetlib]
98-
level: ERROR
98+
level: HIGH
9999
message: "Telnet is considered insecure. Use SSH or some other encrypted protocol."
100100
- info_libs:
101101
imports: [pickle, cPickle, subprocess, Crypto]
102-
level: INFO
102+
level: LOW
103103
message: "Consider possible security implications associated with {module} module."
104104

105105
hardcoded_password:

bandit/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,5 @@
2424
from core import tester # noqa
2525
from core import utils # noqa
2626
from core.constants import * # noqa
27+
from core.objects import * # noqa
2728
from core.test_properties import * # noqa

bandit/bandit.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,13 @@ def main():
3939
parser.add_argument(
4040
'-a', '--aggregate', dest='agg_type',
4141
action='store', default='file', type=str,
42-
help='group results by (vuln)erability type or (file) it occurs in'
42+
choices=['file', 'vuln'],
43+
help='group results by vulnerability type or file it occurs in'
4344
)
4445
parser.add_argument(
4546
'-n', '--number', dest='context_lines',
4647
action='store', default=-1, type=int,
47-
help='max number of code lines to display'
48+
help='max number of code lines to display for each issue identified'
4849
)
4950
parser.add_argument(
5051
'-c', '--configfile', dest='config_file',

bandit/core/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,5 @@
2424
import tester # noqa
2525
import utils # noqa
2626
from constants import * # noqa
27+
from objects import * # noqa
2728
from test_properties import * # noqa

bandit/core/config.py

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -112,26 +112,33 @@ def _init_progress_increment(self):
112112
def _init_output_colors(self):
113113
'''Sets the settings colors
114114
115-
sets settings['color_xxx'] where xxx is DEFAULT, HEADER, INFO, WARN,
116-
ERROR
115+
sets settings['color_xxx'] where xxx is DEFAULT, HEADER, LOW, MEDIUM,
116+
HIGH
117117
'''
118-
colors = ['HEADER', 'DEFAULT', 'INFO', 'WARN', 'ERROR']
118+
colors = ['HEADER', 'DEFAULT', 'LOW', 'MEDIUM', 'HIGH']
119119
color_settings = dict()
120120

121+
isatty = hasattr(sys.stdout, "isatty") and sys.stdout.isatty()
122+
121123
for color in colors:
122-
# grab the default color from constant
123-
color_settings[color] = constants.color[color]
124-
125-
# check if the option has been set in config file
126-
options_string = 'output_colors.' + color
127-
if self.get_option(options_string):
128-
color_string = self.get_option(options_string)
129-
# some manipulation is needed because escape string doesn't
130-
# come back from yaml correctly
131-
if color_string.find('['):
132-
right_half = color_string[color_string.find('['):]
133-
left_half = '\033'
134-
color_settings[color] = left_half + right_half
124+
# if not a TTY, overwrite color codes in configuration
125+
if not isatty:
126+
color_settings[color] = ""
127+
# else read color codes in from the config
128+
else:
129+
# grab the default color from constant
130+
color_settings[color] = constants.color[color]
131+
132+
# check if the option has been set in config file
133+
options_string = 'output_colors.' + color
134+
if self.get_option(options_string):
135+
color_string = self.get_option(options_string)
136+
# some manipulation is needed because escape string doesn't
137+
# come back from yaml correctly
138+
if color_string.find('['):
139+
right_half = color_string[color_string.find('['):]
140+
left_half = '\033'
141+
color_settings[color] = left_half + right_half
135142

136143
# update the settings dict with the color value
137144
settings_string = 'color_' + color

bandit/core/constants.py

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
# License for the specific language governing permissions and limitations
1515
# under the License.
1616

17-
from collections import namedtuple
1817
from distutils.sysconfig import get_python_lib
1918
import os
2019

@@ -23,9 +22,9 @@
2322
color = {
2423
'DEFAULT': '\033[0m',
2524
'HEADER': '\033[95m',
26-
'INFO': '\033[94m',
27-
'WARN': '\033[93m',
28-
'ERROR': '\033[91m',
25+
'LOW': '\033[94m',
26+
'MEDIUM': '\033[93m',
27+
'HIGH': '\033[91m',
2928
}
3029

3130
# default plugin name pattern
@@ -40,21 +39,14 @@
4039
# flag/s used to mark lines where identified issues should not be reported
4140
SKIP_FLAGS = ['nosec', ]
4241

43-
# list severities in ascending order
44-
SEVERITY = ['INFO', 'WARN', 'ERROR']
45-
SEVERITY_VALUES = {'INFO': 1, 'WARN': 5, 'ERROR': 10}
42+
RANKING = ['UNDEFINED', 'LOW', 'MEDIUM', 'HIGH']
43+
RANKING_VALUES = {'UNDEFINED': 1, 'LOW': 3, 'MEDIUM': 5, 'HIGH': 10}
4644

47-
# add each severity to globals, to allow direct access in module name space
48-
for sev in SEVERITY:
49-
globals()[sev] = sev
45+
# add each ranking to globals, to allow direct access in module name space
46+
for rank in RANKING:
47+
globals()[rank] = rank
5048

51-
# severity level constants for assignment to individual plugins
52-
severity_namedtuple = namedtuple('SeverityLevel', 'HIGH MEDIUM LOW')
53-
SEVERITY_LEVEL = severity_namedtuple(HIGH=10, MEDIUM=5, LOW=0)
54-
55-
# confidence level constants for return from individual plugins
56-
confidence_namedtuple = namedtuple('ConfidenceLevel', 'HIGH MEDIUM LOW')
57-
CONFIDENCE_LEVEL = confidence_namedtuple(HIGH=10, MEDIUM=5, LOW=0)
49+
CONFIDENCE_DEFAULT = 'UNDEFINED'
5850

5951
# A list of values Python considers to be False.
6052
# These can be useful in tests to check if a value is True or False.

bandit/core/node_visitor.py

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,10 @@ def __init__(self, fname, logger, config, metaast, results, testset,
161161
debug):
162162
self.debug = debug
163163
self.seen = 0
164-
self.score = [0] * len(constants.SEVERITY)
164+
self.scores = {
165+
'SEVERITY': [0] * len(constants.RANKING),
166+
'CONFIDENCE': [0] * len(constants.RANKING)
167+
}
165168
self.fname = fname
166169
self.logger = logger
167170
self.config = config
@@ -223,7 +226,7 @@ def visit_FunctionDef(self, node):
223226
# For all child nodes and any tests run, add this function name to
224227
# current namespace
225228
self.namespace = b_utils.namespace_path_join(self.namespace, name)
226-
self.update_score(self.tester.run_tests(self.context, 'FunctionDef'))
229+
self.update_scores(self.tester.run_tests(self.context, 'FunctionDef'))
227230
super(BanditNodeVisitor, self).generic_visit(node)
228231
self.namespace = b_utils.namespace_path_split(self.namespace)[0]
229232

@@ -246,7 +249,7 @@ def visit_Call(self, node):
246249
self.context['qualname'] = qualname
247250
self.context['name'] = name
248251

249-
self.update_score(self.tester.run_tests(self.context, 'Call'))
252+
self.update_scores(self.tester.run_tests(self.context, 'Call'))
250253
super(BanditNodeVisitor, self).generic_visit(node)
251254

252255
def visit_Import(self, node):
@@ -264,7 +267,7 @@ def visit_Import(self, node):
264267
self.context['import_aliases'][nodename.asname] = nodename.name
265268
self.context['imports'].add(nodename.name)
266269
self.context['module'] = nodename.name
267-
self.update_score(self.tester.run_tests(self.context, 'Import'))
270+
self.update_scores(self.tester.run_tests(self.context, 'Import'))
268271
super(BanditNodeVisitor, self).generic_visit(node)
269272

270273
def visit_ImportFrom(self, node):
@@ -300,7 +303,7 @@ def visit_ImportFrom(self, node):
300303
self.context['imports'].add(module + "." + nodename.name)
301304
self.context['module'] = module
302305
self.context['name'] = nodename.name
303-
self.update_score(self.tester.run_tests(self.context, 'ImportFrom'))
306+
self.update_scores(self.tester.run_tests(self.context, 'ImportFrom'))
304307
super(BanditNodeVisitor, self).generic_visit(node)
305308

306309
def visit_Str(self, node):
@@ -328,14 +331,14 @@ def visit_Str(self, node):
328331
is_standalone_expr = False
329332
# if we don't have either one of those, run the test
330333
if not (is_str or is_standalone_expr):
331-
self.update_score(self.tester.run_tests(self.context, 'Str'))
334+
self.update_scores(self.tester.run_tests(self.context, 'Str'))
332335
super(BanditNodeVisitor, self).generic_visit(node)
333336

334337
def visit_Exec(self, node):
335338
self.context['str'] = 'exec'
336339

337340
self.logger.debug("visit_Exec called (%s)" % ast.dump(node))
338-
self.update_score(self.tester.run_tests(self.context, 'Exec'))
341+
self.update_scores(self.tester.run_tests(self.context, 'Exec'))
339342
super(BanditNodeVisitor, self).generic_visit(node)
340343

341344
def visit(self, node):
@@ -364,9 +367,9 @@ def visit(self, node):
364367
super(BanditNodeVisitor, self).visit(node)
365368
self.depth -= 1
366369
self.logger.debug("%s\texiting : %s" % (self.depth, hex(id(node))))
367-
return self.score
370+
return self.scores
368371

369-
def update_score(self, score):
372+
def update_scores(self, scores):
370373
'''Score updater
371374
372375
Since we moved from a single score value to a map of scores per
@@ -375,7 +378,10 @@ def update_score(self, score):
375378
'''
376379
def add(x, y):
377380
return x + y
378-
self.score = map(add, self.score, score)
381+
for score_type in self.scores:
382+
self.scores[score_type] = map(
383+
add, self.scores[score_type], scores[score_type]
384+
)
379385

380386
def process(self, fdata):
381387
'''Main process loop
@@ -394,4 +400,4 @@ def process(self, fdata):
394400

395401
self.visit(self.statement['node'])
396402
self.statement = self.stmt_buffer.get_next()
397-
return self.score
403+
return self.scores

bandit/core/objects.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# -*- coding:utf-8 -*-
2+
#
3+
# Copyright 2014 Hewlett-Packard Development Company, L.P.
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
6+
# not use this file except in compliance with the License. You may obtain
7+
# a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14+
# License for the specific language governing permissions and limitations
15+
# under the License.
16+
17+
from collections import namedtuple
18+
19+
Issue = namedtuple('Issue', 'severity confidence text')

0 commit comments

Comments
 (0)