CoolFace
Datasetpublic

12Parker/python-migrations

sourceHugging Faceupdated 2y agoView on Hugging Face
0likes92downloads
tartufo-task-instances.jsonl.all2 linesDownload Raw Back to train
1{"repo": "godaddy/tartufo", "pull_number": 19, "instance_id": "godaddy__tartufo-19", "issue_numbers": "", "base_commit": "db990f3a75a995d7936a343b8d3e0928ed2e38a5", "patch": "diff --git a/setup.py b/setup.py\n--- a/setup.py\n+++ b/setup.py\n@@ -2,8 +2,11 @@\n from setuptools import setup\n \n INSTALL_REQUIRES = [\n-    'GitPython == 2.1.1',\n-    'truffleHogRegexes == 0.0.7',\n+    \"click >= 7.0.0, < 8.0.0\",\n+    \"GitPython >= 2.1.1, < 4.0.0\",\n+    \"pathlib2; python_version < '3.4'\",\n+    \"toml >= 0.10.0, < 1.0.0\",\n+    \"truffleHogRegexes >= 0.0.7, < 1.0.0\",\n     \"typing; python_version < '3.5'\",\n ]\n \ndiff --git a/tartufo/__main__.py b/tartufo/__main__.py\n--- a/tartufo/__main__.py\n+++ b/tartufo/__main__.py\n@@ -1,7 +1,5 @@\n-import sys\n-\n from tartufo import cli\n \n \n if __name__ == \"__main__\":\n-    cli.main(sys.argv[1:])\n+    cli.main()  # pylint: disable=no-value-for-parameter\ndiff --git a/tartufo/cli.py b/tartufo/cli.py\n--- a/tartufo/cli.py\n+++ b/tartufo/cli.py\n@@ -1,200 +1,142 @@\n # -*- coding: utf-8 -*-\n \n-import argparse\n import re\n-from typing import List, Optional\n+from functools import partial\n+from typing import cast, TextIO\n \n+import click\n import truffleHogRegexes.regexChecks\n \n from tartufo import config, scanner, util\n \n \n-def main(argv=None):\n-    # type: (Optional[List[str]]) -> int\n-    args = parse_args(argv)\n+err = partial(click.secho, fg=\"red\", bold=True, err=True)  # pylint: disable=invalid-name\n \n-    if not (args.do_entropy or args.do_regex):\n-        raise RuntimeError(\"no analysis requested\")\n \n-    rules_regexes = config.configure_regexes_from_args(args, truffleHogRegexes.regexChecks.regexes)\n+@click.command(name=\"tartufo\",  # noqa: C901\n+               context_settings=dict(help_option_names=[\"-h\", \"--help\"]))\n+@click.option(\"--json/--no-json\", help=\"Output in JSON format.\", is_flag=True)\n+@click.option(\"--rules\", multiple=True, type=click.File(\"r\"),\n+              help=\"Path(s) to regex rules json list file(s).\")\n+@click.option(\"--default-regexes/--no-default-regexes\", is_flag=True, default=True,\n+              help=\"Whether to include the default regex list when configuring\"\n+                   \" search patterns. Only applicable if --rules is also specified.\"\n+                   \" [default: --default-regexes]\")\n+@click.option(\"--entropy/--no-entropy\", is_flag=True, default=True,\n+              help=\"Enable entropy checks. [default: True]\")\n+@click.option(\"--regex/--no-regex\", is_flag=True, default=False,\n+              help=\"Enable high signal regexes checks. [default: False]\")\n+@click.option(\"--since-commit\", help=\"Only scan from a given commit hash.\")\n+@click.option(\"--max-depth\", default=1000000,\n+              help=\"The max commit depth to go back when searching for secrets.\"\n+                   \" [default: 1000000]\")\n+@click.option(\"--branch\", help=\"Specify a branch name to scan only that branch.\")\n+@click.option(\"-i\", \"--include-paths\", type=click.File(\"r\"),\n+              help=\"File with regular expressions (one per line), at least one of \"\n+                   \"which must match a Git object path in order for it to be scanned; \"\n+                   \"lines starting with '#' are treated as comments and are ignored. \"\n+                   \"If empty or not provided (default), all Git object paths are \"\n+                   \"included unless otherwise excluded via the --exclude-paths option.\")\n+@click.option(\"-x\", \"--exclude-paths\", type=click.File(\"r\"),\n+              help=\"File with regular expressions (one per line), none of which may \"\n+                   \"match a Git object path in order for it to be scanned; lines \"\n+                   \"starting with '#' are treated as comments and are ignored. If \"\n+                   \"empty or not provided (default), no Git object paths are excluded \"\n+                   \"unless effectively excluded via the --include-paths option.\")\n+@click.option(\"--repo-path\",\n+              type=click.Path(\n+                  exists=True,\n+                  file_okay=False,\n+                  resolve_path=True,\n+                  allow_dash=False\n+              ),\n+              help=\"Path to local repo clone. If provided, git_url will not be used.\")\n+@click.option(\"--cleanup/--no-cleanup\", is_flag=True, default=False,\n+              help=\"Clean up all temporary result files. [default: False]\")\n+@click.option(\"--pre-commit\", is_flag=True, default=False,\n+              help=\"Scan staged files in local repo clone.\")\n+@click.option(\"--config\",\n+              type=click.File(mode='r'),\n+              is_eager=True,\n+              callback=config.read_pyproject_toml,\n+              help=\"Read configuration from specified file. [default: pyproject.toml]\")\n+@click.argument(\"git_url\", required=False)\n+@click.pass_context\n+def main(ctx, **kwargs):\n+    # type: (click.Context, config.OptionTypes) -> None\n+    \"\"\"Find secrets hidden in the depths of git.\n+\n+    Tartufo will, by default, scan the entire history of a git repository\n+    for any text which looks like a secret, password, credential, etc. It can\n+    also be made to work in pre-commit mode, for scanning blobs of text as a\n+    pre-commit hook.\n+    \"\"\"\n+    if not any((kwargs[\"entropy\"], kwargs[\"regex\"])):\n+        err(\"No analysis requested.\")\n+        ctx.exit(1)\n+    if not any((kwargs[\"pre_commit\"], kwargs[\"repo_path\"], kwargs[\"git_url\"])):\n+        err(\"You must specify one of --pre-commit, --repo-path, or git_url.\")\n+        ctx.exit(1)\n+    try:\n+        rules_regexes = config.configure_regexes_from_args(\n+            kwargs,\n+            truffleHogRegexes.regexChecks.regexes\n+        )\n+    except ValueError as exc:\n+        err(str(exc))\n+        ctx.exit(1)\n+    if kwargs[\"regex\"] and not rules_regexes:\n+        err(\"Regex checks requested, but no regexes found.\")\n+        ctx.exit(1)\n \n     # read & compile path inclusion/exclusion patterns\n     path_inclusions = []\n     path_exclusions = []\n-    if args.include_paths:\n-        for pattern in set(l[:-1].lstrip() for l in args.include_paths):\n+    paths_file = cast(TextIO, kwargs[\"include_paths\"])\n+    if paths_file:\n+        for pattern in [l[:-1].lstrip() for l in paths_file]:\n             if pattern and not pattern.startswith(\"#\"):\n                 path_inclusions.append(re.compile(pattern))\n-    if args.exclude_paths:\n-        for pattern in set(l[:-1].lstrip() for l in args.exclude_paths):\n+    paths_file = cast(TextIO, kwargs[\"exclude_paths\"])\n+    if paths_file:\n+        for pattern in [l[:-1].lstrip() for l in paths_file]:\n             if pattern and not pattern.startswith(\"#\"):\n                 path_exclusions.append(re.compile(pattern))\n \n-    if args.pre_commit:\n+    if kwargs[\"pre_commit\"]:\n         output = scanner.find_staged(\n-            args.repo_path,\n-            args.output_json,\n-            args.do_regex,\n-            args.do_entropy,\n+            cast(str, kwargs[\"repo_path\"]),\n+            cast(bool, kwargs[\"json\"]),\n+            cast(bool, kwargs[\"regex\"]),\n+            cast(bool, kwargs[\"entropy\"]),\n             custom_regexes=rules_regexes,\n             suppress_output=False,\n             path_inclusions=path_inclusions,\n             path_exclusions=path_exclusions,\n         )\n     else:\n-        if args.repo_path is None and args.git_url is None:\n-            print(\"ERROR: One of git_url or --repo_path is required\")\n-            return 1\n         output = scanner.find_strings(\n-            args.git_url,\n-            args.since_commit,\n-            args.max_depth,\n-            args.output_json,\n-            args.do_regex,\n-            args.do_entropy,\n+            cast(str, kwargs[\"git_url\"]),\n+            cast(str, kwargs[\"since_commit\"]),\n+            cast(int, kwargs[\"max_depth\"]),\n+            cast(bool, kwargs[\"json\"]),\n+            cast(bool, kwargs[\"regex\"]),\n+            cast(bool, kwargs[\"entropy\"]),\n             custom_regexes=rules_regexes,\n             suppress_output=False,\n-            branch=args.branch,\n-            repo_path=args.repo_path,\n+            branch=cast(str, kwargs[\"branch\"]),\n+            repo_path=cast(str, kwargs[\"repo_path\"]),\n             path_inclusions=path_inclusions,\n             path_exclusions=path_exclusions,\n         )\n-    if args.cleanup:\n+\n+    if kwargs[\"cleanup\"]:\n         util.clean_outputs(output)\n     else:\n         issues_path = output.get(\"issues_path\", None)\n         if issues_path:\n             print(\"Results have been saved in {}\".format(issues_path))\n \n-    if output[\"found_issues\"]:\n-        return 1\n-    return 0\n-\n-\n-def parse_args(argv=None):\n-    # type: (Optional[List[str]]) -> argparse.Namespace\n-    parser = argparse.ArgumentParser(\n-        description=\"Find secrets hidden in the depths of git.\"\n-    )\n-    parser.add_argument(\n-        \"--json\", dest=\"output_json\", action=\"store_true\", help=\"Output in JSON\"\n-    )\n-    parser.add_argument(\n-        \"--git-rules-repo\",\n-        dest=\"git_rules_repo\",\n-        help=\"Git repo for externally-sourced rules\",\n-    )\n-    parser.add_argument(\n-        \"--git-rules\",\n-        dest=\"git_rules_filenames\",\n-        nargs=\"+\",\n-        default=[],\n-        action=\"append\",\n-        help=\"Git-relative path(s) to regex rules json list file(s)\",\n-    )\n-    parser.add_argument(\n-        \"--rules\",\n-        dest=\"rules_filenames\",\n-        nargs=\"+\",\n-        default=[],\n-        action=\"append\",\n-        help=\"Path(s) to regex rules json list file(s)\",\n-    )\n-    parser.add_argument(\n-        \"--default-regexes\",\n-        dest=\"do_default_regexes\",\n-        metavar=\"BOOLEAN\",\n-        nargs=\"?\",\n-        default=\"True\",\n-        const=\"True\",\n-        help=\"If set to one of (no, n, false, f, n, 0) and --rules or --git-rules is also specified, ignore default\"\n-             \"regexes, otherwise the regexes from the rules files will be appended to the default regexes\",\n-    )\n-    parser.add_argument(\n-        \"--entropy\",\n-        dest=\"do_entropy\",\n-        metavar=\"BOOLEAN\",\n-        nargs=\"?\",\n-        default=\"True\",\n-        const=\"True\",\n-        help=\"Enable entropy checks [default: True]\",\n-    )\n-    parser.add_argument(\n-        \"--regex\",\n-        dest=\"do_regex\",\n-        metavar=\"BOOLEAN\",\n-        nargs=\"?\",\n-        default=\"False\",\n-        const=\"True\",\n-        help=\"Enable high signal regex checks [default: False]\",\n-    )\n-    parser.add_argument(\n-        \"--since_commit\",\n-        dest=\"since_commit\",\n-        default=None,\n-        help=\"Only scan from a given commit hash\",\n-    )\n-    parser.add_argument(\n-        \"--max_depth\",\n-        dest=\"max_depth\",\n-        default=1000000,\n-        help=\"The max commit depth to go back when searching for \" \"secrets\",\n-    )\n-    parser.add_argument(\n-        \"--branch\", dest=\"branch\", default=None, help=\"Name of the branch to be scanned\"\n-    )\n-    parser.add_argument(\n-        \"-i\",\n-        \"--include_paths\",\n-        type=argparse.FileType(\"r\"),\n-        metavar=\"INCLUDE_PATHS_FILE\",\n-        help=\"File with regular expressions (one per line), at least one of which must match a Git \"\n-        'object path in order for it to be scanned; lines starting with \"#\" are treated as '\n-        \"comments and are ignored. If empty or not provided (default), all Git object paths are \"\n-        \"included unless otherwise excluded via the --exclude_paths option.\",\n-    )\n-    parser.add_argument(\n-        \"-x\",\n-        \"--exclude_paths\",\n-        type=argparse.FileType(\"r\"),\n-        metavar=\"EXCLUDE_PATHS_FILE\",\n-        help=\"File with regular expressions (one per line), none of which may match a Git object path \"\n-        'in order for it to be scanned; lines starting with \"#\" are treated as comments and are '\n-        \"ignored. If empty or not provided (default), no Git object paths are excluded unless \"\n-        \"effectively excluded via the --include_paths option.\",\n-    )\n-    parser.add_argument(\n-        \"--repo_path\",\n-        type=str,\n-        dest=\"repo_path\",\n-        default=None,\n-        help=\"Path to local repo clone. If provided, git_url will not be used\",\n-    )\n-    parser.add_argument(\n-        \"--cleanup\",\n-        dest=\"cleanup\",\n-        action=\"store_true\",\n-        help=\"Clean up all temporary result files\",\n-    )\n-    parser.add_argument(\n-        \"git_url\", nargs=\"?\", type=str, help=\"repository URL for secret searching\"\n-    )\n-    parser.add_argument(\n-        \"--pre_commit\",\n-        dest=\"pre_commit\",\n-        action=\"store_true\",\n-        help=\"Scan staged files in local repo clone\",\n-    )\n-\n-    args = parser.parse_args(argv)\n-\n-    # rules_filenames and git_rules_filenames will be generated as a list of lists, they need to be flattened\n-    filename_lists = args.rules_filenames\n-    args.rules_filenames = [filename for filenames in filename_lists for filename in filenames]\n-    filename_lists = args.git_rules_filenames\n-    args.git_rules_filenames = [filename for filenames in filename_lists for filename in filenames]\n-\n-    args.do_entropy = util.str2bool(args.do_entropy)\n-    args.do_regex = util.str2bool(args.do_regex)\n-    args.do_default_regexes = util.str2bool(args.do_default_regexes)\n-    return args\n+    if output.get(\"found_issues\", False):\n+        ctx.exit(1)\n+    ctx.exit(0)\ndiff --git a/tartufo/config.py b/tartufo/config.py\n--- a/tartufo/config.py\n+++ b/tartufo/config.py\n@@ -1,59 +1,98 @@\n import argparse  # pylint: disable=unused-import\n import json\n-import os\n import re\n import shutil\n-from typing import Dict, List, Pattern, Union\n+from functools import partial\n+from typing import cast, Dict, List, Optional, Pattern, TextIO, Tuple, Union\n \n+import click\n+import toml\n from tartufo import util\n \n+try:\n+    import pathlib\n+except ImportError:\n+    import pathlib2 as pathlib  # type: ignore\n \n+\n+err = partial(click.secho, fg=\"red\", bold=True, err=True)  # pylint: disable=invalid-name\n+OptionTypes = Union[str, int, bool, None, TextIO, Tuple[TextIO, ...]]\n+OptionsDict = Dict[str, OptionTypes]\n PatternDict = Dict[str, Union[str, Pattern]]\n \n \n-def configure_regexes_from_args(args, default_regexes):\n-    # type: (argparse.Namespace, PatternDict) -> PatternDict\n-    if args.do_regex:\n-        if args.rules_filenames or (args.git_rules_repo and args.git_rules):\n-            rules_regexes = dict(default_regexes) if args.do_default_regexes else {}\n-            if args.git_rules_repo and args.git_rules:\n-                configure_regexes_from_git(args.git_rules_repo, args.git_rules, rules_regexes)\n-            if args.rules_filenames:\n-                configure_regexes_from_rules_files(args.rules_filenames, rules_regexes)\n-            return rules_regexes\n+def read_pyproject_toml(ctx, _param, value):\n+    # type: (click.Context, click.Parameter, Union[str, TextIO]) -> Optional[str]\n+    if not value:\n+        root_path = ctx.params.get(\"repo_path\", None)\n+        if not root_path:\n+            root_path = \".\"\n+        root_path = pathlib.Path(root_path).resolve()\n+        config_path = root_path / \"pyproject.toml\"\n+        if config_path.is_file():\n+            value = str(config_path)\n+        else:\n+            return None\n+    try:\n+        toml_file = toml.load(value)\n+        config = toml_file.get(\"tool\", {}).get(\"tartufo\", {})\n+    except (toml.TomlDecodeError, OSError) as exc:\n+        raise click.FileError(\n+            filename=str(config_path),\n+            hint=\"Error reading configuration file: {}\".format(exc)\n+        )\n+    if not config:\n+        return None\n+    if ctx.default_map is None:\n+        ctx.default_map = {}\n+    ctx.default_map.update(  # type: ignore\n+        {k.replace(\"--\", \"\").replace(\"-\", \"_\"): v for k, v in config.items()}\n+    )\n+    return str(value)\n+\n \n-        return dict(default_regexes)\n-    return {}\n+def configure_regexes_from_args(args, default_regexes):\n+    # type: (OptionsDict, PatternDict) -> PatternDict\n+    regexes = {}\n+    if args[\"regex\"]:\n+        if args[\"default_regexes\"]:\n+            regexes.update(default_regexes)\n+        # FIXME: git_rules(_repo) functionality was never called, nor tested.\n+        #   https://github.com/godaddy/tartufo/issues/17 added for a new feature\n+        rules_files = cast(Tuple[TextIO, ...], args[\"rules\"])\n+        if rules_files:  # or (args.git_rules_repo and args.git_rules):\n+            # if args.git_rules_repo and args.git_rules:\n+            #     configure_regexes_from_git(args.git_rules_repo, args.git_rules, rules_regexes)\n+            if rules_files:\n+                for rules_file in rules_files:\n+                    loaded = load_rules_from_file(rules_file)\n+                    dupes = set(loaded.keys()).intersection(regexes.keys())\n+                    if dupes:\n+                        raise ValueError(\"Rule(s) were defined multiple time: {}\".format(dupes))\n+                    regexes.update(loaded)\n+    return regexes\n \n \n-def configure_regexes_from_git(git_url, repo_rules_filenames, rules_regexes):\n+def configure_regexes_from_git(git_url, repo_rules_filenames, rules_regexes):  # pylint: disable=unused-argument\n     # type: (str, List[str], PatternDict) -> PatternDict\n+    # FIXME: This was never called or tested.\n+    #  https://github.com/godaddy/tartufo/issues/17 has been added for tracking\n     rules_project_path = util.clone_git_repo(git_url)\n     try:\n-        rules_filenames = [os.path.join(rules_project_path, repo_rules_filename)\n-                           for repo_rules_filename in repo_rules_filenames]\n-        return configure_regexes_from_rules_files(rules_filenames, rules_regexes)\n+        # rules_filenames = [os.path.join(rules_project_path, repo_rules_filename)\n+        #                    for repo_rules_filename in repo_rules_filenames]\n+        return {}  # configure_regexes_from_rules_files(rules_filenames, rules_regexes)\n     finally:\n         shutil.rmtree(rules_project_path)\n \n \n-def configure_regexes_from_rules_files(rules_filenames, rules_regexes):\n-    # type: (List[str], PatternDict) -> PatternDict\n-    for rules_filename in rules_filenames:\n-        load_rules_from_file(rules_filename, rules_regexes)\n-\n-    return rules_regexes\n-\n-\n-def load_rules_from_file(rules_filename, rules_regexes):\n-    # type: (str, PatternDict) -> None\n-    # FIXME: This relies on side-effects by mutating the passed-in dictionary\n+def load_rules_from_file(rules_file):\n+    # type: (TextIO) -> Dict[str, Pattern]\n+    regexes = {}\n     try:\n-        with open(rules_filename, \"r\") as rules_file:\n-            new_rules = json.loads(rules_file.read())\n-            for rule in new_rules:\n-                if rule in rules_regexes:\n-                    raise ValueError(\"Rule '{}' has been defined multiple times\".format(rule))\n-                rules_regexes[rule] = re.compile(new_rules[rule])\n-    except (IOError, ValueError) as err:\n-        raise Exception(\"Error reading rules file '{}': {}\".format(rules_filename, err))\n+        new_rules = json.load(rules_file)\n+    except json.JSONDecodeError:\n+        raise ValueError(\"Error loading rules from file: {}\".format(rules_file.name))\n+    for rule in new_rules:\n+        regexes[rule] = re.compile(new_rules[rule])\n+    return regexes\n", "test_patch": "diff --git a/tests/data/exclude-files b/tests/data/exclude-files\nnew file mode 100644\n--- /dev/null\n+++ b/tests/data/exclude-files\n@@ -0,0 +1,4 @@\n+# This should be ignored\n+tests/\n+\\.venv/\n+.*\\.egg-info/\ndiff --git a/tests/data/include-files b/tests/data/include-files\nnew file mode 100644\n--- /dev/null\n+++ b/tests/data/include-files\n@@ -0,0 +1,3 @@\n+# This should be ignored.\n+tartufo/\n+scripts/\ndiff --git a/tests/test_cli.py b/tests/test_cli.py\n--- a/tests/test_cli.py\n+++ b/tests/test_cli.py\n@@ -1,105 +1,201 @@\n+import re\n import unittest\n \n+from click.testing import CliRunner\n from tartufo import cli\n \n+try:\n+    import pathlib\n+except ImportError:\n+    import pathlib2 as pathlib  # type: ignore\n+\n+try:\n+    from unittest import mock\n+except ImportError:\n+    import mock  # type: ignore\n+\n \n class CLITests(unittest.TestCase):\n \n-    def test_main_exits_gracefully_with_empty_argv(self):\n-        return_val = cli.main([])\n-        self.assertEqual(return_val, 1)\n-\n-    def test_parse_args_git_rules_repo(self):\n-        argv = [\"--git-rules-repo\", \"git@github.test:test-owner/tartufo-test.git\"]\n-        expected_git_rules_repo = \"git@github.test:test-owner/tartufo-test.git\"\n-        args = cli.parse_args(argv)\n-        self.assertEqual(expected_git_rules_repo, args.git_rules_repo)\n-\n-    def test_parse_args_git_rules_not_specified(self):\n-        argv = []\n-        args = cli.parse_args(argv)\n-        self.assertEqual(0, len(args.git_rules_filenames), \"args.git_rules_filenames should be empty\")\n-\n-    def test_parse_args_git_rules(self):\n-        argv = [\"--git-rules\", \"file1\", \"file2\"]\n-        expected_rules_filenames = [\"file1\", \"file2\"]\n-        args = cli.parse_args(argv)\n-        self.assertEqual(\n-            expected_rules_filenames, args.git_rules_filenames,\n-            \"args.git_rules_filenames should be {}, is actually {}\".format(\n-                expected_rules_filenames, args.rules_filenames\n+    def test_command_exits_gracefully_with_empty_argv(self):\n+        runner = CliRunner()\n+        with runner.isolated_filesystem():\n+            result = runner.invoke(cli.main)\n+            self.assertEqual(result.exit_code, 1)\n+\n+    def test_command_fails_when_no_entropy_or_regex(self):\n+        runner = CliRunner()\n+        with runner.isolated_filesystem():\n+            result = runner.invoke(cli.main, [\"--no-entropy\", \"--no-regex\"])\n+            self.assertEqual(result.output, \"No analysis requested.\\n\")\n+\n+    def test_command_fails_when_regex_requested_but_none_available(self):\n+        runner = CliRunner()\n+        with runner.isolated_filesystem():\n+            result = runner.invoke(\n+                cli.main, [\"--regex\", \"--no-default-regexes\", \"--repo-path\", \".\"]\n+            )\n+            self.assertEqual(\n+                result.output, \"Regex checks requested, but no regexes found.\\n\"\n+            )\n+\n+    @mock.patch(\"tartufo.cli.config.configure_regexes_from_args\")\n+    def test_command_fails_from_invalid_regex(self, mock_config_regex):\n+        mock_config_regex.side_effect = ValueError(\"Foo!\")\n+        runner = CliRunner()\n+        with runner.isolated_filesystem():\n+            result = runner.invoke(\n+                cli.main, [\"--repo-path\", \".\"]\n+            )\n+            self.assertEqual(result.output, \"Foo!\\n\")\n+\n+    @mock.patch(\"tartufo.cli.scanner.find_staged\")\n+    def test_command_calls_find_staged_for_pre_commit(\n+            self, mock_find_staged\n+    ):\n+        mock_find_staged.return_value = {}\n+        runner = CliRunner()\n+        with runner.isolated_filesystem():\n+            runner.invoke(\n+                cli.main, [\"--pre-commit\", \"--repo-path\", \"/\", \"--no-regex\", \"--entropy\"]\n+            )\n+            mock_find_staged.assert_called_once_with(\n+                \"/\",\n+                False,\n+                False,\n+                True,\n+                custom_regexes={},\n+                suppress_output=False,\n+                path_inclusions=[],\n+                path_exclusions=[]\n+            )\n+\n+    @mock.patch(\"tartufo.cli.scanner.find_strings\")\n+    def test_command_calls_find_strings_by_default(\n+            self, mock_find_strings\n+    ):\n+        mock_find_strings.return_value = {}\n+        runner = CliRunner()\n+        with runner.isolated_filesystem():\n+            runner.invoke(\n+                cli.main, [\"--no-regex\", \"--max-depth\", \"42\", \"--entropy\", \"git@github.com:godaddy/tartufo.git\"]\n             )\n-        )\n-\n-    def test_parse_args_git_rules_multiple_times(self):\n-        argv = [\"--git-rules\", \"file1\", \"--git-rules\", \"file2\"]\n-        expected_rules_filenames = [\"file1\", \"file2\"]\n-        args = cli.parse_args(argv)\n-        self.assertEqual(\n-            expected_rules_filenames, args.git_rules_filenames,\n-            \"args.git_rules_filenames should be {}, is actually {}\".format(\n-                expected_rules_filenames, args.rules_filenames\n+            mock_find_strings.assert_called_once_with(\n+                \"git@github.com:godaddy/tartufo.git\",\n+                None,\n+                42,\n+                False,\n+                False,\n+                True,\n+                custom_regexes={},\n+                suppress_output=False,\n+                branch=None,\n+                repo_path=None,\n+                path_inclusions=[],\n+                path_exclusions=[]\n             )\n-        )\n-\n-    def test_parse_args_rules_not_specified(self):\n-        argv = []\n-        args = cli.parse_args(argv)\n-        self.assertEqual(0, len(args.rules_filenames), \"args.rules_filenames should be empty\")\n-\n-    def test_parse_args_rules(self):\n-        argv = [\"--rules\", \"file1\", \"file2\"]\n-        expected_rules_filenames = [\"file1\", \"file2\"]\n-        args = cli.parse_args(argv)\n-        self.assertEqual(\n-            expected_rules_filenames, args.rules_filenames,\n-            \"args.rules_filenames should be {}, is actually {}\".format(\n-                expected_rules_filenames, args.rules_filenames\n+\n+    @mock.patch(\"tartufo.cli.scanner.find_strings\")\n+    @mock.patch(\"tartufo.cli.util.clean_outputs\")\n+    def test_command_calls_cleanup_when_requested(\n+            self, mock_clean, mock_find_strings\n+    ):\n+        mock_find_strings.return_value = {\"foo\": \"bar\"}\n+        runner = CliRunner()\n+        with runner.isolated_filesystem():\n+            runner.invoke(\n+                cli.main,\n+                [\"--cleanup\", \"--no-regex\", \"--max-depth\", \"42\", \"--entropy\", \"git@github.com:godaddy/tartufo.git\"]\n+            )\n+            mock_clean.assert_called_once_with({\"foo\": \"bar\"})\n+\n+    @mock.patch(\"tartufo.cli.scanner.find_strings\")\n+    def test_path_inclusions(self, mock_find_strings):\n+        mock_find_strings.return_value = {}\n+        runner = CliRunner()\n+        with runner.isolated_filesystem():\n+            include_files = pathlib.Path(__file__).parent / \"data\" / \"include-files\"\n+            runner.invoke(\n+                cli.main,\n+                [\n+                    \"-i\",\n+                    str(include_files.resolve()),\n+                    \"--no-regex\",\n+                    \"--max-depth\",\n+                    \"42\",\n+                    \"--entropy\",\n+                    \"git@github.com:godaddy/tartufo.git\"\n+                ]\n+            )\n+            mock_find_strings.assert_called_once_with(\n+                \"git@github.com:godaddy/tartufo.git\",\n+                None,\n+                42,\n+                False,\n+                False,\n+                True,\n+                custom_regexes={},\n+                suppress_output=False,\n+                branch=None,\n+                repo_path=None,\n+                path_inclusions=[re.compile(\"tartufo/\"), re.compile(\"scripts/\")],\n+                path_exclusions=[]\n+            )\n+\n+    @mock.patch(\"tartufo.cli.scanner.find_strings\")\n+    def test_path_exclusions(self, mock_find_strings):\n+        mock_find_strings.return_value = {}\n+        runner = CliRunner()\n+        with runner.isolated_filesystem():\n+            exclude_files = pathlib.Path(__file__).parent / \"data\" / \"exclude-files\"\n+            runner.invoke(\n+                cli.main,\n+                [\n+                    \"-x\",\n+                    str(exclude_files.resolve()),\n+                    \"--no-regex\",\n+                    \"--max-depth\",\n+                    \"42\",\n+                    \"--entropy\",\n+                    \"git@github.com:godaddy/tartufo.git\"\n+                ]\n+            )\n+            mock_find_strings.assert_called_once_with(\n+                \"git@github.com:godaddy/tartufo.git\",\n+                None,\n+                42,\n+                False,\n+                False,\n+                True,\n+                custom_regexes={},\n+                suppress_output=False,\n+                branch=None,\n+                repo_path=None,\n+                path_inclusions=[],\n+                path_exclusions=[re.compile(\"tests/\"), re.compile(r\"\\.venv/\"), re.compile(r\".*\\.egg-info/\")]\n+            )\n+\n+    @mock.patch(\"tartufo.cli.scanner.find_strings\")\n+    def test_issues_path_is_called_out(self, mock_find_strings):\n+        mock_find_strings.return_value = {\"issues_path\": \"/foo\"}\n+        runner = CliRunner()\n+        with runner.isolated_filesystem():\n+            result = runner.invoke(\n+                cli.main,\n+                [\"git@github.com:godaddy/tartufo.git\"]\n             )\n-        )\n-\n-    def test_parse_args_rules_multiple_times(self):\n-        argv = [\"--rules\", \"file1\", \"--rules\", \"file2\"]\n-        expected_rules_filenames = [\"file1\", \"file2\"]\n-        args = cli.parse_args(argv)\n-        self.assertEqual(\n-            expected_rules_filenames, args.rules_filenames,\n-            \"args.rules_filenames should be {}, is actually {}\".format(\n-                expected_rules_filenames, args.rules_filenames\n+            self.assertEqual(result.output, \"Results have been saved in /foo\\n\")\n+\n+    @mock.patch(\"tartufo.cli.scanner.find_strings\")\n+    def test_command_exits_with_positive_return_code_when_issues_found(self, mock_find_strings):\n+        mock_find_strings.return_value = {\"found_issues\": True}\n+        runner = CliRunner()\n+        with runner.isolated_filesystem():\n+            result = runner.invoke(\n+                cli.main,\n+                [\"git@github.com:godaddy/tartufo.git\"]\n             )\n-        )\n-\n-    def test_parse_args_rules_default_regexes_set_to_false(self):\n-        argv = [\"--default-regexes\", \"f\"]\n-        args = cli.parse_args(argv)\n-        self.assertFalse(\n-            args.do_default_regexes,\n-            \"args.do_default_regexes should be False, is actually {}\".format(args.do_default_regexes)\n-        )\n-\n-    def test_parse_args_rules_default_regexes_set_to_true(self):\n-        argv = [\"--default-regexes\", \"t\"]\n-        args = cli.parse_args(argv)\n-        self.assertTrue(\n-            args.do_default_regexes,\n-            \"args.do_default_regexes should be True, is actually {}\".format(args.do_default_regexes)\n-        )\n-\n-    def test_parse_args_rules_default_regexes_specified_with_no_value(self):\n-        argv = [\"--default-regexes\", \"--regex\"]\n-        args = cli.parse_args(argv)\n-        self.assertTrue(\n-            args.do_default_regexes,\n-            \"args.do_default_regexes should be True, is actually {}\".format(args.do_default_regexes)\n-        )\n-\n-    def test_parse_args_rules_default_regexes_unset(self):\n-        argv = []\n-        args = cli.parse_args(argv)\n-        self.assertTrue(\n-            args.do_default_regexes,\n-            \"args.do_default_regexes should be True, is actually {}\".format(args.do_default_regexes)\n-        )\n+            self.assertGreater(result.exit_code, 0)\n \n \n if __name__ == \"__main__\":\ndiff --git a/tests/test_config_regexes.py b/tests/test_config.py\nsimilarity index 61%\nrename from tests/test_config_regexes.py\nrename to tests/test_config.py\n--- a/tests/test_config_regexes.py\n+++ b/tests/test_config.py\n@@ -1,24 +1,26 @@\n from __future__ import unicode_literals\n \n-import os.path\n import re\n import unittest\n-from collections import namedtuple\n \n from truffleHogRegexes.regexChecks import regexes as default_regexes\n \n from tartufo import config\n \n+try:\n+    import pathlib\n+except ImportError:\n+    import pathlib2 as pathlib  # type: ignore\n+\n \n class ConfigureRegexTests(unittest.TestCase):\n \n     def test_configure_regexes_from_args_rules_files_without_defaults(self):\n-        rules_filenames = [os.path.join(os.path.dirname(__file__), \"data\", \"testRules.json\")]\n+        rules_path = pathlib.Path(__file__).parent / \"data\" / \"testRules.json\"\n+        rules_files = (rules_path.open(), )\n         expected_regexes = {\"RSA private key 2\": re.compile(\"-----BEGIN EC PRIVATE KEY-----\")}\n \n-        Args = namedtuple(\"Args\", (\"do_regex\", \"git_rules_repo\", \"git_rules_filenames\", \"rules_filenames\",\n-                                   \"do_default_regexes\"))\n-        args = Args(True, None, None, rules_filenames, False)\n+        args = {\"regex\": True, \"default_regexes\": False, \"rules\": rules_files}\n         actual_regexes = config.configure_regexes_from_args(args, default_regexes)\n \n         self.assertEqual(\n@@ -28,13 +30,12 @@ def test_configure_regexes_from_args_rules_files_without_defaults(self):\n         )\n \n     def test_configure_regexes_from_args_rules_files_with_defaults(self):\n-        rules_filenames = [os.path.join(os.path.dirname(__file__), \"data\", \"testRules.json\")]\n+        rules_path = pathlib.Path(__file__).parent / \"data\" / \"testRules.json\"\n+        rules_files = (rules_path.open(), )\n         expected_regexes = dict(default_regexes)\n         expected_regexes[\"RSA private key 2\"] = re.compile(\"-----BEGIN EC PRIVATE KEY-----\")\n \n-        Args = namedtuple(\"Args\", (\"do_regex\", \"git_rules_repo\", \"git_rules_filenames\", \"rules_filenames\",\n-                                   \"do_default_regexes\"))\n-        args = Args(True, None, None, rules_filenames, True)\n+        args = {\"regex\": True, \"default_regexes\": True, \"rules\": rules_files}\n         actual_regexes = config.configure_regexes_from_args(args, default_regexes)\n \n         self.assertEqual(\n@@ -44,22 +45,17 @@ def test_configure_regexes_from_args_rules_files_with_defaults(self):\n         )\n \n     def test_configure_regexes_from_args_no_do_regex(self):\n-        rules_filenames = [\"testRules.json\"]\n-\n-        Args = namedtuple(\"Args\", (\"do_regex\", \"git_rules_repo\", \"git_rules_filenames\", \"rules_filenames\",\n-                                   \"do_default_regexes\"))\n-        args = Args(False, None, None, rules_filenames, True)\n+        rules_path = pathlib.Path(__file__).parent / \"data\" / \"testRules.json\"\n+        rules_files = (rules_path.open(), )\n+        args = {\"regex\": False, \"default_regexes\": True, \"rules\": rules_files}\n         actual_regexes = config.configure_regexes_from_args(args, default_regexes)\n \n         self.assertEqual({}, actual_regexes, \"The regexes dictionary should be empty when do_regex is False\")\n \n     def test_configure_regexes_from_args_no_rules(self):\n-        rules_filenames = []\n         expected_regexes = dict(default_regexes)\n \n-        Args = namedtuple(\"Args\", (\"do_regex\", \"git_rules_repo\", \"git_rules_filenames\", \"rules_filenames\",\n-                                   \"do_default_regexes\"))\n-        args = Args(True, None, None, rules_filenames, False)\n+        args = {\"regex\": True, \"default_regexes\": True, \"rules\": ()}\n         actual_regexes = config.configure_regexes_from_args(args, default_regexes)\n \n         self.assertEqual(\n", "problem_statement": "", "hints_text": "", "created_at": "2019-11-15T18:22:32Z"}2