From 2cf15e15294a11241b5c91a439d1b0ab33dabd66 Mon Sep 17 00:00:00 2001 From: Joey Paskhay Date: Tue, 7 Mar 2023 18:01:53 -0700 Subject: [PATCH 1/5] add exclude-regex-patterns - mimics the functionality of the exclude-entropy-patterns option, but for regex scans. this will help reduce false positives such as env variables for user info auth in URLs - note that the scope is forced to "line" and not configurable due to the way the regex scan is done compared to the entropy scan - will follow up with changelog update after creating PR - a few unrelated changes were necessary pre-commit linters to pass --- docs/source/configuration.rst | 47 ++++++++++++++ tartufo/cli.py | 10 +++ tartufo/config.py | 19 +++--- tartufo/scanner.py | 48 ++++++++++++-- tartufo/types.py | 4 ++ tartufo/util.py | 19 +++++- tests/test_base_scanner.py | 116 ++++++++++++++++++++++++++++++++++ tests/test_config.py | 28 +++++++- tests/test_util.py | 14 ++++ 9 files changed, 287 insertions(+), 18 deletions(-) diff --git a/docs/source/configuration.rst b/docs/source/configuration.rst index b76c5376..b5aaebee 100644 --- a/docs/source/configuration.rst +++ b/docs/source/configuration.rst @@ -203,6 +203,53 @@ match-type No String ("search" or "match") Whether to perform a `search scope No String ("word" or "line") Whether to match against the current word or full line of text ============ ======== ============================ ============================================================== +.. regex-exclusion-patterns: + +Regex Exclusion Patterns +++++++++++++++++++++++++++ + +Regex scans can produce false positive matches such as environment variables in +URLs. To avoid these false positives, you can use the +``exclude-regex-patterns`` configuration option. These patterns will be +applied to and matched against any strings flagged by regex pattern checks. As +above, this directive utilizes an `array of tables`_, enabling two forms: + +Option 1: + +.. code-block:: toml + + [tool.tartufo] + exclude-regex-patterns = [ + {path-pattern = 'products_.*\.txt', pattern = '^SK[\d]{16,32}$', reason = 'SKU pattern that resembles Twilio API Key'}, + {path-pattern = '\.github/workflows/.*\.yaml', pattern = 'https://\${\S+}:\${\S+}@\S+', reason = 'URL with env variables for auth'}, + ] + +Option 2: + +.. code-block:: toml + + [[tool.tartufo.exclude-regex-patterns]] + path-pattern = 'products_.*\.txt' + pattern = '^SK[\d]{16,32}$' + reason = 'SKU pattern that resembles Twilio API Key' + + [[tool.tartufo.exclude-regex-patterns]] + path-pattern = '\.github/workflows/.*\.yaml' + pattern = 'https://\${\S+}:\${\S+}@\S+' + reason = 'URL with env variables for auth' + + +There are 4 relevant keys for this directive, as described below. + +============ ======== ============================ ============================================================== +Key Required Value Description +============ ======== ============================ ============================================================== +pattern Yes Regular expression The pattern used to check against the match +path-pattern No Regular expression A pattern to specify to what files the exclusion will apply +reason No String A plaintext reason the exclusion has been added +match-type No String ("search" or "match") Whether to perform a `search or match`_ regex operation +============ ======== ============================ ============================================================== + .. _TOML: https://toml.io/ .. _array of tables: https://toml.io/en/v1.0.0#array-of-tables .. _search or match: https://docs.python.org/3/library/re.html#search-vs-match diff --git a/tartufo/cli.py b/tartufo/cli.py index 02883e09..808d27f7 100644 --- a/tartufo/cli.py +++ b/tartufo/cli.py @@ -135,6 +135,16 @@ def get_command(self, ctx: click.Context, cmd_name: str) -> Optional[click.Comma excluded. ({"path-pattern": {path regex}, "pattern": {pattern regex}, "match-type": "match"|"search", "scope": "word"|"line"}).""", ) +@click.option( + "-xr", + "--exclude-regex-patterns", + multiple=True, + hidden=True, + type=click.UNPROCESSED, + help="""Specify a regular expression which matches regex strings to exclude from the scan. This option can be + specified multiple times to exclude multiple patterns. If not provided (default), no regex strings will be + excluded. ({"path-pattern": {path regex}, "pattern": {pattern regex}, "match-type": "match"|"search"}).""", +) @click.option( "-e", "--exclude-signatures", diff --git a/tartufo/config.py b/tartufo/config.py index 5fec84f1..50b6acee 100644 --- a/tartufo/config.py +++ b/tartufo/config.py @@ -264,7 +264,7 @@ def compile_path_rules(patterns: Iterable[str]) -> List[Pattern]: ] -def compile_rules(patterns: Iterable[Dict[str, str]]) -> List[Rule]: +def compile_rules(patterns: Iterable[Dict[str, str]], exclude_type: str) -> List[Rule]: """Take a list of regex string with paths and compile them into a List of Rule. :param patterns: The list of patterns to be compiled @@ -278,12 +278,15 @@ def compile_rules(patterns: Iterable[Dict[str, str]]) -> List[Rule]: raise ConfigException( f"Invalid value for match-type: {pattern.get('match-type')}" ) from exc - try: - scope = Scope(pattern.get("scope", Scope.Line.value)) - except ValueError as exc: - raise ConfigException( - f"Invalid value for scope: {pattern.get('scope')}" - ) from exc + if exclude_type != "regex": + try: + scope = Scope(pattern.get("scope", Scope.Line.value)) + except ValueError as exc: + raise ConfigException( + f"Invalid value for scope: {pattern.get('scope')}" + ) from exc + else: + scope = Scope.Line try: rules.append( Rule( @@ -296,6 +299,6 @@ def compile_rules(patterns: Iterable[Dict[str, str]]) -> List[Rule]: ) except KeyError as exc: raise ConfigException( - f"Invalid exclude-entropy-patterns: {patterns}" + f"Invalid exclude-{exclude_type}-patterns: {patterns}" ) from exc return rules diff --git a/tartufo/scanner.py b/tartufo/scanner.py index 7f730862..3805346a 100755 --- a/tartufo/scanner.py +++ b/tartufo/scanner.py @@ -140,6 +140,7 @@ class ScannerBase(abc.ABC): # pylint: disable=too-many-instance-attributes _included_paths: Optional[List[Pattern]] = None _excluded_paths: Optional[List[Pattern]] = None _excluded_entropy: Optional[List[Rule]] = None + _excluded_regex: Optional[List[Rule]] = None _rules_regexes: Optional[Set[Rule]] = None global_options: types.GlobalOptions logger: logging.Logger @@ -253,12 +254,30 @@ def excluded_entropy(self) -> List[Rule]: patterns = list(self.global_options.exclude_entropy_patterns or ()) + list( self.config_data.get("exclude_entropy_patterns", ()) ) - self._excluded_entropy = config.compile_rules(patterns) if patterns else [] + self._excluded_entropy = ( + config.compile_rules(patterns, "entropy") if patterns else [] + ) self.logger.debug( "Excluded entropy was initialized as: %s", self._excluded_entropy ) return self._excluded_entropy + @property + def excluded_regex(self) -> List[Rule]: + """Get a list of regexes used as an exclusive list of paths to scan.""" + if self._excluded_regex is None: + self.logger.info("Initializing excluded regex patterns") + patterns = list(self.global_options.exclude_regex_patterns or ()) + list( + self.config_data.get("exclude_regex_patterns", ()) + ) + self._excluded_regex = ( + config.compile_rules(patterns, "regex") if patterns else [] + ) + self.logger.debug( + "Excluded regex was initialized as: %s", self._excluded_regex + ) + return self._excluded_regex + @property def excluded_paths(self) -> List[Pattern]: """Get a list of regexes used to match paths to exclude from the scan""" @@ -371,7 +390,7 @@ def signature_is_excluded(self, blob: str, file_path: str) -> bool: @staticmethod @lru_cache(maxsize=None) - def rule_matches(rule: Rule, string: str, line: str, path: str) -> bool: + def rule_matches(rule: Rule, string: Optional[str], line: str, path: str) -> bool: """ Match string and path against rule. @@ -383,6 +402,8 @@ def rule_matches(rule: Rule, string: str, line: str, path: str) -> bool: """ match = False if rule.re_match_scope == Scope.Word: + if not string: + raise TartufoException(f"String required for {Scope.Word} scope") scope = string elif rule.re_match_scope == Scope.Line: scope = line @@ -415,6 +436,18 @@ def entropy_string_is_excluded(self, string: str, line: str, path: str) -> bool: for p in self.excluded_entropy ) + def regex_string_is_excluded(self, line: str, path: str) -> bool: + """Find whether the signature of some data has been excluded in configuration. + + :param line: Source line containing string of interest + :param path: Path to check against rule path pattern + :return: True if excluded, False otherwise + """ + + return bool(self.excluded_regex) and any( + ScannerBase.rule_matches(p, None, line, path) for p in self.excluded_regex + ) + @staticmethod @lru_cache(maxsize=None) def calculate_entropy(data: str) -> float: @@ -589,9 +622,14 @@ def scan_regex(self, chunk: types.Chunk) -> Generator[Issue, None, None]: for match in found_strings: # Filter out any explicitly "allowed" match signatures if not self.signature_is_excluded(match, chunk.file_path): - issue = Issue(types.IssueType.RegEx, match, chunk) - issue.issue_detail = rule.name - yield issue + if self.regex_string_is_excluded(match, chunk.file_path): + self.logger.debug( + "line containing regex was excluded: %s", match + ) + else: + issue = Issue(types.IssueType.RegEx, match, chunk) + issue.issue_detail = rule.name + yield issue @property @abc.abstractmethod diff --git a/tartufo/types.py b/tartufo/types.py index f8c2e088..1e7c6821 100644 --- a/tartufo/types.py +++ b/tartufo/types.py @@ -58,6 +58,8 @@ class GlobalOptions: :param exclude_path_patterns: A list of paths to be excluded from the scan :param exclude_entropy_patterns: Patterns to be excluded from entropy matches + :param exclude_regex_patterns: Patterns to be excluded from regex + matches :param exclude_signatures: Signatures of previously found findings to be excluded from the list of current findings :param exclude_findings: Signatures of previously found findings to be @@ -90,6 +92,7 @@ class GlobalOptions: "include_path_patterns", "exclude_path_patterns", "exclude_entropy_patterns", + "exclude_regex_patterns", "exclude_signatures", "output_dir", "temp_dir", @@ -111,6 +114,7 @@ class GlobalOptions: include_path_patterns: Tuple[Dict[str, str], ...] exclude_path_patterns: Tuple[Dict[str, str], ...] exclude_entropy_patterns: Tuple[Dict[str, str], ...] + exclude_regex_patterns: Tuple[Dict[str, str], ...] exclude_signatures: Tuple[Dict[str, str], ...] output_dir: Optional[str] temp_dir: Optional[str] diff --git a/tartufo/util.py b/tartufo/util.py index 09d4940c..b371c079 100644 --- a/tartufo/util.py +++ b/tartufo/util.py @@ -18,7 +18,6 @@ Generator, List, Optional, - NoReturn, Tuple, TYPE_CHECKING, Pattern, @@ -118,6 +117,17 @@ def echo_report_result(scanner: "ScannerBase", now: str): f" {pattern} (path={path_pattern}, scope={m_scope}, type={m_type}): {reason}" ) + click.echo("\nExcluded regex patterns:") + for e_item in scanner.excluded_regex: + pattern = e_item.pattern.pattern if e_item.pattern else "" + path_pattern = e_item.path_pattern.pattern if e_item.path_pattern else "" + m_scope = e_item.re_match_scope.value if e_item.re_match_scope else "" + m_type = e_item.re_match_type.value if e_item.re_match_type else "" + reason = e_item.name + click.echo( + f" {pattern} (path={path_pattern}, scope={m_scope}, type={m_type}): {reason}" + ) + def echo_result( options: "types.GlobalOptions", @@ -146,6 +156,9 @@ def echo_result( "exclude_entropy_patterns": [ str(pattern) for pattern in options.exclude_entropy_patterns ], + "exclude_regex_patterns": [ + str(pattern) for pattern in options.exclude_regex_patterns + ], # This member is for reference. Read below... # "found_issues": [ # issue.as_dict(compact=options.compact) for issue in scanner.issues @@ -186,6 +199,8 @@ def echo_result( click.echo("\n".join(scanner.excluded_signatures)) click.echo("\nExcluded entropy patterns:") click.echo("\n".join(str(path) for path in scanner.excluded_entropy)) + click.echo("\nExcluded regex patterns:") + click.echo("\n".join(str(path) for path in scanner.excluded_regex)) def write_outputs( @@ -242,7 +257,7 @@ def _style_func(msg: str, *_: Any, **__: Any) -> str: style_ok = style_error = style_warning = partial(_style_func) -def fail(msg: str, ctx: click.Context, code: int = 1) -> NoReturn: +def fail(msg: str, ctx: click.Context, code: int = 1) -> None: """Print out a styled error message and exit. :param msg: The message to print out to the user diff --git a/tests/test_base_scanner.py b/tests/test_base_scanner.py index 7c944027..08ca66bf 100644 --- a/tests/test_base_scanner.py +++ b/tests/test_base_scanner.py @@ -404,6 +404,122 @@ def test_issue_is_returned_if_signature_is_not_excluded( self.assertEqual(issues[0].issue_type, types.IssueType.RegEx) self.assertEqual(issues[0].matched_string, "foo") + @mock.patch("tartufo.scanner.ScannerBase.regex_string_is_excluded") + def test_issue_is_not_created_if_regex_string_is_excluded( + self, mock_regex_string: mock.MagicMock + ): + mock_regex_string.return_value = True + test_scanner = TestScanner(self.options) + test_scanner._rules_regexes = { # pylint: disable=protected-access + Rule( + name="foo", + pattern=re.compile("foo"), + path_pattern=None, + re_match_type=MatchType.Match, + re_match_scope=None, + ) + } + chunk = types.Chunk("foo", "bar", {}, False) + issues = list(test_scanner.scan_regex(chunk)) + mock_regex_string.assert_called_once_with("foo", "bar") + self.assertEqual(issues, []) + + @mock.patch("tartufo.scanner.ScannerBase.regex_string_is_excluded") + def test_issue_is_returned_if_regex_string_is_not_excluded( + self, mock_regex_string: mock.MagicMock + ): + mock_regex_string.return_value = False + test_scanner = TestScanner(self.options) + test_scanner._rules_regexes = { # pylint: disable=protected-access + Rule( + name="foo", + pattern=re.compile("foo"), + path_pattern=None, + re_match_type=MatchType.Match, + re_match_scope=None, + ) + } + chunk = types.Chunk("foo", "bar", {}, False) + issues = list(test_scanner.scan_regex(chunk)) + mock_regex_string.assert_called_once_with("foo", "bar") + self.assertEqual(len(issues), 1) + self.assertEqual(issues[0].issue_detail, "foo") + self.assertEqual(issues[0].issue_type, types.IssueType.RegEx) + self.assertEqual(issues[0].matched_string, "foo") + + def test_regex_string_is_excluded(self): + self.options.exclude_regex_patterns = [ + { + "path-pattern": r"docs/.*\.md", + "pattern": "f.*", + } + ] + test_scanner = TestScanner(self.options) + test_scanner._rules_regexes = { # pylint: disable=protected-access + Rule( + name="foo", + pattern=re.compile("foo"), + path_pattern=None, + re_match_type=MatchType.Match, + re_match_scope=None, + ) + } + excluded = test_scanner.regex_string_is_excluded("barfoo", "docs/README.md") + self.assertTrue(excluded) + + def test_regex_string_is_excluded_given_partial_line_match(self): + self.options.exclude_regex_patterns = [ + {"path-pattern": r"docs/.*\.md", "pattern": "line.+?foo"} + ] + test_scanner = TestScanner(self.options) + test_scanner._rules_regexes = { # pylint: disable=protected-access + Rule( + name="foo", + pattern=re.compile("foo"), + path_pattern=None, + re_match_type=MatchType.Match, + re_match_scope=None, + ) + } + excluded = test_scanner.regex_string_is_excluded( + "+a line that contains foo", "docs/README.md" + ) + self.assertTrue(excluded) + + def test_regex_string_is_not_excluded(self): + self.options.exclude_regex_patterns = [ + {"path-pattern": r"foo\..*", "pattern": "f.*", "match-type": "match"} + ] + test_scanner = TestScanner(self.options) + test_scanner._rules_regexes = { # pylint: disable=protected-access + Rule( + name="foo", + pattern=re.compile("foo"), + path_pattern=None, + re_match_type=MatchType.Match, + re_match_scope=None, + ) + } + excluded = test_scanner.regex_string_is_excluded("bar", "foo.py") + self.assertFalse(excluded) + + def test_regex_string_is_not_excluded_given_different_path(self): + self.options.exclude_regex_patterns = [ + {"path-pattern": r"foo\..*", "pattern": "f.*", "match-type": "match"} + ] + test_scanner = TestScanner(self.options) + test_scanner._rules_regexes = { # pylint: disable=protected-access + Rule( + name="foo", + pattern=re.compile("foo"), + path_pattern=None, + re_match_type=MatchType.Match, + re_match_scope=None, + ) + } + excluded = test_scanner.regex_string_is_excluded("bar", "bar.py") + self.assertFalse(excluded) + class EntropyManagementTests(ScannerTestCase): def setUp(self) -> None: diff --git a/tests/test_config.py b/tests/test_config.py index 943f4a0e..bdc42df2 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -300,7 +300,8 @@ def test_path_is_used(self): {"path-pattern": r"src/.*", "pattern": r"^[a-zA-Z0-9]{26}$"}, {"pattern": r"^[a-zA-Z0-9]test$"}, {"path-pattern": r"src/.*", "pattern": r"^[a-zA-Z0-9]{26}::test$"}, - ] + ], + "entropy", ) self.assertCountEqual( rules, @@ -335,7 +336,28 @@ def test_match_can_contain_delimiter(self): rules = config.compile_rules( [ {"pattern": r"^[a-zA-Z0-9]::test$"}, - ] + ], + "entropy", + ) + self.assertEqual( + rules, + [ + Rule( + None, + re.compile(r"^[a-zA-Z0-9]::test$"), + re.compile(r""), + re_match_type=MatchType.Search, + re_match_scope=Scope.Line, + ) + ], + ) + + def test_regex_ignores_scope(self): + rules = config.compile_rules( + [ + {"pattern": r"^[a-zA-Z0-9]::test$", "scope": "word"}, + ], + "regex", ) self.assertEqual( rules, @@ -354,7 +376,7 @@ def test_config_exception_is_raised_if_no_match_field_found(self): with self.assertRaisesRegex( types.ConfigException, "Invalid exclude-entropy-patterns: " ): - config.compile_rules([{"foo": "bar"}]) + config.compile_rules([{"foo": "bar"}], "entropy") @mock.patch("tartufo.util.process_issues", new=mock.MagicMock()) @mock.patch("tartufo.scanner.FolderScanner") diff --git a/tests/test_util.py b/tests/test_util.py index 58be214a..5d73739a 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -270,6 +270,7 @@ def test_echo_result_outputs_proper_json_when_requested( output_format=types.OutputFormat.Json.value, exclude_signatures=[], exclude_entropy_patterns=[], + exclude_regex_patterns=[], ) # We're generating JSON piecemeal, so if we want to be safe we'll recover @@ -288,6 +289,7 @@ def test_echo_result_outputs_proper_json_when_requested( "excluded_paths": [], "excluded_signatures": [], "exclude_entropy_patterns": [], + "exclude_regex_patterns": [], "found_issues": [ { "issue_type": "High Entropy", @@ -334,10 +336,15 @@ def test_echo_result_outputs_proper_json_when_requested_pathtype( "aaaaa::bbbbb", "ccccc::ddddd", ] + exclude_regex_patterns = [ + "eeeee::fffff", + "ggggg::hhhhh", + ] options = generate_options( GlobalOptions, output_format=types.OutputFormat.Json.value, exclude_entropy_patterns=exclude_entropy_patterns, + exclude_regex_patterns=exclude_regex_patterns, ) # We're generating JSON piecemeal, so if we want to be safe we'll recover @@ -361,6 +368,10 @@ def test_echo_result_outputs_proper_json_when_requested_pathtype( "aaaaa::bbbbb", "ccccc::ddddd", ], + "exclude_regex_patterns": [ + "eeeee::fffff", + "ggggg::hhhhh", + ], "found_issues": [ { "issue_type": "High Entropy", @@ -519,6 +530,7 @@ def test_echo_report_result_given_no_excludes_outputs_empty_report( exclude_signatures=[], exclude_path_patterns=[], exclude_entropy_patterns=[], + exclude_regex_patterns=[], ) mock_scanner.global_options = options mock_scanner.issues = [] @@ -561,6 +573,7 @@ def test_echo_report_result_given_disabled_report_shows_disabled( exclude_signatures=[], exclude_path_patterns=[], exclude_entropy_patterns=[], + exclude_regex_patterns=[], ) mock_scanner.global_options = options mock_scanner.issues = [] @@ -602,6 +615,7 @@ def test_echo_report_result_issues_report_shows_issues( exclude_signatures=[], exclude_path_patterns=[], exclude_entropy_patterns=[], + exclude_regex_patterns=[], ) mock_scanner.global_options = options issue_1 = scanner.Issue( From 9e1cc98b78e2fc166ef541c5427453449131614f Mon Sep 17 00:00:00 2001 From: Joey Paskhay Date: Tue, 7 Mar 2023 18:11:06 -0700 Subject: [PATCH 2/5] update changelog with `exclude-regex-patterns` --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fc04e51..75df9460 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ vX.X.X - Mar 3 2023 Features: * [#455](https://github.com/godaddy/tartufo/pull/455) - Update documentation to fix incorrect wording +* [#458](https://github.com/godaddy/tartufo/pull/458) - Adds `--exclude-regex-patterns` to allow for regex-based exclusions v4.0.1 - Mar 1 2023 -------------------- From 16f0c147c887e440155d628d7edf8156c72c286c Mon Sep 17 00:00:00 2001 From: Joey Paskhay Date: Wed, 8 Mar 2023 12:25:42 -0700 Subject: [PATCH 3/5] Apply suggestions from code review Co-authored-by: Scott Bailey <72747501+rbailey-godaddy@users.noreply.github.com> --- docs/source/configuration.rst | 2 +- tartufo/config.py | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/source/configuration.rst b/docs/source/configuration.rst index b5aaebee..795f57cc 100644 --- a/docs/source/configuration.rst +++ b/docs/source/configuration.rst @@ -206,7 +206,7 @@ scope No String ("word" or "line") Whether to match against the .. regex-exclusion-patterns: Regex Exclusion Patterns -++++++++++++++++++++++++++ +++++++++++++++++++++++++ Regex scans can produce false positive matches such as environment variables in URLs. To avoid these false positives, you can use the diff --git a/tartufo/config.py b/tartufo/config.py index 50b6acee..69477c61 100644 --- a/tartufo/config.py +++ b/tartufo/config.py @@ -278,15 +278,17 @@ def compile_rules(patterns: Iterable[Dict[str, str]], exclude_type: str) -> List raise ConfigException( f"Invalid value for match-type: {pattern.get('match-type')}" ) from exc - if exclude_type != "regex": + if exclude_type == "regex": + # regex exclusions always have line scope + scope = Scope.Line + else: + # entropy exclusions can specify scope try: scope = Scope(pattern.get("scope", Scope.Line.value)) except ValueError as exc: raise ConfigException( f"Invalid value for scope: {pattern.get('scope')}" ) from exc - else: - scope = Scope.Line try: rules.append( Rule( From 8695197b16d82977b3d327677147ebbd0921dcf4 Mon Sep 17 00:00:00 2001 From: Joey Paskhay Date: Wed, 8 Mar 2023 13:07:01 -0700 Subject: [PATCH 4/5] update documentation to note lack of scope option, attempting to revert type hint change --- docs/source/configuration.rst | 5 ++++- tartufo/util.py | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/source/configuration.rst b/docs/source/configuration.rst index 795f57cc..5e528f1d 100644 --- a/docs/source/configuration.rst +++ b/docs/source/configuration.rst @@ -239,7 +239,10 @@ Option 2: reason = 'URL with env variables for auth' -There are 4 relevant keys for this directive, as described below. +There are 4 relevant keys for this directive, as described below. Note that +regex scans differ from entropy scans, so the exclusion pattern is always +tested against the offending regex match(es). As a result, there is no +``scope`` key for this directive. ============ ======== ============================ ============================================================== Key Required Value Description diff --git a/tartufo/util.py b/tartufo/util.py index b371c079..cbd5601b 100644 --- a/tartufo/util.py +++ b/tartufo/util.py @@ -18,6 +18,7 @@ Generator, List, Optional, + NoReturn, Tuple, TYPE_CHECKING, Pattern, @@ -257,7 +258,7 @@ def _style_func(msg: str, *_: Any, **__: Any) -> str: style_ok = style_error = style_warning = partial(_style_func) -def fail(msg: str, ctx: click.Context, code: int = 1) -> None: +def fail(msg: str, ctx: click.Context, code: int = 1) -> NoReturn: """Print out a styled error message and exit. :param msg: The message to print out to the user From bd02480e8299c50b064035e6f12b7ab36113dec1 Mon Sep 17 00:00:00 2001 From: Scott Bailey Date: Thu, 30 Mar 2023 17:44:55 -0400 Subject: [PATCH 5/5] Fix unit test Force cache flush before calling function with @lru_cache decorator; this ensures the return actually reflects the environment constructed by the unit test and not something left over from an unrelated test. --- tests/test_util.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_util.py b/tests/test_util.py index 5d73739a..e388ce7e 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -707,6 +707,7 @@ def test_style_when_is_a_tty(self, mock_stdout): @mock.patch("tartufo.util.blake2s") def test_signature_is_generated_with_snippet_and_filename(self, mock_hash): + util.generate_signature.cache_clear() util.generate_signature("foo", "bar") mock_hash.assert_called_once_with(b"foo$$bar")