Custom formatter

Implements: custom formatter

Custom formatter can be used to output a machine-readable, easily
parsable and customizable format using set of predefined tags
to suite various needs.

Output string is formatted using python string.format() standards
and therefore provides familiar usage.

Usage: bandit --format custom [--msg-template MSG-TEMPLATE] targets

See bandit --help for additional information and list of available tags

modified:   bandit/cli/main.py
modified:   bandit/core/manager.py
modified:   README.rst
modified:   setup.cfg
new file:   bandit/formatters/custom.py

Change-Id: I900c9689cddb048db58608c443305e05e7a4be14
Signed-off-by: Marek Cermak <macermak@redhat.com>
This commit is contained in:
Marek Cermak 2017-10-09 15:57:56 +02:00
parent dab37aace4
commit d159335700
7 changed files with 311 additions and 13 deletions

View File

@ -87,8 +87,9 @@ 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,yaml}] [-o [OUTPUT_FILE]] [-v]
[-d] [--ignore-nosec] [-x EXCLUDED_PATHS] [-b BASELINE]
[-f {csv,custom,html,json,screen,txt,xml,yaml}]
[--msg-template MSG_TEMPLATE] [-o [OUTPUT_FILE]] [-v] [-d]
[--ignore-nosec] [-x EXCLUDED_PATHS] [-b BASELINE]
[--ini INI_PATH] [--version]
targets [targets ...]
@ -118,8 +119,12 @@ 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,yaml}, --format {csv,html,json,screen,txt,xml,yaml}
-f {csv,custom,html,json,screen,txt,xml,yaml}, --format {csv,custom,html,json,screen,txt,xml,yaml}
specify output format
--msg-template MSG_TEMPLATE
specify output message template (only usable with
--format custom), see CUSTOM FORMAT section for list
of available values
-o [OUTPUT_FILE], --output [OUTPUT_FILE]
write report to filename
-v, --verbose output extra information like excluded and included
@ -137,7 +142,33 @@ Usage::
arguments
--version show program's version number and exit
CUSTOM FORMATTING
-----------------
Available tags:
{abspath}, {relpath}, {line}, {test_id},
{severity}, {msg}, {confidence}, {range}
Example usage:
Default template:
bandit -r examples/ --format custom --msg-template \
"{abspath}:{line}: {test_id}[bandit]: {severity}: {msg}"
Provides same output as:
bandit -r examples/ --format custom
Tags can also be formatted in python string.format() style:
bandit -r examples/ --format custom --msg-template \
"{relpath:20.20s}: {line:03}: {test_id:^8}: DEFECT: {msg:>20}"
See python documentation for more information about formatting style:
https://docs.python.org/3.4/library/string.html
The following tests were discovered and loaded:
-----------------------------------------------
B101 assert_used
B102 exec_used
B103 set_bad_file_permissions
@ -339,6 +370,33 @@ To register your plugin, you have two options:
bandit.plugins =
mako = bandit_mako
Custom Formatting
-----------------
Available tags:
::
{abspath}, {relpath}, {line}, {test_id},
{severity}, {msg}, {confidence}, {range}
Example usage:
Default template::
bandit -r examples/ --format custom --msg-template \
"{abspath}:{line}: {test_id}[bandit]: {severity}: {msg}"
Provides same output as::
bandit -r examples/ --format custom
Tags can also be formatted in python string.format() style::
bandit -r examples/ --format custom --msg-template \
"{relpath:20.20s}: {line:03}: {test_id:^8}: DEFECT: {msg:>20}"
See python documentation for more information about formatting style:
https://docs.python.org/3.4/library/string.html
Contributing
------------
Contributions to Bandit are always welcome! We can be found on

View File

