custom.py 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. #
  2. # Copyright (c) 2017 Hewlett Packard Enterprise
  3. #
  4. # SPDX-License-Identifier: Apache-2.0
  5. """
  6. ================
  7. Custom Formatter
  8. ================
  9. This formatter outputs the issues in custom machine-readable format.
  10. default template: ``{abspath}:{line}: {test_id}[bandit]: {severity}: {msg}``
  11. :Example:
  12. .. code-block:: none
  13. /usr/lib/python3.6/site-packages/openlp/core/utils/__init__.py:\
  14. 405: B310[bandit]: MEDIUM: Audit url open for permitted schemes. \
  15. Allowing use of file:/ or custom schemes is often unexpected.
  16. .. versionadded:: 1.5.0
  17. .. versionchanged:: 1.7.3
  18. New field `CWE` added to output
  19. """
  20. import logging
  21. import os
  22. import re
  23. import string
  24. import sys
  25. from bandit.core import test_properties
  26. LOG = logging.getLogger(__name__)
  27. class SafeMapper(dict):
  28. """Safe mapper to handle format key errors"""
  29. @classmethod # To prevent PEP8 warnings in the test suite
  30. def __missing__(cls, key):
  31. return "{%s}" % key
  32. @test_properties.accepts_baseline
  33. def report(manager, fileobj, sev_level, conf_level, template=None):
  34. """Prints issues in custom format
  35. :param manager: the bandit manager object
  36. :param fileobj: The output file object, which may be sys.stdout
  37. :param sev_level: Filtering severity level
  38. :param conf_level: Filtering confidence level
  39. :param template: Output template with non-terminal tags <N>
  40. (default: '{abspath}:{line}:
  41. {test_id}[bandit]: {severity}: {msg}')
  42. """
  43. machine_output = {"results": [], "errors": []}
  44. for (fname, reason) in manager.get_skipped():
  45. machine_output["errors"].append({"filename": fname, "reason": reason})
  46. results = manager.get_issue_list(
  47. sev_level=sev_level, conf_level=conf_level
  48. )
  49. msg_template = template
  50. if template is None:
  51. msg_template = "{abspath}:{line}: {test_id}[bandit]: {severity}: {msg}"
  52. # Dictionary of non-terminal tags that will be expanded
  53. tag_mapper = {
  54. "abspath": lambda issue: os.path.abspath(issue.fname),
  55. "relpath": lambda issue: os.path.relpath(issue.fname),
  56. "line": lambda issue: issue.lineno,
  57. "col": lambda issue: issue.col_offset,
  58. "end_col": lambda issue: issue.end_col_offset,
  59. "test_id": lambda issue: issue.test_id,
  60. "severity": lambda issue: issue.severity,
  61. "msg": lambda issue: issue.text,
  62. "confidence": lambda issue: issue.confidence,
  63. "range": lambda issue: issue.linerange,
  64. "cwe": lambda issue: issue.cwe,
  65. }
  66. # Create dictionary with tag sets to speed up search for similar tags
  67. tag_sim_dict = {tag: set(tag) for tag, _ in tag_mapper.items()}
  68. # Parse the format_string template and check the validity of tags
  69. try:
  70. parsed_template_orig = list(string.Formatter().parse(msg_template))
  71. # of type (literal_text, field_name, fmt_spec, conversion)
  72. # Check the format validity only, ignore keys
  73. string.Formatter().vformat(msg_template, (), SafeMapper(line=0))
  74. except ValueError as e:
  75. LOG.error("Template is not in valid format: %s", e.args[0])
  76. sys.exit(2)
  77. tag_set = {t[1] for t in parsed_template_orig if t[1] is not None}
  78. if not tag_set:
  79. LOG.error("No tags were found in the template. Are you missing '{}'?")
  80. sys.exit(2)
  81. def get_similar_tag(tag):
  82. similarity_list = [
  83. (len(set(tag) & t_set), t) for t, t_set in tag_sim_dict.items()
  84. ]
  85. return sorted(similarity_list)[-1][1]
  86. tag_blacklist = []
  87. for tag in tag_set:
  88. # check if the tag is in dictionary
  89. if tag not in tag_mapper:
  90. similar_tag = get_similar_tag(tag)
  91. LOG.warning(
  92. "Tag '%s' was not recognized and will be skipped, "
  93. "did you mean to use '%s'?",
  94. tag,
  95. similar_tag,
  96. )
  97. tag_blacklist += [tag]
  98. # Compose the message template back with the valid values only
  99. msg_parsed_template_list = []
  100. for literal_text, field_name, fmt_spec, conversion in parsed_template_orig:
  101. if literal_text:
  102. # if there is '{' or '}', double it to prevent expansion
  103. literal_text = re.sub("{", "{{", literal_text)
  104. literal_text = re.sub("}", "}}", literal_text)
  105. msg_parsed_template_list.append(literal_text)
  106. if field_name is not None:
  107. if field_name in tag_blacklist:
  108. msg_parsed_template_list.append(field_name)
  109. continue
  110. # Append the fmt_spec part
  111. params = [field_name, fmt_spec, conversion]
  112. markers = ["", ":", "!"]
  113. msg_parsed_template_list.append(
  114. ["{"]
  115. + [
  116. "%s" % (m + p) if p else ""
  117. for m, p in zip(markers, params)
  118. ]
  119. + ["}"]
  120. )
  121. msg_parsed_template = (
  122. "".join([item for lst in msg_parsed_template_list for item in lst])
  123. + "\n"
  124. )
  125. with fileobj:
  126. for defect in results:
  127. evaluated_tags = SafeMapper(
  128. (k, v(defect)) for k, v in tag_mapper.items()
  129. )
  130. output = msg_parsed_template.format(**evaluated_tags)
  131. fileobj.write(output)
  132. if fileobj.name != sys.stdout.name:
  133. LOG.info("Result written to file: %s", fileobj.name)