From fbd4e83efe15987f4f8a4608456211fba37fc80b Mon Sep 17 00:00:00 2001 From: Eric Brown Date: Wed, 22 Feb 2017 20:28:19 -0800 Subject: [PATCH] Yet Another Formatter (yaml) This patch adds a yaml formatter to the output options of bandit. Change-Id: Ibbe0cff062ce2c11138b746f95109f31de10f5b1 --- README.rst | 6 +- bandit/formatters/yaml.py | 92 ++++++++++++++++++++++++++++ doc/source/formatters/yaml.rst | 5 ++ doc/source/man/bandit.rst | 6 +- setup.cfg | 1 + tests/unit/formatters/test_yaml.py | 96 ++++++++++++++++++++++++++++++ 6 files changed, 200 insertions(+), 6 deletions(-) create mode 100644 bandit/formatters/yaml.py create mode 100644 doc/source/formatters/yaml.rst create mode 100644 tests/unit/formatters/test_yaml.py diff --git a/README.rst b/README.rst index 61a5e7b..b7f4ba9 100644 --- a/README.rst +++ b/README.rst @@ -87,8 +87,8 @@ 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] - [--ignore-nosec] [-x EXCLUDED_PATHS] [-b BASELINE] + [-f {csv,html,json,screen,txt,xml,yaml}] [-o [OUTPUT_FILE]] [-v] + [-d] [--ignore-nosec] [-x EXCLUDED_PATHS] [-b BASELINE] [--ini INI_PATH] [--version] targets [targets ...] @@ -118,7 +118,7 @@ Usage:: (-l for LOW, -ll for MEDIUM, -lll for HIGH) -i, --confidence report only issues of a given confidence level or higher (-i for LOW, -ii for MEDIUM, -iii for HIGH) - -f {csv,html,json,screen,txt,xml}, --format {csv,html,json,screen,txt,xml} + -f {csv,html,json,screen,txt,xml,yaml}, --format {csv,html,json,screen,txt,xml,yaml} specify output format -o [OUTPUT_FILE], --output [OUTPUT_FILE] write report to filename diff --git a/bandit/formatters/yaml.py b/bandit/formatters/yaml.py new file mode 100644 index 0000000..69aeedb --- /dev/null +++ b/bandit/formatters/yaml.py @@ -0,0 +1,92 @@ +# Copyright (c) 2017 VMware, Inc. +# +# 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""" +============== +YAML Formatter +============== + +This formatter outputs the issues in a yaml format. + +:Example: + +.. code-block:: none + + filename,test_name,test_id,issue_severity,issue_confidence,issue_text, + line_number,line_range + examples/yaml_load.py,blacklist_calls,B301,MEDIUM,HIGH,"Use of unsafe yaml + load. Allows instantiation of arbitrary objects. Consider yaml.safe_load(). + ",5,[5] + +.. versionadded:: 1.4.1 + +""" +# Necessary for this formatter to work when imported on Python 2. Importing +# the standard library's yaml module conflicts with the name of this module. +from __future__ import absolute_import + +import datetime +import logging +import operator +import sys + +import yaml + +LOG = logging.getLogger(__name__) + + +def report(manager, fileobj, sev_level, conf_level, lines=-1): + '''Prints issues in YAML format + + :param manager: the bandit manager object + :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 + ''' + + machine_output = {'results': [], 'errors': []} + for (fname, reason) in manager.get_skipped(): + machine_output['errors'].append({'filename': fname, 'reason': reason}) + + results = manager.get_issue_list(sev_level=sev_level, + conf_level=conf_level) + + collector = [r.as_dict() for r in results] + + itemgetter = operator.itemgetter + if manager.agg_type == 'vuln': + machine_output['results'] = sorted(collector, + key=itemgetter('test_name')) + else: + machine_output['results'] = sorted(collector, + key=itemgetter('filename')) + + machine_output['metrics'] = manager.metrics.data + + for result in machine_output['results']: + if 'code' in result: + code = result['code'].replace('\n', '\\n') + result['code'] = code + + # timezone agnostic format + TS_FORMAT = "%Y-%m-%dT%H:%M:%SZ" + + time_string = datetime.datetime.utcnow().strftime(TS_FORMAT) + machine_output['generated_at'] = time_string + + yaml.safe_dump(machine_output, fileobj, default_flow_style=False) + + if fileobj.name != sys.stdout.name: + LOG.info("YAML output written to file: %s", fileobj.name) diff --git a/doc/source/formatters/yaml.rst b/doc/source/formatters/yaml.rst new file mode 100644 index 0000000..020feae --- /dev/null +++ b/doc/source/formatters/yaml.rst @@ -0,0 +1,5 @@ +---- +yaml +---- + +.. automodule:: bandit.formatters.yaml diff --git a/doc/source/man/bandit.rst b/doc/source/man/bandit.rst index be0b993..04a3a43 100644 --- a/doc/source/man/bandit.rst +++ b/doc/source/man/bandit.rst @@ -7,8 +7,8 @@ SYNOPSIS 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] - [--ignore-nosec] [-x EXCLUDED_PATHS] [-b BASELINE] + [-f {csv,html,json,screen,txt,xml,yaml}] [-o OUTPUT_FILE] [-v] + [-d] [--ignore-nosec] [-x EXCLUDED_PATHS] [-b BASELINE] [--ini INI_PATH] [--version] targets [targets ...] @@ -43,7 +43,7 @@ OPTIONS (-l for LOW, -ll for MEDIUM, -lll for HIGH) -i, --confidence report only issues of a given confidence level or higher (-i for LOW, -ii for MEDIUM, -iii for HIGH) - -f {csv,html,json,screen,txt,xml}, --format {csv,html,json,screen,txt,xml} + -f {csv,html,json,screen,txt,xml,yaml}, --format {csv,html,json,screen,txt,xml,yaml} specify output format -o OUTPUT_FILE, --output OUTPUT_FILE write report to filename diff --git a/setup.cfg b/setup.cfg index ac21b7e..cb3aad6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -36,6 +36,7 @@ bandit.formatters = xml = bandit.formatters.xml:report html = bandit.formatters.html:report screen = bandit.formatters.screen:report + yaml = bandit.formatters.yaml:report bandit.plugins = # bandit/plugins/app_debug.py flask_debug_true = bandit.plugins.app_debug:flask_debug_true diff --git a/tests/unit/formatters/test_yaml.py b/tests/unit/formatters/test_yaml.py new file mode 100644 index 0000000..348066d --- /dev/null +++ b/tests/unit/formatters/test_yaml.py @@ -0,0 +1,96 @@ +# Copyright (c) 2017 VMware, Inc. +# +# 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 collections +import tempfile + +import mock +import testtools +import yaml + +import bandit +from bandit.core import config +from bandit.core import constants +from bandit.core import issue +from bandit.core import manager +from bandit.core import metrics +from bandit.formatters import json as b_json + + +class JsonFormatterTests(testtools.TestCase): + + def setUp(self): + super(JsonFormatterTests, self).setUp() + conf = config.BanditConfig() + self.manager = manager.BanditManager(conf, 'file') + (tmp_fd, self.tmp_fname) = tempfile.mkstemp() + self.context = {'filename': self.tmp_fname, + 'lineno': 4, + 'linerange': [4]} + self.check_name = 'hardcoded_bind_all_interfaces' + self.issue = issue.Issue(bandit.MEDIUM, bandit.MEDIUM, + 'Possible binding to all interfaces.') + + self.candidates = [issue.Issue(bandit.LOW, bandit.LOW, 'Candidate A', + lineno=1), + issue.Issue(bandit.HIGH, bandit.HIGH, 'Candiate B', + lineno=2)] + + self.manager.out_file = self.tmp_fname + + self.issue.fname = self.context['filename'] + self.issue.lineno = self.context['lineno'] + self.issue.linerange = self.context['linerange'] + self.issue.test = self.check_name + + self.manager.results.append(self.issue) + self.manager.metrics = metrics.Metrics() + + # mock up the metrics + for key in ['_totals', 'binding.py']: + self.manager.metrics.data[key] = {'loc': 4, 'nosec': 2} + for (criteria, default) in constants.CRITERIA: + for rank in constants.RANKING: + self.manager.metrics.data[key]['{0}.{1}'.format( + criteria, rank + )] = 0 + + @mock.patch('bandit.core.manager.BanditManager.get_issue_list') + def test_report(self, get_issue_list): + self.manager.files_list = ['binding.py'] + self.manager.scores = [{'SEVERITY': [0] * len(constants.RANKING), + 'CONFIDENCE': [0] * len(constants.RANKING)}] + + get_issue_list.return_value = collections.OrderedDict( + [(self.issue, self.candidates)]) + + 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: + data = yaml.load(f.read()) + self.assertIsNotNone(data['generated_at']) + self.assertEqual(self.tmp_fname, data['results'][0]['filename']) + self.assertEqual(self.issue.severity, + data['results'][0]['issue_severity']) + self.assertEqual(self.issue.confidence, + data['results'][0]['issue_confidence']) + self.assertEqual(self.issue.text, data['results'][0]['issue_text']) + self.assertEqual(self.context['lineno'], + data['results'][0]['line_number']) + self.assertEqual(self.context['linerange'], + data['results'][0]['line_range']) + self.assertEqual(self.check_name, data['results'][0]['test_name']) + self.assertIn('candidates', data['results'][0])