@ -18,6 +18,7 @@ import fnmatch
import logging
import os
import sys
import textwrap
import bandit
@ -205,6 +206,13 @@ def main():
default=output_format, help='specify output format',
choices=sorted(extension_mgr.formatter_names)
)
parser.add_argument(
'--msg-template', action='store',
default=None, help='specify output message template'
' (only usable with --format custom),'
' see CUSTOM FORMAT section'
' for list of available values',
)
parser.add_argument(
'-o', '--output', dest='output_file', action='store', nargs='?',
type=argparse.FileType('w'), default=sys.stdout,
@ -253,11 +261,41 @@ def main():
blacklist_info.append('%s\t%s' % (b['id'], b['name']))
plugin_list = '\n\t'.join(sorted(set(plugin_info + blacklist_info)))
parser.epilog = ('The following tests were discovered and'
' loaded:\n\t{0}\n'.format(plugin_list))
dedent_text = textwrap.dedent('''
CUSTOM FORMATTING
-----------------
Available tags:
{abspath}, {relpath}, {line}, {test_id},
{severity}, {msg}, {confidence}, {range}
Example usage:
Default template:
bandit -r examples/ --format custom --msg-template \\
"{abspath}:{line}: {test_id}[bandit]: {severity}: {msg}"
Provides same output as:
bandit -r examples/ --format custom
Tags can also be formatted in python string.format() style:
bandit -r examples/ --format custom --msg-template \\
"{relpath:20.20s}: {line:03}: {test_id:^8}: DEFECT: {msg:>20}"
See python documentation for more information about formatting style:
https://docs.python.org/3.4/library/string.html
The following tests were discovered and loaded:
-----------------------------------------------
''')
parser.epilog = dedent_text + "\t{0}".format(plugin_list)
# setup work - parse arguments, and initialize BanditManager
args = parser.parse_args()
# Check if `--msg-template` is not present without custom formatter
if args.output_format != 'custom' and args.msg_template is not None:
parser.error("--msg-template can only be used with --format=custom")
try:
b_conf = b_config.BanditConfig(config_file=args.config_file)
@ -341,7 +379,8 @@ def main():
sev_level,
conf_level,
args.output_file,
args.output_format)
args.output_format,
args.msg_template)
# return an exit code of 1 if there are results, 0 otherwise
if b_mgr.results_count(sev_filter=sev_level, conf_filter=conf_level) > 0:

View File

@ -136,7 +136,7 @@ class BanditManager(object):
return len(self.get_issue_list(sev_filter, conf_filter))
def output_results(self, lines, sev_level, conf_level, output_file,
output_format):
output_format, template=None):
'''Outputs results from the result store
:param lines: How many surrounding lines to show per result
@ -144,6 +144,9 @@ class BanditManager(object):
:param conf_level: Which confidence levels to show (LOW, MEDIUM, HIGH)
:param output_file: File to store results
:param output_format: output format plugin name
:param template: Output template with non-terminal tags <N>
(default: {abspath}:{line}:
{test_id}[bandit]: {severity}: {msg})
:return: -
'''
try:
@ -153,8 +156,13 @@ class BanditManager(object):
formatter = formatters_mgr[output_format]
report_func = formatter.plugin
report_func(self, fileobj=output_file, sev_level=sev_level,
conf_level=conf_level, lines=lines)
if output_format == 'custom':
report_func(self, fileobj=output_file, sev_level=sev_level,
conf_level=conf_level, lines=lines,
template=template)
else:
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: "

163
bandit/formatters/custom.py Normal file
View File

