# 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 (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)