Allow output to default to stdout using argparse

The argparse module already has the capability to default to stdout
at CLI parameter definition time. This patch utilizes this and avoids
the opening of the output file by each formatter.

Change-Id: Ib1e89492558fe1fc06966711b6014bd5b86b84c8
This commit is contained in:
Eric Brown 2016-06-03 15:56:12 -07:00
parent 9fb201f839
commit 1310d18275
20 changed files with 95 additions and 120 deletions

View File

@ -78,7 +78,7 @@ Usage::
$ bandit -h
usage: bandit [-h] [-r] [-a {file,vuln}] [-n CONTEXT_LINES] [-c CONFIG_FILE]
[-p PROFILE] [-t TESTS] [-s SKIPS] [-l] [-i]
[-f {csv,html,json,screen,txt,xml}] [-o OUTPUT_FILE] [-v] [-d]
[-f {csv,html,json,screen,txt,xml}] [-o [OUTPUT_FILE]] [-v] [-d]
[--ignore-nosec] [-x EXCLUDED_PATHS] [-b BASELINE]
[--ini INI_PATH] [--version]
targets [targets ...]
@ -111,7 +111,7 @@ Usage::
higher (-i for LOW, -ii for MEDIUM, -iii for HIGH)
-f {csv,html,json,screen,txt,xml}, --format {csv,html,json,screen,txt,xml}
specify output format
-o OUTPUT_FILE, --output OUTPUT_FILE
-o [OUTPUT_FILE], --output [OUTPUT_FILE]
write report to filename
-v, --verbose output extra information like excluded and included
files

View File