@ -0,0 +1,163 @@
# Copyright (c) 2017 Hewlett Packard Enterprise
# -*- coding:utf-8 -*-
#
# 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"""
================
Custom Formatter
================
This formatter outputs the issues in custom machine-readable format.
default template: {abspath}:{line}: {test_id}[bandit]: {severity}: {msg}
:Example:
/usr/lib/python3.6/site-packages/openlp/core/utils/__init__.py: \
405: B310[bandit]: MEDIUM: Audit url open for permitted schemes. \
Allowing use of file:/ or custom schemes is often unexpected.
"""
import logging
import os
import re
import string
import sys
from bandit.core import test_properties
LOG = logging.getLogger(__name__)
class SafeMapper(dict):
"""Safe mapper to handle format key errors"""
@classmethod # To prevent PEP8 warnings in the test suite
def __missing__(cls, key):
return "{%s}" % key
@test_properties.accepts_baseline
def report(manager, fileobj, sev_level, conf_level, lines=-1, template=None):
"""Prints issues in custom 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
:param template: Output template with non-terminal tags <N>
(default: '{abspath}:{line}:
{test_id}[bandit]: {severity}: {msg}')
"""
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)
msg_template = template
if template is None:
msg_template = "{abspath}:{line}: {test_id}[bandit]: {severity}: {msg}"
# Dictionary of non-terminal tags that will be expanded
tag_mapper = {
'abspath': lambda issue: os.path.abspath(issue.fname),
'relpath': lambda issue: os.path.relpath(issue.fname),
'line': lambda issue: issue.lineno,
'test_id': lambda issue: issue.test_id,
'severity': lambda issue: issue.severity,
'msg': lambda issue: issue.text,
'confidence': lambda issue: issue.confidence,
'range': lambda issue: issue.linerange
}
# Create dictionary with tag sets to speed up search for similar tags
tag_sim_dict = dict(
[(tag, set(tag)) for tag, _ in tag_mapper.items()]
)
# Parse the format_string template and check the validity of tags
try:
parsed_template_orig = list(string.Formatter().parse(msg_template))
# of type (literal_text, field_name, fmt_spec, conversion)
# Check the format validity only, ignore keys
string.Formatter().vformat(msg_template, (), SafeMapper(line=0))
except ValueError as e:
LOG.error("Template is not in valid format: %s", e.args[0])
sys.exit(2)
tag_set = {t[1] for t in parsed_template_orig if t[1] is not None}
if not tag_set:
LOG.error("No tags were found in the template. Are you missing '{}'?")
sys.exit(2)
def get_similar_tag(tag):
similarity_list = [(len(set(tag) & t_set), t)
for t, t_set in tag_sim_dict.items()]
return sorted(similarity_list)[-1][1]
tag_blacklist = []
for tag in tag_set:
# check if the tag is in dictionary
if tag not in tag_mapper:
similar_tag = get_similar_tag(tag)
LOG.warning(
"Tag '%s' was not recognized and will be skipped, "
"did you mean to use '%s'?", tag, similar_tag
)
tag_blacklist += [tag]
# Compose the message template back with the valid values only
msg_parsed_template_list = []
for literal_text, field_name, fmt_spec, conversion in parsed_template_orig:
if literal_text:
# if there is '{' or '}', double it to prevent expansion
literal_text = re.sub('{', '{{', literal_text)
literal_text = re.sub('}', '}}', literal_text)
msg_parsed_template_list.append(literal_text)
if field_name is not None:
if field_name in tag_blacklist:
msg_parsed_template_list.append(field_name)
continue
# Append the fmt_spec part
params = [field_name, fmt_spec, conversion]
markers = ['', ':', '!']
msg_parsed_template_list.append(
['{'] +
["%s" % (m + p) if p else ''
for m, p in zip(markers, params)] +
['}']
)
msg_parsed_template = "".join([item for lst in msg_parsed_template_list
for item in lst]) + "\n"
limit = lines if lines > 0 else None
with fileobj:
for defect in results[:limit]:
evaluated_tags = SafeMapper(
(k, v(defect)) for k, v in tag_mapper.items()
)
output = msg_parsed_template.format(**evaluated_tags)
fileobj.write(output)
if fileobj.name != sys.stdout.name:
LOG.info("Result written to file: %s", fileobj.name)

View File

@ -7,8 +7,9 @@ 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,yaml}] [-o OUTPUT_FILE] [-v]
[-d] [--ignore-nosec] [-x EXCLUDED_PATHS] [-b BASELINE]
[-f {csv,custom,html,json,screen,txt,xml,yaml}]
[--msg-template MSG_TEMPLATE] [-o OUTPUT_FILE] [-v] [-d]
[--ignore-nosec] [-x EXCLUDED_PATHS] [-b BASELINE]
[--ini INI_PATH] [--version]
targets [targets ...]
@ -43,8 +44,12 @@ 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,yaml}, --format {csv,html,json,screen,txt,xml,yaml}
-f {csv,custom,html,json,screen,txt,xml,yaml}, --format {csv,custom,html,json,screen,txt,xml,yaml}
specify output format
--msg-template MSG_TEMPLATE
specify output message template (only usable with
--format custom), see CUSTOM FORMAT section for list
of available values
-o OUTPUT_FILE, --output OUTPUT_FILE
write report to filename
-v, --verbose output extra information like excluded and included
@ -62,6 +67,30 @@ OPTIONS
arguments
--version show program's version number and exit
CUSTOM FORMATTING
-----------------
Available tags:
{abspath}, {relpath}, {line}, {test_id},
{severity}, {msg}, {confidence}, {range}
Example usage:
Default template:
bandit -r examples/ --format custom --msg-template \
"{abspath}:{line}: {test_id}[bandit]: {severity}: {msg}"
Provides same output as:
bandit -r examples/ --format custom
Tags can also be formatted in python string.format() style:
bandit -r examples/ --format custom --msg-template \
"{relpath:20.20s}: {line:03}: {test_id:^8}: DEFECT: {msg:>20}"
See python documentation for more information about formatting style:
https://docs.python.org/3.4/library/string.html
FILES
=====

View File

@ -37,6 +37,7 @@ bandit.formatters =
html = bandit.formatters.html:report
screen = bandit.formatters.screen:report
yaml = bandit.formatters.yaml:report
custom = bandit.formatters.custom:report
bandit.plugins =
# bandit/plugins/app_debug.py
flask_debug_true = bandit.plugins.app_debug:flask_debug_true

View File

@ -77,7 +77,7 @@ class RuntimeTests(testtools.TestCase):
self.assertIn("tests were discovered and loaded:", output)
def test_help_in_readme(self):
replace_list = [' ', '\t']
replace_list = [' ', '\t', '\n']
(retcode, output) = self._test_runtime(['bandit', '-h'])
for i in replace_list:
output = output.replace(i, '')