169 lines
6.4 KiB
Python
169 lines
6.4 KiB
Python
#
|
|
# Copyright 2014 Hewlett-Packard Development Company, L.P.
|
|
#
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
import copy
|
|
import logging
|
|
import warnings
|
|
|
|
from bandit.core import constants
|
|
from bandit.core import context as b_context
|
|
from bandit.core import utils
|
|
|
|
warnings.formatwarning = utils.warnings_formatter
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
|
|
class BanditTester:
|
|
def __init__(self, testset, debug, nosec_lines, metrics):
|
|
self.results = []
|
|
self.testset = testset
|
|
self.last_result = None
|
|
self.debug = debug
|
|
self.nosec_lines = nosec_lines
|
|
self.metrics = metrics
|
|
|
|
def run_tests(self, raw_context, checktype):
|
|
"""Runs all tests for a certain type of check, for example
|
|
|
|
Runs all tests for a certain type of check, for example 'functions'
|
|
store results in results.
|
|
|
|
:param raw_context: Raw context dictionary
|
|
:param checktype: The type of checks to run
|
|
:return: a score based on the number and type of test results with
|
|
extra metrics about nosec comments
|
|
"""
|
|
|
|
scores = {
|
|
"SEVERITY": [0] * len(constants.RANKING),
|
|
"CONFIDENCE": [0] * len(constants.RANKING),
|
|
}
|
|
|
|
tests = self.testset.get_tests(checktype)
|
|
for test in tests:
|
|
name = test.__name__
|
|
# execute test with an instance of the context class
|
|
temp_context = copy.copy(raw_context)
|
|
context = b_context.Context(temp_context)
|
|
try:
|
|
if hasattr(test, "_config"):
|
|
result = test(context, test._config)
|
|
else:
|
|
result = test(context)
|
|
|
|
if result is not None:
|
|
nosec_tests_to_skip = self._get_nosecs_from_contexts(
|
|
temp_context, test_result=result
|
|
)
|
|
|
|
if isinstance(temp_context["filename"], bytes):
|
|
result.fname = temp_context["filename"].decode("utf-8")
|
|
else:
|
|
result.fname = temp_context["filename"]
|
|
result.fdata = temp_context["file_data"]
|
|
|
|
if result.lineno is None:
|
|
result.lineno = temp_context["lineno"]
|
|
if result.linerange == []:
|
|
result.linerange = temp_context["linerange"]
|
|
if result.col_offset == -1:
|
|
result.col_offset = temp_context["col_offset"]
|
|
result.end_col_offset = temp_context.get(
|
|
"end_col_offset", 0
|
|
)
|
|
result.test = name
|
|
if result.test_id == "":
|
|
result.test_id = test._test_id
|
|
|
|
# don't skip the test if there was no nosec comment
|
|
if nosec_tests_to_skip is not None:
|
|
# If the set is empty then it means that nosec was
|
|
# used without test number -> update nosecs counter.
|
|
# If the test id is in the set of tests to skip,
|
|
# log and increment the skip by test count.
|
|
if not nosec_tests_to_skip:
|
|
LOG.debug("skipped, nosec without test number")
|
|
self.metrics.note_nosec()
|
|
continue
|
|
if result.test_id in nosec_tests_to_skip:
|
|
LOG.debug(
|
|
f"skipped, nosec for test {result.test_id}"
|
|
)
|
|
self.metrics.note_skipped_test()
|
|
continue
|
|
|
|
self.results.append(result)
|
|
|
|
LOG.debug("Issue identified by %s: %s", name, result)
|
|
sev = constants.RANKING.index(result.severity)
|
|
val = constants.RANKING_VALUES[result.severity]
|
|
scores["SEVERITY"][sev] += val
|
|
con = constants.RANKING.index(result.confidence)
|
|
val = constants.RANKING_VALUES[result.confidence]
|
|
scores["CONFIDENCE"][con] += val
|
|
else:
|
|
nosec_tests_to_skip = self._get_nosecs_from_contexts(
|
|
temp_context
|
|
)
|
|
if (
|
|
nosec_tests_to_skip
|
|
and test._test_id in nosec_tests_to_skip
|
|
):
|
|
LOG.warning(
|
|
f"nosec encountered ({test._test_id}), but no "
|
|
f"failed test on file "
|
|
f"{temp_context['filename']}:"
|
|
f"{temp_context['lineno']}"
|
|
)
|
|
|
|
except Exception as e:
|
|
self.report_error(name, context, e)
|
|
if self.debug:
|
|
raise
|
|
LOG.debug("Returning scores: %s", scores)
|
|
return scores
|
|
|
|
def _get_nosecs_from_contexts(self, context, test_result=None):
|
|
"""Use context and optional test result to get set of tests to skip.
|
|
:param context: temp context
|
|
:param test_result: optional test result
|
|
:return: set of tests to skip for the line based on contexts
|
|
"""
|
|
nosec_tests_to_skip = set()
|
|
base_tests = (
|
|
self.nosec_lines.get(test_result.lineno, None)
|
|
if test_result
|
|
else None
|
|
)
|
|
context_tests = utils.get_nosec(self.nosec_lines, context)
|
|
|
|
# if both are none there were no comments
|
|
# this is explicitly different from being empty.
|
|
# empty set indicates blanket nosec comment without
|
|
# individual test names or ids
|
|
if base_tests is None and context_tests is None:
|
|
nosec_tests_to_skip = None
|
|
|
|
# combine tests from current line and context line
|
|
if base_tests is not None:
|
|
nosec_tests_to_skip.update(base_tests)
|
|
if context_tests is not None:
|
|
nosec_tests_to_skip.update(context_tests)
|
|
|
|
return nosec_tests_to_skip
|
|
|
|
@staticmethod
|
|
def report_error(test, context, error):
|
|
what = "Bandit internal error running: "
|
|
what += f"{test} "
|
|
what += "on file %s at line %i: " % (
|
|
context._context["filename"],
|
|
context._context["lineno"],
|
|
)
|
|
what += str(error)
|
|
import traceback
|
|
|
|
what += traceback.format_exc()
|
|
LOG.error(what)
|