@ -209,8 +209,9 @@ def main():
choices=sorted(extension_mgr.formatter_names)
)
parser.add_argument(
'-o', '--output', dest='output_file', action='store',
default=None, help='write report to filename'
'-o', '--output', dest='output_file', action='store', nargs='?',
type=argparse.FileType('w'), default=sys.stdout,
help='write report to filename'
)
parser.add_argument(
'-v', '--verbose', dest='verbose', action='store_true',

View File

@ -123,14 +123,14 @@ class BanditManager():
'''
return len(self.get_issue_list(sev_filter, conf_filter))
def output_results(self, lines, sev_level, conf_level, output_filename,
def output_results(self, lines, sev_level, conf_level, output_file,
output_format):
'''Outputs results from the result store
:param lines: How many surrounding lines to show per result
:param sev_level: Which severity levels to show (LOW, MEDIUM, HIGH)
:param conf_level: Which confidence levels to show (LOW, MEDIUM, HIGH)
:param output_filename: File to store results
:param output_file: File to store results
:param output_format: output format plugin name
:return: -
'''
@ -141,9 +141,8 @@ class BanditManager():
formatter = formatters_mgr[output_format]
report_func = formatter.plugin
report_func(self, filename=output_filename,
sev_level=sev_level, conf_level=conf_level,
lines=lines)
report_func(self, fileobj=output_file, sev_level=sev_level,
conf_level=conf_level, lines=lines)
except Exception as e:
raise RuntimeError("Unable to output report using '%s' formatter: "

View File

@ -16,7 +16,6 @@
import _ast
import ast
import contextlib
import logging
import os.path
import sys
@ -32,22 +31,6 @@ logger = logging.getLogger(__name__)
"""Various helper functions."""
@contextlib.contextmanager
def output_file(filename, filemode):
try:
out = sys.stdout
if filename is not None:
if os.path.isdir(filename):
raise RuntimeError('Specified destination is a directory')
out = open(filename, filemode)
yield out
except Exception:
raise
finally:
if out is not sys.stdout:
out.close()
def _get_attr_qual_name(node, aliases):
'''Get a the full name for the attribute node.

View File

@ -38,17 +38,16 @@ from __future__ import absolute_import
import csv
import logging
from bandit.core import utils
import sys
logger = logging.getLogger(__name__)
def report(manager, filename, sev_level, conf_level, lines=-1):
def report(manager, fileobj, sev_level, conf_level, lines=-1):
'''Prints issues in CSV format
:param manager: the bandit manager object
:param filename: The output file name, or None for stdout
:param fileobj: The output file object, which may be sys.stdout
:param sev_level: Filtering severity level
:param conf_level: Filtering confidence level
:param lines: Number of lines to report, -1 for all
@ -57,7 +56,7 @@ def report(manager, filename, sev_level, conf_level, lines=-1):
results = manager.get_issue_list(sev_level=sev_level,
conf_level=conf_level)
with utils.output_file(filename, 'w') as fout:
with fileobj:
fieldnames = ['filename',
'test_name',
'test_id',
@ -67,11 +66,11 @@ def report(manager, filename, sev_level, conf_level, lines=-1):
'line_number',
'line_range']
writer = csv.DictWriter(fout, fieldnames=fieldnames,
writer = csv.DictWriter(fileobj, fieldnames=fieldnames,
extrasaction='ignore')
writer.writeheader()
for result in results:
writer.writerow(result.as_dict(with_code=False))
if filename is not None:
logger.info("CSV output written to file: %s" % filename)
if fileobj.name != sys.stdout.name:
logger.info("CSV output written to file: %s" % fileobj.name)

View File

@ -147,20 +147,20 @@ This formatter outputs the issues as HTML.
"""
import logging
import sys
from bandit.core import docs_utils
from bandit.core.test_properties import accepts_baseline
from bandit.core import utils
logger = logging.getLogger(__name__)
@accepts_baseline
def report(manager, filename, sev_level, conf_level, lines=-1):
"""Writes issues to 'filename' in HTML format
def report(manager, fileobj, sev_level, conf_level, lines=-1):
"""Writes issues to 'fileobj' in HTML format
:param manager: the bandit manager object
:param filename: output file name
:param fileobj: The output file object, which may be sys.stdout
:param sev_level: Filtering severity level
:param conf_level: Filtering confidence level
:param lines: Number of lines to report, -1 for all
@ -369,9 +369,9 @@ pre {
skipped=skipped_text,
results=results_str)
with utils.output_file(filename, 'w') as fout:
fout.write(str(header_block.encode('utf-8')))
fout.write(str(report_contents.encode('utf-8')))
with fileobj:
fileobj.write(str(header_block.encode('utf-8')))
fileobj.write(str(report_contents.encode('utf-8')))
if filename is not None:
logger.info("HTML output written to file: %s" % filename)
if fileobj.name != sys.stdout.name:
logger.info("HTML output written to file: %s" % fileobj.name)

View File

@ -97,22 +97,22 @@ import datetime
import json
import logging
from operator import itemgetter
import sys
import six
from bandit.core import constants
from bandit.core.test_properties import accepts_baseline
from bandit.core import utils
logger = logging.getLogger(__name__)
@accepts_baseline
def report(manager, filename, sev_level, conf_level, lines=-1):
def report(manager, fileobj, sev_level, conf_level, lines=-1):
'''''Prints issues in JSON format
:param manager: the bandit manager object
:param filename: The output file name, or None for stdout
:param fileobj: The output file object, which may be sys.stdout
:param sev_level: Filtering severity level
:param conf_level: Filtering confidence level
:param lines: Number of lines to report, -1 for all
@ -177,8 +177,8 @@ def report(manager, filename, sev_level, conf_level, lines=-1):
result = json.dumps(machine_output, sort_keys=True,
indent=2, separators=(',', ': '))
with utils.output_file(filename, 'w') as fout:
fout.write(result)
with fileobj:
fileobj.write(result)
if filename is not None:
logger.info("JSON output written to file: %s" % filename)
if fileobj.name != sys.stdout.name:
logger.info("JSON output written to file: %s" % fileobj.name)

View File

@ -41,6 +41,7 @@ from __future__ import print_function
import datetime
import logging
import sys
from bandit.core import constants
from bandit.core.test_properties import accepts_baseline
@ -143,13 +144,13 @@ def do_print(bits):
@accepts_baseline
def report(manager, filename, sev_level, conf_level, lines=-1):
def report(manager, fileobj, sev_level, conf_level, lines=-1):
"""Prints discovered issues formatted for screen reading
This makes use of VT100 terminal codes for colored text.
:param manager: the bandit manager object
:param filename: The output file name, or None for stdout
:param fileobj: The output file object, which may be sys.stdout
:param sev_level: Filtering severity level
:param conf_level: Filtering confidence level
:param lines: Number of lines to report, -1 for all
@ -175,6 +176,6 @@ def report(manager, filename, sev_level, conf_level, lines=-1):
bits.extend(["\t%s (%s)" % skip for skip in manager.skipped])
do_print(bits)
if filename is not None:
if fileobj.name != sys.stdout.name:
logger.info(("Screen formatter output was not written to file: %s"
", consider '-f txt'") % filename)
", consider '-f txt'") % fileobj.name)

View File

@ -41,10 +41,10 @@ from __future__ import print_function
import datetime
import logging
import sys
from bandit.core import constants
from bandit.core.test_properties import accepts_baseline
from bandit.core import utils
logger = logging.getLogger(__name__)
@ -124,11 +124,11 @@ def get_results(manager, sev_level, conf_level, lines):
@accepts_baseline
def report(manager, filename, sev_level, conf_level, lines=-1):
def report(manager, fileobj, sev_level, conf_level, lines=-1):
"""Prints discovered issues in the text format
:param manager: the bandit manager object
:param filename: The output file name, or None for stdout
:param fileobj: The output file object, which may be sys.stdout
:param sev_level: Filtering severity level
:param conf_level: Filtering confidence level
:param lines: Number of lines to report, -1 for all
@ -154,8 +154,8 @@ def report(manager, filename, sev_level, conf_level, lines=-1):
bits.extend(["\t%s (%s)" % skip for skip in manager.skipped])
result = '\n'.join([bit for bit in bits]) + '\n'
with utils.output_file(filename, 'w') as fout:
fout.write(str(result.encode('utf-8')))
with fileobj:
fileobj.write(str(result.encode('utf-8')))
if filename is not None:
logger.info("Text output written to file: %s", filename)
if fileobj.name != sys.stdout.name:
logger.info("Text output written to file: %s", fileobj.name)

View File

@ -49,11 +49,11 @@ import six
logger = logging.getLogger(__name__)
def report(manager, filename, sev_level, conf_level, lines=-1):
def report(manager, fileobj, sev_level, conf_level, lines=-1):
'''Prints issues in XML format
:param manager: the bandit manager object
:param filename: The output file name, or None for stdout
:param fileobj: The output file object, which may be sys.stdout
:param sev_level: Filtering severity level
:param conf_level: Filtering confidence level
:param lines: Number of lines to report, -1 for all
@ -75,14 +75,17 @@ def report(manager, filename, sev_level, conf_level, lines=-1):
tree = ET.ElementTree(root)
if six.PY2:
outfile = sys.stdout
else:
outfile = sys.stdout.buffer
if filename is not None:
outfile = open(filename, "wb")
if fileobj.name == sys.stdout.name:
if six.PY2:
fileobj = sys.stdout
else:
fileobj = sys.stdout.buffer
elif fileobj.mode == 'w':
fileobj.close()
fileobj = open(fileobj.name, "wb")
tree.write(outfile, encoding='utf-8', xml_declaration=True)
with fileobj:
tree.write(fileobj, encoding='utf-8', xml_declaration=True)
if filename is not None:
logger.info("XML output written to file: %s" % filename)
if fileobj.name != sys.stdout.name:
logger.info("XML output written to file: %s" % fileobj.name)

View File

@ -10,11 +10,10 @@ Example Formatter
.. code-block:: python
def report(manager, filename, sev_level, conf_level, lines=-1,
out_format='bson'):
def report(manager, fileobj, sev_level, conf_level, lines=-1):
result = bson.dumps(issues)
with utils.output_file(filename, 'w') as fout:
fout.write(result)
with fileobj:
fileobj.write(result)
To register your plugin, you have two options:

View File

@ -256,7 +256,7 @@ class BanditCLIMainTests(testtools.TestCase):
# assert a SystemExit with code 2
self.assertRaisesRegex(SystemExit, '2', bandit.main)
@patch('sys.argv', ['bandit', '-c', 'bandit.yaml', 'test'])
@patch('sys.argv', ['bandit', '-c', 'bandit.yaml', 'test', '-o', 'output'])
def test_main_exit_with_results(self):
# Test that bandit exits when there are results
temp_directory = self.useFixture(fixtures.TempDir()).path
@ -269,7 +269,7 @@ class BanditCLIMainTests(testtools.TestCase):
# assert a SystemExit with code 1
self.assertRaisesRegex(SystemExit, '1', bandit.main)
@patch('sys.argv', ['bandit', '-c', 'bandit.yaml', 'test'])
@patch('sys.argv', ['bandit', '-c', 'bandit.yaml', 'test', '-o', 'output'])
def test_main_exit_with_no_results(self):
# Test that bandit exits when there are no results
temp_directory = self.useFixture(fixtures.TempDir()).path

View File

@ -158,8 +158,9 @@ class ManagerTests(testtools.TestCase):
conf_level = constants.LOW
output_filename = os.path.join(temp_directory, "_temp_output")
output_format = "invalid"
self.manager.output_results(lines, sev_level, conf_level,
output_filename, output_format)
tmp_file = open(output_filename, 'w')
self.manager.output_results(lines, sev_level, conf_level, tmp_file,
output_format)
if sys.stdout.isatty():
self.assertFalse(os.path.isfile(output_filename))
else:
@ -173,8 +174,9 @@ class ManagerTests(testtools.TestCase):
conf_level = constants.LOW
output_filename = os.path.join(temp_directory, "_temp_output.txt")
output_format = "txt"
self.manager.output_results(lines, sev_level, conf_level,
output_filename, output_format)
tmp_file = open(output_filename, 'w')
self.manager.output_results(lines, sev_level, conf_level, tmp_file,
output_format)
self.assertTrue(os.path.isfile(output_filename))
@mock.patch('os.path.isdir')

View File

@ -21,8 +21,6 @@ import shutil
import sys
import tempfile
import mock
import six.moves.builtins as builtins
import testtools
from bandit.core import utils as b_utils
@ -296,28 +294,6 @@ class UtilTests(testtools.TestCase):
self.assertEqual(b_utils.parse_ini_file(t.name),
test['expected'])
@mock.patch('os.path.isdir')
def test_check_output_destination_dir(self, isdir):
isdir.return_value = True
def _b_tester(a, b):
with b_utils.output_file(a, b):
pass
self.assertRaises(RuntimeError, _b_tester, 'derp', 'r')
@mock.patch('os.path.isdir')
def test_check_output_destination_bad(self, isdir):
with mock.patch.object(builtins, 'open') as b_open:
isdir.return_value = False
b_open.side_effect = IOError()
def _b_tester(a, b):
with b_utils.output_file(a, b):
pass
self.assertRaises(IOError, _b_tester, 'derp', 'r')
def test_check_ast_node_good(self):
node = b_utils.check_ast_node("Call")
self.assertEqual("Call", node)

View File

@ -48,7 +48,8 @@ class CsvFormatterTests(testtools.TestCase):
self.manager.results.append(self.issue)
def test_report(self):
b_csv.report(self.manager, self.tmp_fname, self.issue.severity,
tmp_file = open(self.tmp_fname, 'w')
b_csv.report(self.manager, tmp_file, self.issue.severity,
self.issue.confidence)
with open(self.tmp_fname) as f:

View File

@ -41,8 +41,9 @@ class HtmlFormatterTests(testtools.TestCase):
def test_report_with_skipped(self):
self.manager.skipped = [('abc.py', 'File is bad')]
tmp_file = open(self.tmp_fname, 'w')
b_html.report(
self.manager, self.tmp_fname, bandit.LOW, bandit.LOW)
self.manager, tmp_file, bandit.LOW, bandit.LOW)
with open(self.tmp_fname) as f:
soup = BeautifulSoup(f.read(), 'html.parser')
@ -78,8 +79,9 @@ class HtmlFormatterTests(testtools.TestCase):
(issue_b, [issue_x]),
(issue_c, [issue_y])])
tmp_file = open(self.tmp_fname, 'w')
b_html.report(
self.manager, self.tmp_fname, bandit.LOW, bandit.LOW)
self.manager, tmp_file, bandit.LOW, bandit.LOW)
with open(self.tmp_fname) as f:
soup = BeautifulSoup(f.read(), 'html.parser')

View File

@ -75,7 +75,8 @@ class JsonFormatterTests(testtools.TestCase):
get_issue_list.return_value = OrderedDict([(self.issue,
self.candidates)])
b_json.report(self.manager, self.tmp_fname, self.issue.severity,
tmp_file = open(self.tmp_fname, 'w')
b_json.report(self.manager, tmp_file, self.issue.severity,
self.issue.confidence)
with open(self.tmp_fname) as f:

View File

@ -79,7 +79,8 @@ class ScreenFormatterTests(testtools.TestCase):
get_issue_list.return_value = OrderedDict()
with mock.patch('bandit.formatters.screen.do_print') as m:
screen.report(self.manager, self.tmp_fname, bandit.LOW, bandit.LOW,
tmp_file = open(self.tmp_fname, 'w')
screen.report(self.manager, tmp_file, bandit.LOW, bandit.LOW,
lines=5)
self.assertIn('No issues identified.',
'\n'.join([str(a) for a in m.call_args]))
@ -117,7 +118,8 @@ class ScreenFormatterTests(testtools.TestCase):
with mock.patch(output_str_fn) as output_str:
output_str.return_value = 'ISSUE_OUTPUT_TEXT'
screen.report(self.manager, self.tmp_fname, bandit.LOW, bandit.LOW,
tmp_file = open(self.tmp_fname, 'w')
screen.report(self.manager, tmp_file, bandit.LOW, bandit.LOW,
lines=5)
calls = [mock.call(issue_a, '', lines=5),
@ -128,7 +130,8 @@ class ScreenFormatterTests(testtools.TestCase):
# Validate that we're outputting all of the expected fields and the
# correct values
with mock.patch('bandit.formatters.screen.do_print') as m:
screen.report(self.manager, self.tmp_fname, bandit.LOW, bandit.LOW,
tmp_file = open(self.tmp_fname, 'w')
screen.report(self.manager, tmp_file, bandit.LOW, bandit.LOW,
lines=5)
data = '\n'.join([str(a) for a in m.call_args[0][0]])
@ -190,7 +193,8 @@ class ScreenFormatterTests(testtools.TestCase):
with mock.patch(output_str_fn) as output_str:
output_str.return_value = 'ISSUE_OUTPUT_TEXT'
screen.report(self.manager, self.tmp_fname, bandit.LOW, bandit.LOW,
tmp_file = open(self.tmp_fname, 'w')
screen.report(self.manager, tmp_file, bandit.LOW, bandit.LOW,
lines=5)
calls = [mock.call(issue_a, '', lines=5),

View File

@ -74,8 +74,8 @@ class TextFormatterTests(testtools.TestCase):
self.manager.out_file = self.tmp_fname
get_issue_list.return_value = OrderedDict()
b_text.report(self.manager, self.tmp_fname, bandit.LOW, bandit.LOW,
lines=5)
tmp_file = open(self.tmp_fname, 'w')
b_text.report(self.manager, tmp_file, bandit.LOW, bandit.LOW, lines=5)
with open(self.tmp_fname) as f:
data = f.read()
@ -114,7 +114,8 @@ class TextFormatterTests(testtools.TestCase):
with mock.patch(output_str_fn) as output_str:
output_str.return_value = 'ISSUE_OUTPUT_TEXT'
b_text.report(self.manager, self.tmp_fname, bandit.LOW, bandit.LOW,
tmp_file = open(self.tmp_fname, 'w')
b_text.report(self.manager, tmp_file, bandit.LOW, bandit.LOW,
lines=5)
calls = [mock.call(issue_a, '', lines=5),
@ -124,7 +125,8 @@ class TextFormatterTests(testtools.TestCase):
# Validate that we're outputting all of the expected fields and the
# correct values
b_text.report(self.manager, self.tmp_fname, bandit.LOW, bandit.LOW,
tmp_file = open(self.tmp_fname, 'w')
b_text.report(self.manager, tmp_file, bandit.LOW, bandit.LOW,
lines=5)
with open(self.tmp_fname) as f:
data = f.read()
@ -178,7 +180,8 @@ class TextFormatterTests(testtools.TestCase):
with mock.patch(output_str_fn) as output_str:
output_str.return_value = 'ISSUE_OUTPUT_TEXT'
b_text.report(self.manager, self.tmp_fname, bandit.LOW, bandit.LOW,
tmp_file = open(self.tmp_fname, 'w')
b_text.report(self.manager, tmp_file, bandit.LOW, bandit.LOW,
lines=5)
calls = [mock.call(issue_a, '', lines=5),

View File

@ -70,7 +70,8 @@ class XmlFormatterTests(testtools.TestCase):
return d
def test_report(self):
b_xml.report(self.manager, self.tmp_fname, self.issue.severity,
tmp_file = open(self.tmp_fname, 'wb')
b_xml.report(self.manager, tmp_file, self.issue.severity,
self.issue.confidence)
with open(self.tmp_fname) as f: