From c0178d3daaaa7f1f00f5ef595b25a1233d76bf2a Mon Sep 17 00:00:00 2001 From: Joel Carpenter <66389984+AldosAC@users.noreply.github.com> Date: Thu, 6 Apr 2023 08:38:57 -0700 Subject: [PATCH] feat: STFT-076: Secureli ignore - ability to suppress issues - auto prompt user (#18) [STFT-076: STFT-076: Secureli ignore - ability to suppress issues - auto prompt user](https://slalom.atlassian.net/browse/STFT-76) This PR adds a new feature which prompts users after a scan that has resulted in a linter failure if they would like to add an ignore rule for any of the detected failures. If the user agrees to add an ignore rule, then secureli iterates through each failure and asks the user if they want to add an ignore for that failure, if so, then it also asks if the user wants to ignore the failure for all files or a specific file. To test this, make any change (and stage the file) that would cause a linter failure. Some simple ones would be adding some white space at the end of a line or removing the blank line at the end of a file. Any linter failure should trigger the notification. Note: New ignore rules are detected as an available upgrade, so when you run your next scan after adding an ignore, you will want to confirm the upgrade. --- .secureli.yaml | 17 +- dist/secureli-0.1.0-py3-none-any.whl | Bin 31430 -> 0 bytes dist/secureli-0.1.0.tar.gz | Bin 28832 -> 0 bytes secureli/abstractions/pre_commit.py | 2 +- secureli/actions/scan.py | 200 +++++++++- secureli/container.py | 4 + secureli/repositories/settings.py | 170 +++++++++ secureli/services/scanner.py | 151 +++++++- secureli/settings.py | 123 +----- tests/__pycache__/__init__.cpython-39.pyc | Bin 137 -> 0 bytes .../conftest.cpython-39-pytest-7.1.3.pyc | Bin 1001 -> 0 bytes .../__pycache__/__init__.cpython-39.pyc | Bin 150 -> 0 bytes ...est_pre_commit.cpython-39-pytest-7.1.3.pyc | Bin 11526 -> 0 bytes ...e_commit_hooks.cpython-39-pytest-7.1.3.pyc | Bin 3487 -> 0 bytes ..._lexer_guesser.cpython-39-pytest-7.1.3.pyc | Bin 1565 -> 0 bytes ...est_typer_echo.cpython-39-pytest-7.1.3.pyc | Bin 3322 -> 0 bytes tests/abstractions/test_pre_commit.py | 2 +- .../__pycache__/__init__.cpython-39.pyc | Bin 145 -> 0 bytes .../conftest.cpython-39-pytest-7.1.3.pyc | Bin 1286 -> 0 bytes .../test_action.cpython-39-pytest-7.1.3.pyc | Bin 5971 -> 0 bytes ...ializer_action.cpython-39-pytest-7.1.3.pyc | Bin 1835 -> 0 bytes ...st_scan_action.cpython-39-pytest-7.1.3.pyc | Bin 2525 -> 0 bytes ...st_yeti_action.cpython-39-pytest-7.1.3.pyc | Bin 1087 -> 0 bytes tests/actions/test_scan_action.py | 349 +++++++++++++++++- .../__pycache__/__init__.cpython-39.pyc | Bin 149 -> 0 bytes ...test_container.cpython-39-pytest-7.1.3.pyc | Bin 1129 -> 0 bytes .../test_main.cpython-39-pytest-7.1.3.pyc | Bin 1963 -> 0 bytes .../test_patterns.cpython-39-pytest-7.1.3.pyc | Bin 2068 -> 0 bytes tests/conftest.py | 6 + .../__pycache__/__init__.cpython-39.pyc | Bin 150 -> 0 bytes ...les_repository.cpython-39-pytest-7.1.3.pyc | Bin 6651 -> 0 bytes ...ecureli_config.cpython-39-pytest-7.1.3.pyc | Bin 2840 -> 0 bytes .../repositories/test_settings_repository.py | 96 +++++ .../__pycache__/__init__.cpython-39.pyc | Bin 147 -> 0 bytes ..._read_resource.cpython-39-pytest-7.1.3.pyc | Bin 1715 -> 0 bytes .../__pycache__/__init__.cpython-39.pyc | Bin 146 -> 0 bytes ...est_git_ignore.cpython-39-pytest-7.1.3.pyc | Bin 5891 -> 0 bytes ...guage_analyzer.cpython-39-pytest-7.1.3.pyc | Bin 4132 -> 0 bytes ...nguage_support.cpython-39-pytest-7.1.3.pyc | Bin 2374 -> 0 bytes ...ogging_service.cpython-39-pytest-7.1.3.pyc | Bin 3034 -> 0 bytes ...canner_service.cpython-39-pytest-7.1.3.pyc | Bin 1216 -> 0 bytes ...ecureli_ignore.cpython-39-pytest-7.1.3.pyc | Bin 2165 -> 0 bytes tests/services/test_scanner_service.py | 158 +++++++- .../__pycache__/__init__.cpython-39.pyc | Bin 147 -> 0 bytes .../test_git_meta.cpython-39-pytest-7.1.3.pyc | Bin 3499 -> 0 bytes .../test_patterns.cpython-39-pytest-7.1.3.pyc | Bin 2076 -> 0 bytes ..._secureli_meta.cpython-39-pytest-7.1.3.pyc | Bin 934 -> 0 bytes 47 files changed, 1143 insertions(+), 135 deletions(-) delete mode 100644 dist/secureli-0.1.0-py3-none-any.whl delete mode 100644 dist/secureli-0.1.0.tar.gz create mode 100644 secureli/repositories/settings.py delete mode 100644 tests/__pycache__/__init__.cpython-39.pyc delete mode 100644 tests/__pycache__/conftest.cpython-39-pytest-7.1.3.pyc delete mode 100644 tests/abstractions/__pycache__/__init__.cpython-39.pyc delete mode 100644 tests/abstractions/__pycache__/test_pre_commit.cpython-39-pytest-7.1.3.pyc delete mode 100644 tests/abstractions/__pycache__/test_pre_commit_hooks.cpython-39-pytest-7.1.3.pyc delete mode 100644 tests/abstractions/__pycache__/test_pygments_lexer_guesser.cpython-39-pytest-7.1.3.pyc delete mode 100644 tests/abstractions/__pycache__/test_typer_echo.cpython-39-pytest-7.1.3.pyc delete mode 100644 tests/actions/__pycache__/__init__.cpython-39.pyc delete mode 100644 tests/actions/__pycache__/conftest.cpython-39-pytest-7.1.3.pyc delete mode 100644 tests/actions/__pycache__/test_action.cpython-39-pytest-7.1.3.pyc delete mode 100644 tests/actions/__pycache__/test_initializer_action.cpython-39-pytest-7.1.3.pyc delete mode 100644 tests/actions/__pycache__/test_scan_action.cpython-39-pytest-7.1.3.pyc delete mode 100644 tests/actions/__pycache__/test_yeti_action.cpython-39-pytest-7.1.3.pyc delete mode 100644 tests/application/__pycache__/__init__.cpython-39.pyc delete mode 100644 tests/application/__pycache__/test_container.cpython-39-pytest-7.1.3.pyc delete mode 100644 tests/application/__pycache__/test_main.cpython-39-pytest-7.1.3.pyc delete mode 100644 tests/application/__pycache__/test_patterns.cpython-39-pytest-7.1.3.pyc delete mode 100644 tests/repositories/__pycache__/__init__.cpython-39.pyc delete mode 100644 tests/repositories/__pycache__/test_repo_files_repository.cpython-39-pytest-7.1.3.pyc delete mode 100644 tests/repositories/__pycache__/test_secureli_config.cpython-39-pytest-7.1.3.pyc create mode 100644 tests/repositories/test_settings_repository.py delete mode 100644 tests/resources/__pycache__/__init__.cpython-39.pyc delete mode 100644 tests/resources/__pycache__/test_read_resource.cpython-39-pytest-7.1.3.pyc delete mode 100644 tests/services/__pycache__/__init__.cpython-39.pyc delete mode 100644 tests/services/__pycache__/test_git_ignore.cpython-39-pytest-7.1.3.pyc delete mode 100644 tests/services/__pycache__/test_language_analyzer.cpython-39-pytest-7.1.3.pyc delete mode 100644 tests/services/__pycache__/test_language_support.cpython-39-pytest-7.1.3.pyc delete mode 100644 tests/services/__pycache__/test_logging_service.cpython-39-pytest-7.1.3.pyc delete mode 100644 tests/services/__pycache__/test_scanner_service.cpython-39-pytest-7.1.3.pyc delete mode 100644 tests/services/__pycache__/test_secureli_ignore.cpython-39-pytest-7.1.3.pyc delete mode 100644 tests/utilities/__pycache__/__init__.cpython-39.pyc delete mode 100644 tests/utilities/__pycache__/test_git_meta.cpython-39-pytest-7.1.3.pyc delete mode 100644 tests/utilities/__pycache__/test_patterns.cpython-39-pytest-7.1.3.pyc delete mode 100644 tests/utilities/__pycache__/test_secureli_meta.cpython-39-pytest-7.1.3.pyc diff --git a/.secureli.yaml b/.secureli.yaml index 2013e58a..41456942 100644 --- a/.secureli.yaml +++ b/.secureli.yaml @@ -1,12 +1,11 @@ +echo: + level: ERROR repo_files: - max_file_size: 1000000 exclude_file_patterns: - - .idea/ + - .idea/ ignored_file_extensions: - - .pyc - - .drawio - - .png - - .jpg - -echo: - level: ERROR + - .pyc + - .drawio + - .png + - .jpg + max_file_size: 1000000 diff --git a/dist/secureli-0.1.0-py3-none-any.whl b/dist/secureli-0.1.0-py3-none-any.whl deleted file mode 100644 index f1e719a2d7cfd103288d65b19b0e0afc44199854..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 31430 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 diff --git a/dist/secureli-0.1.0.tar.gz b/dist/secureli-0.1.0.tar.gz deleted file mode 100644 index 7cda930f503af7c7595d1a84fc33e219b24f275b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28832 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 diff --git a/secureli/abstractions/pre_commit.py b/secureli/abstractions/pre_commit.py index cb504406..13d761bf 100644 --- a/secureli/abstractions/pre_commit.py +++ b/secureli/abstractions/pre_commit.py @@ -8,7 +8,7 @@ import pydantic import yaml -from secureli.settings import PreCommitSettings, PreCommitRepo +from secureli.repositories.settings import PreCommitSettings, PreCommitRepo from secureli.utilities.patterns import combine_patterns from secureli.resources.slugify import slugify diff --git a/secureli/actions/scan.py b/secureli/actions/scan.py index 015acf07..8a3bb366 100644 --- a/secureli/actions/scan.py +++ b/secureli/actions/scan.py @@ -3,8 +3,20 @@ from secureli.abstractions.echo import EchoAbstraction from secureli.services.logging import LoggingService, LogAction -from secureli.services.scanner import ScanMode, ScannerService +from secureli.services.scanner import ( + ScanMode, + ScannerService, + Failure, + OutputParseErrors, +) from secureli.actions.action import VerifyOutcome, Action, ActionDependencies +from secureli.repositories.settings import ( + SecureliRepository, + SecureliFile, + PreCommitSettings, + PreCommitRepo, + PreCommitHook, +) class ScanAction(Action): @@ -22,11 +34,13 @@ def __init__( echo: EchoAbstraction, logging: LoggingService, scanner: ScannerService, + settings_repository: SecureliRepository, ): super().__init__(action_deps) self.scanner = scanner self.echo = echo self.logging = logging + self.settings = settings_repository def scan_repo( self, @@ -51,11 +65,193 @@ def scan_repo( return scan_result = self.scanner.scan_repo(scan_mode, specific_test) + details = scan_result.output or "Unknown output during scan" self.echo.print(details) + + failure_count = len(scan_result.failures) + if failure_count > 0: + self._process_failures(scan_result.failures, always_yes=always_yes) + if not scan_result.successful: - self.echo.print(details) self.logging.failure(LogAction.scan, details) else: self.echo.print("Scan executed successfully and detected no issues!") self.logging.success(LogAction.scan) + + def _process_failures( + self, + failures: list[Failure], + always_yes: bool, + ): + """ + Processes any failures found during the scan. + :param failures: List of Failure objects representing linter failures + :param always_yes: Assume "Yes" to all prompts + """ + settings = self.settings.load() + + ignore_fail_prompt = "Failures detected during scan.\n" + ignore_fail_prompt += "Add an ignore rule?" + + # Ask if the user wants to ignore a failure + if always_yes: + always_yes_warning = "Hook failures were detected but the scan was initiated with the 'yes' flag.\n" + always_yes_warning += "SeCureLI cannot automatically add ignore rules with the 'yes' flag enabled.\n" + always_yes_warning += "Re-run your scan without the 'yes' flag to add an ignore rule for one of the\n" + always_yes_warning += "detected failures." + + self.echo.print(always_yes_warning) + elif self.echo.confirm(ignore_fail_prompt, default_response=False): + # verify pre_commit exists in settings file. + if not settings.pre_commit: + settings.pre_commit = PreCommitSettings() + + for failure in failures: + add_ignore_for_id = self.echo.confirm( + "\nWould you like to add an ignore for the {} failure on {}?".format( + failure.id, failure.file + ) + ) + if failure.repo == OutputParseErrors.REPO_NOT_FOUND: + self._handle_repo_not_found(failure) + elif always_yes or add_ignore_for_id: + settings = self._add_ignore_for_failure( + failure=failure, always_yes=always_yes, settings_file=settings + ) + + self.settings.save(settings=settings) + + def _add_ignore_for_failure( + self, + failure: Failure, + always_yes: bool, + settings_file: SecureliFile, + ): + """ + Processes an individual failure and adds an ignore rule for either the entire + hook or a particular file. + :param failure: Failure object representing a rule failure during a scan + :param always_yes: Assume "Yes" to all prompts + :param settings_file: SecureliFile representing the contents of the .secureli.yaml file + """ + ignore_repo_prompt = "You can add an ignore rule for just this file, or you can add an ignore rule for all files.\n" + ignore_repo_prompt += ( + "Would you like to ignore this failure for all files?".format(failure.id) + ) + ignore_file_prompt = ( + "\nWould you like to ignore this failure for just the {} file?".format( + failure.file + ) + ) + + self.echo.print("\nAdding an ignore rule for: {}\n".format(failure.id)) + + if always_yes or self.echo.confirm( + message=ignore_repo_prompt, default_response=False + ): + # ignore for all files + self.echo.print("Adding an ignore for all files.") + modified_settings = self._ignore_all_files( + failure=failure, settings_file=settings_file + ) + else: + if always_yes or self.echo.confirm(ignore_file_prompt, False): + self.echo.print("Adding an ignore for {}".format(failure.file)) + modified_settings = self._ignore_one_file( + failure=failure, settings_file=settings_file + ) + else: + self.echo.print( + "\nSkipping {} failure on {}".format(failure.id, failure.file) + ) + modified_settings = settings_file + + return modified_settings + + def _handle_repo_not_found(self, failure: Failure): + """ + Handles a REPO_NOT_FOUND error + :param failure: A Failure object representing the scan failure with a missing repo url + """ + id = failure.id + self.echo.print( + "Unable to add an ignore for {}, SeCureLI was unable to identify the repo it belongs to.".format( + failure.id + ) + ) + self.echo.print("Skipping {}".format(id)) + + def _ignore_all_files(self, failure: Failure, settings_file: SecureliFile): + """ + Supresses a hook for all files in this repo + :param failure: Failure object representing the failed hook + :param settings_file: SecureliFile representing the contents of the .secureli.yaml file + :return: Returns the settings file after modifications + """ + pre_commit_settings = settings_file.pre_commit + repos = pre_commit_settings.repos + repo_settings_index = next( + (index for (index, repo) in enumerate(repos) if repo.url == failure.repo), + None, + ) + + if repo_settings_index is not None: + repo_settings = pre_commit_settings.repos[repo_settings_index] + if failure.id not in repo_settings.suppressed_hook_ids: + repo_settings.suppressed_hook_ids.append(failure.id) + else: + repo_settings = PreCommitRepo( + url=failure.repo, suppressed_hook_ids=[failure.id] + ) + repos.append(repo_settings) + + self.echo.print( + "Added {} to the suppressed_hooks_ids list for the {} repo".format( + failure.id, failure.repo + ) + ) + + return settings_file + + def _ignore_one_file(self, failure: Failure, settings_file: SecureliFile): + """ + Adds the failed file to the file exemptions list for the failed hook + :param failure: Failure object representing the failed hook + :param settings_file: SecureliFile representing the contents of the .secureli.yaml file + """ + pre_commit_settings = settings_file.pre_commit + repos = pre_commit_settings.repos + repo_settings_index = next( + (index for (index, repo) in enumerate(repos) if repo.url == failure.repo), + None, + ) + + if repo_settings_index is not None: + repo_settings = pre_commit_settings.repos[repo_settings_index] + else: + repo_settings = PreCommitRepo(url=failure.repo) + repos.append(repo_settings) + + hooks = repo_settings.hooks + hook_settings_index = next( + (index for (index, hook) in enumerate(hooks) if hook.id == failure.id), + None, + ) + + if hook_settings_index is not None: + hook_settings = hooks[hook_settings_index] + if failure.file not in hook_settings.exclude_file_patterns: + hook_settings.exclude_file_patterns.append(failure.file) + else: + self.echo.print( + "An ignore rule is already present for the {} file".format( + failure.file + ) + ) + else: + hook_settings = PreCommitHook(id=failure.id) + hook_settings.exclude_file_patterns.append(failure.file) + repo_settings.hooks.append(hook_settings) + + return settings_file diff --git a/secureli/container.py b/secureli/container.py index 9950c6f1..ef3ad797 100644 --- a/secureli/container.py +++ b/secureli/container.py @@ -10,6 +10,7 @@ from secureli.actions.update import UpdateAction from secureli.repositories.repo_files import RepoFilesRepository from secureli.repositories.secureli_config import SecureliConfigRepository +from secureli.repositories.settings import SecureliRepository from secureli.resources import read_resource from secureli.services.git_ignore import GitIgnoreService from secureli.services.language_analyzer import LanguageAnalyzerService @@ -63,6 +64,8 @@ class Container(containers.DeclarativeContainer): """ secureli_config_repository = providers.Factory(SecureliConfigRepository) + settings_repository = providers.Factory(SecureliRepository) + # Abstractions """The echo service, used to stylistically render text to the terminal""" @@ -161,6 +164,7 @@ class Container(containers.DeclarativeContainer): echo=echo, logging=logging_service, scanner=scanner_service, + settings_repository=settings_repository, ) """Update Action, representing what happens when the update command is invoked""" diff --git a/secureli/repositories/settings.py b/secureli/repositories/settings.py new file mode 100644 index 00000000..17b41870 --- /dev/null +++ b/secureli/repositories/settings.py @@ -0,0 +1,170 @@ +from pathlib import Path +from typing import Optional, Literal +from enum import Enum +import yaml + +from pydantic import BaseModel, BaseSettings, Field + + +default_ignored_extensions = [ + # Images + ".png", + ".jpg", + ".jpeg", + ".gif", + ".bmp", + ".tiff", + "psd", + # Videos + ".mp4", + ".mkv", + ".avi", + ".mov", + ".mpg", + ".vob", + # Audio + ".mp3", + ".aac", + ".wav", + ".flac", + ".ogg", + ".mka", + ".wma", + # Documents + ".pdf", + ".doc", + ".xls", + ".ppt", + ".docx", + ".odt", + ".drawio", + # Archives + ".zip", + ".rar", + ".7z", + ".tar", + ".iso", + # Databases + ".mdb", + ".accde", + ".frm", + ".sqlite", + # Executable + ".exe", + ".dll", + ".so", + ".class", + # Other + ".pyc", +] + + +class RepoFilesSettings(BaseSettings): + """ + Settings that adjust how SeCureLI evaluates the consuming repository. + """ + + max_file_size: int = Field(default=100000) + ignored_file_extensions: list[str] = Field(default=default_ignored_extensions) + exclude_file_patterns: list[str] = Field(default=[]) + + +class EchoLevel(str, Enum): + debug = "DEBUG" + info = "INFO" + warn = "WARN" + error = "ERROR" + + +class EchoSettings(BaseSettings): + """ + Settings that affect how SeCureLI provides information to the user. + """ + + level: EchoLevel = Field(default=EchoLevel.error) + + +class LanguageSupportSettings(BaseSettings): + """ + Settings that affect how SeCureLI performs language analysis and support. + """ + + command_timeout_seconds: int = Field(default=300) + + +class PreCommitHook(BaseSettings): + """ + Hook settings for pre-commit. + """ + + id: str + arguments: Optional[list[str]] = Field(default=[]) + additional_args: Optional[list[str]] = Field(default=[]) + exclude_file_patterns: Optional[list[str]] = Field(default=[]) + + +class PreCommitRepo(BaseSettings): + """ + Repo settings for pre-commit. + """ + + url: str + hooks: list[PreCommitHook] = Field(default=[]) + suppressed_hook_ids: list[str] = Field(default=[]) + + +class PreCommitSettings(BaseSettings): + """ + Various adjustments that affect how SeCureLI configures the pre-commit system. + """ + + repos: list[PreCommitRepo] = Field(default=[]) + suppressed_repos: list[str] = Field(default=[]) + + +class SecureliFile(BaseModel): + """ + Represents the contents of the .secureli file + """ + + repo_files: Optional[RepoFilesSettings] + echo: Optional[EchoSettings] + language_support: Optional[LanguageSupportSettings] = Field(default=None) + pre_commit: Optional[PreCommitSettings] = Field(default=None) + + +class SecureliRepository: + """ + Represents the .secureli.yaml file in the root directory. Saves and loads data from the file. + """ + + def __init__(self): + self.secureli_file_path = Path(".") / ".secureli.yaml" + + def save(self, settings: SecureliFile): + """ + Saves changes to the settings file + :param settings: The populated settings file to save + """ + # Removes empty keys to prevent type errors + settings_dict = { + key: value for (key, value) in settings.dict().items() if value is not None + } + + # Converts EchoLevel to string + settings_dict["echo"]["level"] = "{}".format(settings_dict["echo"]["level"]) + + with open(self.secureli_file_path, "w") as f: + yaml.dump(settings_dict, f) + + def load(self) -> SecureliFile: + """ + Reads the contents of the .secureli.yaml file and returns it + :return: SecureliFile containing the contents of the settings file + """ + if not self.secureli_file_path.exists(): + return SecureliFile() + + with open(self.secureli_file_path, "r") as f: + data = yaml.safe_load(f) + return SecureliFile.parse_obj(data) diff --git a/secureli/services/scanner.py b/secureli/services/scanner.py index 7ae64d8e..f47437a3 100644 --- a/secureli/services/scanner.py +++ b/secureli/services/scanner.py @@ -1,7 +1,10 @@ from enum import Enum from typing import Optional +from pathlib import Path import pydantic +import re +import yaml from secureli.abstractions.pre_commit import PreCommitAbstraction @@ -15,6 +18,24 @@ class ScanMode(str, Enum): ALL_FILES = "all-files" +class OutputParseErrors(str, Enum): + """ + Possible errors when parsing scan output + """ + + REPO_NOT_FOUND = "repo-not-found" + + +class Failure(pydantic.BaseModel): + """ + Represents the details of a failed rule from a scan + """ + + repo: str + id: str + file: str + + class ScanResult(pydantic.BaseModel): """ The results of calling scan_repo @@ -22,6 +43,15 @@ class ScanResult(pydantic.BaseModel): successful: bool output: Optional[str] = None + failures: list[Failure] + + +class ScanOuput(pydantic.BaseModel): + """ + Represents the parsed output from a scan + """ + + failures: list[Failure] class ScannerService: @@ -45,6 +75,125 @@ def scan_repo( """ all_files = True if scan_mode == ScanMode.ALL_FILES else False execute_result = self.pre_commit.execute_hooks(all_files, hook_id=specific_test) + parsed_output = self._parse_scan_ouput(output=execute_result.output) + return ScanResult( - successful=execute_result.successful, output=execute_result.output + successful=execute_result.successful, + output=execute_result.output, + failures=parsed_output.failures, ) + + def _parse_scan_ouput(self, output: str = "") -> ScanOuput: + """ + Parses the output from a scan and returns a list of Failure objects representing any + hook rule failures during a scan. + :param output: Raw output from a scan. + :return: ScanOuput object representing a list of hook rule Failure objects. + """ + failures = [] + failure_indexes = [] + config_data = self._get_config() + + # Split the output up by each line and record the index of each failure + output_by_line = output.split("\n") + for index, line in enumerate(output_by_line): + if line.find("Failed") != -1: + failure_indexes.append(index) + + # Process each failure + for failure_index in failure_indexes: + # Remove ANSI encoding and record hook id + id_with_encoding = output_by_line[failure_index + 1].split(": ")[1] + id = self._remove_ansi_from_string(id_with_encoding) + + # Retrieve repo url for failure + repo = self._find_repo_from_id(hook_id=id, config=config_data) + + # Capture all output lines for this failure + failure_output_list = self._get_single_failure_output( + failure_start=failure_index, output_by_line=output_by_line + ) + # Capture files that failed + files = self._find_file_names(failure_output_list=failure_output_list) + + for file in files: + failures.append(Failure(id=id, file=file, repo=repo)) + + print("Failures: {}".format(failures)) + + return ScanOuput(failures=failures) + + def _get_single_failure_output( + self, failure_start: int, output_by_line: list[str] + ) -> list[str]: + failure_lines = [] + + for index in range(failure_start + 1, len(output_by_line)): + line = output_by_line[index] + + if line.find(".....") == -1: # Look for line break + failure_lines.append(line) + else: + break + + return failure_lines + + def _find_file_names(self, failure_output_list: list[str]) -> str: + """ + Finds the file name for a hook rule failure + :param failure_index: The index of the initial failure in failure_output_list + :param output_by_line: List containing the scan output delimited by newlines + :return: Returns the file name that caused the failure. + """ + regexp = re.compile(r"^(?!http:|https)[a-z0-9-_/]+\.+[a-z][^:\s]*") + file_names = [] + for line in failure_output_list: + words = line.split() + for word in words: + file_name = regexp.match(word) + + if file_name: + clean_file_name = self._remove_ansi_from_string(file_name.group(0)) + file_names.append(clean_file_name) + + return file_names + + def _remove_ansi_from_string(self, string: str) -> str: + """ + Removes ANSI encoding from a string. + :param string: A string that needs to be processed + :return: A string that has had its ANSI encoding removed + """ + ansi_regexp = r"(\x9B|\x1B\[)[0-?]*[ -/]*[@-~]" + clean_string = re.sub(ansi_regexp, "", string) + + return clean_string + + def _find_repo_from_id(self, hook_id: str, config: dict): + """ + Retrieves the repo URL that a hook ID belongs to and returns it + :param linter_id: The hook id we want to retrieve the repo url for + :config: A dict containing the contents of the .pre-commit-config.yaml file + :return: The repo url our hook id belongs to + """ + repos = config.get("repos") + + for repo in repos: + hooks = repo["hooks"] + repo = repo["repo"] + + for hook in hooks: + if hook["id"] == hook_id: + return repo + + return OutputParseErrors.REPO_NOT_FOUND + + def _get_config(self): + """ + Gets the contents of the .pre-commit-config file and returns it as a dict + :return: Dict containing the contents of the .pre-commit-config.yaml file + """ + path_to_config = Path(".pre-commit-config.yaml") + with open(path_to_config, "r") as f: + data = yaml.safe_load(f) + return data diff --git a/secureli/settings.py b/secureli/settings.py index f39115aa..d9f7a85b 100644 --- a/secureli/settings.py +++ b/secureli/settings.py @@ -1,126 +1,15 @@ -from enum import Enum from pathlib import Path from typing import Any, Optional import pydantic import yaml -from pydantic import Field - -default_ignored_extensions = [ - # Images - ".png", - ".jpg", - ".jpeg", - ".gif", - ".bmp", - ".tiff", - "psd", - # Videos - ".mp4", - ".mkv", - ".avi", - ".mov", - ".mpg", - ".vob", - # Audio - ".mp3", - ".aac", - ".wav", - ".flac", - ".ogg", - ".mka", - ".wma", - # Documents - ".pdf", - ".doc", - ".xls", - ".ppt", - ".docx", - ".odt", - ".drawio", - # Archives - ".zip", - ".rar", - ".7z", - ".tar", - ".iso", - # Databases - ".mdb", - ".accde", - ".frm", - ".sqlite", - # Executable - ".exe", - ".dll", - ".so", - ".class", - # Other - ".pyc", -] - - -class RepoFilesSettings(pydantic.BaseSettings): - """ - Settings that adjust how SeCureLI evaluates the consuming repository. - """ - - max_file_size: int = Field(default=100000) - ignored_file_extensions: list[str] = Field(default=default_ignored_extensions) - exclude_file_patterns: list[str] = Field(default=[]) - - -class EchoLevel(str, Enum): - debug = "DEBUG" - info = "INFO" - warn = "WARN" - error = "ERROR" - - -class EchoSettings(pydantic.BaseSettings): - """ - Settings that affect how SeCureLI provides information to the user. - """ - - level: EchoLevel = Field(default=EchoLevel.error) - - -class LanguageSupportSettings(pydantic.BaseSettings): - """ - Settings that affect how SeCureLI performs language analysis and support. - """ - - command_timeout_seconds: int = Field(default=300) - - -class PreCommitHook(pydantic.BaseSettings): - """ - Hook settings for pre-commit. - """ - - id: str - arguments: Optional[list[str]] = Field(default=None) - additional_args: Optional[list[str]] = Field(default=None) - exclude_file_patterns: Optional[list[str]] = Field(default=None) - - -class PreCommitRepo(pydantic.BaseSettings): - """ - Repo settings for pre-commit. - """ - - url: str - hooks: list[PreCommitHook] = Field(default=[]) - suppressed_hook_ids: list[str] = Field(default=[]) - - -class PreCommitSettings(pydantic.BaseSettings): - """ - Various adjustments that affect how SeCureLI configures the pre-commit system. - """ - - repos: list[PreCommitRepo] = Field(default=[]) - suppressed_repos: list[str] = Field(default=[]) +from secureli.repositories.settings import ( + RepoFilesSettings, + EchoSettings, + LanguageSupportSettings, + PreCommitSettings, +) def secureli_yaml_settings( diff --git a/tests/__pycache__/__init__.cpython-39.pyc b/tests/__pycache__/__init__.cpython-39.pyc deleted file mode 100644 index 9a6eb64dafe010099673d273e3b890786144e4a6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 137 zcmYe~<>g`kf*r4-l0o!i5P=LBfgA@QE@lA|DGb33nv8xc8Hzx{2;!HXerR!OQL%n; zVsUatqFz#BNh*+&np|3xnvlIYq;;_lhPbtkwwF8;+8HgDG Do`oMW diff --git a/tests/__pycache__/conftest.cpython-39-pytest-7.1.3.pyc b/tests/__pycache__/conftest.cpython-39-pytest-7.1.3.pyc deleted file mode 100644 index a4912358558b4a891c1225d000bdb14ef4a9cd24..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1001 zcmZWn&2H2%5VpP9B%4jT1%brRg%eUP*%v^l5`tUzz|SR@D7C$9X#Qk7utnQbxxy3l z&>s6rzV^f`aDf?bw-m56W6$Rq&o?tJlO!UrmMg`t86nqb++7|Rdm#EdC_x0RNlx9) znh_(EG+sWYc*APH3G%?5X&vSvV554{M0w;eubwt>9#bO2i{z5XK#b3+@I~;|JEQr| zUuY;M4xQbg5qL-EOiV@W@LfUQku>=MoXFye+R1)-Sn>UiAE(qNxJ&h>TA4vf1YWNs zAB=n}^-voCd?n$ktg2Gb8F_0$N0)fiNs$W7wnsOyfkjW=nFPh~wIuUP{`i(biscTH27E25?l z_A{G*&{FBSE;TuL^$(R1FlOFU)$iNXoSAli+R`IJ%bS2S|?e>GyA zu10j;)aYuW3kCBhKu;{@^v2EIVDg`kf*r4-l0o!i5P=LBfgA@QE@lA|DGb33nv8xc8Hzx{2;!HuerR!OQL%n; zVsUatqFz#BNh*+&np|3xnv&M4u=4F<|$LkeT-r}&y Q%}*)KNwotR`Wc8B0EAp5djJ3c diff --git a/tests/abstractions/__pycache__/test_pre_commit.cpython-39-pytest-7.1.3.pyc b/tests/abstractions/__pycache__/test_pre_commit.cpython-39-pytest-7.1.3.pyc deleted file mode 100644 index 84e32851e00260c39d1139e9b4258979d538660d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11526 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 diff --git a/tests/abstractions/__pycache__/test_pygments_lexer_guesser.cpython-39-pytest-7.1.3.pyc b/tests/abstractions/__pycache__/test_pygments_lexer_guesser.cpython-39-pytest-7.1.3.pyc deleted file mode 100644 index 04f61b5c9078fb7379bf54108a3563853b124fb7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1565 zcmZuxJC7ST5GJ`VJyyC`aJZy0kO#7jl*Dl=U*I5!-Ppn*L7HtA)Gj5hy$^w;?7PFN z$+^f6Na^BK`XA)CuuYZx1Q$VOxYAh(LV?_Qi8J%f4EbQtBd~t|D^8y{g#3Zb{&InN z1Y7+EM@BMQkeJ#pD;-8CDc#tks9}X)2613%r|6X3xNB)z^x_`q-J)NHacE^;F(`-e zkP^|mJ=hQtWd0kP1zG2{dqd;Htuz$Ftow##y{vELM`8$;AxH+<@UhuLL1N;j4IKR{@DM0YS#nrqNl~zy1rI1fnLMb6Z?n?m{ zRf@OmF9yu_uvG^R4G7$zS2npmT!Oo?uE(>y5F>r5C7Qk^5)JL~ZZiHEE>~m4Rl4A# zDc1rRkwPRzJ_bgsF`p_ed8+feQdX2Kw*e*vMwrao5J$@^Go06TmY}}`onz?~IwedZ z9WmpVHeDG2n^IJ57ZjJ^wyAsqL_=;NnGIc2O>fAWWvpQd?1nyf5?6aS1k!XS*9W_B zMm8K}f**PIlvne}Hy%WhE+jUIaVuU5b7rI6Mn0>h4c2PH7T@G0FIGbO@R!JuefY!! zjQK9>xMx}G>^+pSi#|?wvHTmF1JEhk$E-(v_;si}LI=!#EHvGAZFd?Xu=_)UDUMb@jHCQNfmZ(L&Dl%)D%!_);i^P_!XT3v1{~%>8%}D}{P;#A!%VohUj;$UA?@J?3AVt$hhi%+n?K5AtOnb%G`^-20 zZI6dGAH4uq|KBLvSyJ2fd6nzMo#hz>p|F#Q8#0L#5}laN4gvWQ2+W6IjXp+goRG&z zidQn%!gTLMCfLd(UTFfH)FPisoIW#HRk^lTV3)U(9~++~#g F{SOunwKxC( diff --git a/tests/abstractions/__pycache__/test_typer_echo.cpython-39-pytest-7.1.3.pyc b/tests/abstractions/__pycache__/test_typer_echo.cpython-39-pytest-7.1.3.pyc deleted file mode 100644 index 895ef459a94eb6111dcc9065405e629714cb57d8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3322 zcmbtWOLN;c5GFuM)Z4P+N8-Gkca)~`BRj2#(Mo8`>>9%(Ze#CPBWH2iyz_VX;+I zm=%=5a;qFxS`}jO{Ncg}gI8JRfUqpH-&wzr*5VlDMQ*d)fywf$phjvlBSltHBWFe< zxT1VOS%obqd})N++~#Fg1zn4*rbd=$M$WJ$HL}9XybAMewtQf*6}I}$YONl@&a!iW zolUVdn0KCC0PI|f;o6Jr5@2gbu*+;6u=7W-E9@#@7g7xMU1Qe)yQndC1DM}rw*a}s z$Sb3M`)~A6vrc9HrMvBWFC*_~X+CQHR41~G0vFHxy`(QVPTAb=aq-maL^AUv3L*iM zvVy~C*F!ba54hNP0&i?Vqqd^Wx1|?62%@{|n;kCr&=5Hon>7DE-gq5z5pTq9>~-A6 zmYZ-8xCgrj{sst1yy0%eiEzEdkGipfoCHZZ99T7a`?3-OX(c%c-%IjHfkIA8v5 zktJ;o=Zzcm`A}~ril9bT6){a#Et(u$AWKn&rUGxAg~p%-k|hc|Ej>n4dS>%&S<~~P zZrc~(rYM0Sg()65`vS}vd#;AdOl1v${aLr63y-i!z-f%W#~3%8GV}e@$FH9c ziOdB24G)GyU@{ugDV4{}qIs)8x&l)*lZ-eE{UZ)JVPo^hXU_zNkyycI6`Nx_t>~D* zGdn@Fz3q3mov7We!_@JvYtSEBYI7C76WShLuknNZ)ClV!peqIty96TlkbHm?J1`Ha zMws!+7?Nh)l$PIZN3!O|aQPC)bAy00C+d1!uFhi4uAg+oc~E}*95?WSI!VWcCcTml zhPwZj$4fK=03b1AUW`bJS)IBqQJBJD_;vhZ+_4L7MSH zns4sF=v34ghh&s_jB&<+9Ash+nnb;&`F{yZ1+Ev@u{nLj$NgSUKwg=ompG01KB$WE z-ay0?_)+ly`cRTFh3k8ZbC!(;I#7-X) zuG5YL*j!a<7r$zL1vT2{8Q}aiNcGDyx7GK9#P7zk^su+@q|!O3w~t#ibl!mF4Z(MX zpKzH^A!(1X0xVXHTp`Q-uAiv2Sa=22F@a~7Sv^Mg`kf*r4-l0o!i5P=LBfgA@QE@lA|DGb33nv8xc8Hzx{2;!HSerR!OQL%n; zVsUatqFz#BNh*+&np|3xnvlIYq;;_lhPbtkw LwF8;=8HgDGz3C!a diff --git a/tests/actions/__pycache__/conftest.cpython-39-pytest-7.1.3.pyc b/tests/actions/__pycache__/conftest.cpython-39-pytest-7.1.3.pyc deleted file mode 100644 index 830a46e7d0b8200fe3f314e1e14790b9b4fe6a24..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1286 zcma)5L2uJA6t>+oNxNi}X-vQgfrQiptpJGwLTtd5^{|PHFOe6!S(uwdb~4s!oW_-Z zumf=0zu}xK7x)Vx0nctzRuBkF&#}Mf=e+NI&nyhP2*&tG#Sa~XKAFwM0T|i>(;r~s zh+~N;wqG)75romip>1qDM7k4?dUlPaPkq2zvNP$@uEiX=G6`sak?`Mz?~(9$`wVlJ zdvBamOjnmspLZ;JX$kG}6^r(k(13>)z0C10>aTu*JkjVJ*PU&ar15qhAM|k@KFOGz z9*Z5JD_H{2d(N_?Vu{!mCF87QfVoPPmC9msLo+Zej6DO>rc@<5MX%8b+C}GhqzE7o z*_ps^A`$QB=SXz``}$z`LJOsbn(26-4fa?mU?bv637HOIQ|cj$%QVmQFwV2FK?cQC z`4Aj{X;XMaTx^m5O-XqW)FicTlahIoq*)R{Hiu~}Y<9EBuu&^8t%3depF#fkpUK9S z;07#aB`7h+obc8eNf3A5k`wfjL@iU8etVR_F&LjW=ZlYCXYQbg7BvE^8reqz=)QS$7tORJVF{9gDH$SF+C%g)m zTm{oTm`s5QA((u39P+K}E93n;`Rm|aCci}68Bv$*#dq)BKMt+bX*s>KpXUdCQhSHc z4WKsdHJ-?LqLer6+Hg4ySy9Moq^rWzLWO_s$NNWIX;C`xbs<77kC6sPva=@C3oVV{KUT&tHQctK`c7Sr6d d|7eo)N{Ywk1?vr%ZWEsbIB;FuYtJoo@lS~;HO~M5 diff --git a/tests/actions/__pycache__/test_action.cpython-39-pytest-7.1.3.pyc b/tests/actions/__pycache__/test_action.cpython-39-pytest-7.1.3.pyc deleted file mode 100644 index b708f7db946a9500ccd1966dc8f9874f71620c2c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5971 zcmb7ITaVku6(%W4l(gEl*6V8$C-g0`np(cZiS4#d?4&V})ZV1A-HUo*xSY{0*Aywt zP`0(zzSKd{ho%pGE8vISN27nCKcOi4OrS4qfi{1^N#lNJcv(qa1ug{*XU=eL-#K&6 zH{;IF*EKxjx|0rAw85!c`f@bvH3?8w!rqN`onyQiFna;ABV+AeWG_MTGD+BxM+SSD zy@K*9MJpSvj;z;-CuAqttB@U=kiEuEL3VsX)?(j+>;%(qX|314AU!R& z^u+wwjn^LQN$sY)>U%dM?=FOUKL};)2EldL4>)^Yh)6)XaEp5b!2|!jDD3*HEhCw~ z7P`T3gMY&1Ac&K>TX+3_pR?;|g)S@Cyx5OIv~1pR!_|Si%5U=6Wp3|qg?r^6^d z-E-rZQ}-3Yn=)pSdHskY}VTy*0PJ;>H|>Yz1dfh~*hqO;DDtIxFK<8*+3QMJaqM{5jhs36{di3rM#s}?xuH6U*W4I=`myf@ z{swnwK8|pG$z?jQqk2`{M1$CI!(nc&=)73is~8|vZrRYyr**T5JzzW)Cn2A$%ePP) zLyx4lM+P)%PHD8v%B(V`QLE5sb@|_DjORb^WSPIc1KnAlxa@!1pd5G}m$Ewuf}wpXUzmNx&U$QR`Vp_7D`8V|l#*jel5$`8VJuFN zU^lEU&c-^^r_&9h`;O!R_hJbJq}UQbavu_{Y#7)xUz?`!Wxbk#`)$Z4b`E)=*=S!t z?f--JHvFjCJ=U@y{fgHpIYr41sNaN2jogN^sA;2&LXV5n)YFu&xb~bt^||Oqq9=hE zc9I_tVVH<3(w85iS4pmE8ZtqixGVk{LL9r!wOj5L|b}{n>@K>2X5a%GDM*ZekLBo9zwsn67Zj6 z1bG07Rv9x|EB6cHB5EeAl;R1wKIPy4fy@r#u^t-?q)Yh$LzOI)s)YNh!D`B1lpGTg zfY7hwH!l);HS}Za^vqVO-y|ucVDGwxnVNEi)$ zq=9N@W#9)fTy9bcA~coBAxExJNLQaat3kBl22SYqcv5qu+vU_lvT#j>RA}RUitfo0 z;;sM-b@+omB896sYi-Q45QRzRXux!d=f(?fj2GS-FTB}NZPLy!r87IHVyF+zIbe;i&NB$w30uRFPtTX z2Bw3OX^xH!X~0Dq2<|B*)7QE97IJx@ph%LET($O@f#MRr z%iUH^VweFUT2i6GjIs8D%#_wETL=_mWF~2REciV?8c1R<;ylg3=rkh)1=?3R9Z43m zD3Q}ZoSZUp^L9aQe3@eiG+ofmLH~3doyH-GPY|2&5nHXQAOsRKh+L+CPWI(v!32vL zj76B&>HC9Ogvr<@1@@G3-bU@TO5UEWlAQVUGZ-kQDKa$1GXID!)6iZ~!K6gmQZ(~t zsF@waucG!D-I%FvSa~9R?-0(?I>Psaj>H}Gqf{e)gj`a2FxEiX2Tuxc>puY+d z#|ta2DR{+Cs0*8tpHlJ(B?XNXSE+b{lHJg|G8;YYm0lmbF^-&UVCViC4;|X{QP+{f z(EAg*O&NL)p014j7m&|}ZdbF;pu-j?vj#(FmWsKlStIIYyb+vd5;8&5AUD|83WF`! zcs6&2qmx&V@ys%(q1!=?>~J<}mu92p4tDU8&j9vk0*p@DpfPl$QPw7YiSlf?uA+99 zrNsnW*{J|FE3Qt!Mug;fVQUw#O&moF=%!Aht+`zdk9@Xx6G<{Bea0Q$?c(4uW{Nw$ zbvIM|?rc!$04ZRKijQQd<~G>{LzI8PsJj{Br;yJ^mOLV2ONnAjY&^u}20SZ888Shg zj*w#}2%46ept+oGPw_*vhb5^EXCps`%8n&{^P+z+d(pm%up)-DS{XX=8g~YWX~c^u zpG7hUU*#d^@0fxlj-y#sy-q>LMZR@1spbqt(9t6~?4+Egow`{tkhb9Wao_J$alWjTgcL0bmOZ=A^RU(rv|D3 diff --git a/tests/actions/__pycache__/test_initializer_action.cpython-39-pytest-7.1.3.pyc b/tests/actions/__pycache__/test_initializer_action.cpython-39-pytest-7.1.3.pyc deleted file mode 100644 index 0b46e4660b943907fa4e737960d6dd730d911233..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1835 zcmah~OK%%D5GJ{+U9H}-orjwo(iTXe0+9~|dTNUVMGgfLqd?k2_d=|fl(gzTgdr8j zfqF`={RckSNB^1r0A73QPiW9XhuoFyBtT1oqj`{=*>7g3q~Grl9J5!D-U|u&(+W37 zgu-K7>JKD>2%3|ax?fg!j8Ibeaf@0D%Y!0}L&tl0yNKeb=)@f-)4UsZagU$(ihkU8 zx>i0YhVjtxK|U(R@t6`Bitr7M_hk2C|B}c^w9l!CMCYxK+JmiDM!Pu_-SbfNMBnYW zC5N>|49^qwbENFUaKJFZ?EQJfX(_G0}?&Gtu*8o~6&L^jG6O zIsOGLhtHC7zE0-yxY94yt5pSBil+b-Tv0Rrq0DrarGzU2 zT~xn&di1lBppI0c(nT^oNwh>krfZOSc7%dfM~RC~If-Z2G4t9mU7dk-+sCDRB!mUj zX(pkGoKu-DDs$Vhc>@Sf$|OI-FE_W|*Q**Pb5Mudgu>IRoMm%&ZDY!-`8+G<9OHkP zr4nqdrGfR|49mB1IbaTu(Fn?hZrH}(c$=1>zXu2rJDvo_cM*<14sNtlZQ}|X1kG>@ zQxlsBn~1S$x|nppLT5#9^Ec(+rb@((*L7qaUrHOVb6K-`KJnWK{ zfC+6?I%}Z{&Y^gDv+jZUW@J diff --git a/tests/actions/__pycache__/test_scan_action.cpython-39-pytest-7.1.3.pyc b/tests/actions/__pycache__/test_scan_action.cpython-39-pytest-7.1.3.pyc deleted file mode 100644 index f6951815f9defe57f7db7c62086cd22b3d8eab73..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2525 zcmaKt&2!sC6u`ABS+X3(vGdgi7+`>wF|-bZ;cJHJw3Mco)-X+(0bewVcJ0W>mS=a> zHW_njul)~lNRIiJ_R5L>016Dex3Ywr23Dih?tA<3_NVvWM$5|$3(DY$i0>|0)-Tvu zEIxE@!YhA+idl@LR-c$J9oy6**4XLS2x@5Rj=jET+ICtW`+eWEN!sW)K<}hW>oU;lnIOyc`l{L>lHjlDRU+=^9_O+eM$<8dx{n%vGNgJjsDx{mt$ z8RNys2VCZt{<5Mi_lg zX4Z&?_RIn+?cPIJqapJ+=28ywwBrbzDe6#ZUxE>#;L}V8#WvxiD9t%c3C3`a5cLx- z&MeV{@h@&{e=oU^+cJ{zLDb!k6o=0IXxq?Hayv3-mZoRshlQj&Jrv8Z2)@#V%Azi5 z5+mz)+Bf_0_#iV9y=nSsG#Td6kcZJEN)L~?&>JtyWj>u|Lh1Fw+_@=XY%tHIX*L`t zlVJ#^KT2XQ(0^lN0a^hC+sMP=~Ikvl=7MT|h~D9alSOp>DrwSE~}0 zCV6vsdZi>(n&r(c5)SOuaYYM0=G3B%tyQenutHl5`J!5Zu3j?#tYC_IiL))Al7;7m zcpYSCw)qq*aoo~W#d-s0SwW&xZO0Px|ewGU3C`~yFA15&Q4O|GlaDl}&v!ptRRN-mp$stE; zWS9&>h@E7Hn6*K7-DAV2lDH z+kkUaO}QhFIR>0Pa!%@}kQfcNT2FfqB6bg4Tf5PI{PE5wXH>VcM_j;h!_tp*8w3*4 zN0>~cG9Y$4zCd5qPMSrmpt@d;8f2oHTTp)!h8o7=Eu3@?YBqsTco&D9gL?HMsOWTt zX=Ti(FjeA@wLF89BVj@1oVHZKT{=X3N=}<(BgMR`< z@X%dYMk2!K>EPz{Fsyo8;q(x<=@uq|yLSbDEE2`FUx~C=${H})SQQEFVGc=?eL-Ohy_`=H zxG`ZRya~)5r`=+(-Y64rcYZl_(VYb%*H5Om$Y1$E+l(Ge{Zgt{$IgLCI2H>U1%42LjU{+^zLsi diff --git a/tests/actions/__pycache__/test_yeti_action.cpython-39-pytest-7.1.3.pyc b/tests/actions/__pycache__/test_yeti_action.cpython-39-pytest-7.1.3.pyc deleted file mode 100644 index 1b348d4b9d94886555e95e08abe2526891670b08..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1087 zcmah|&2AGh5VpPl+axWBg5bodQiPPe0IC{Q6+$TCKsj_TtIgU?Htc32+o280jUKr4 z4SIlMpM-btl@qUkgvyMQHo4)Iy))l<>~DUYHJdSkaegWDpNx=SsB8}xlp~nx8!Vh~ zT9Sm?KdT%@D5>1UqiA8JUj<2EWm<+w2$)kwRh+~YbIYBoku)d~;rHebB3j(Lq1@-e zmHU+@yK6MIXviar?ul3gurt1K_zrJexyd6=Kah6wH?SdtHZ@T{8x?uK&OhB!6Q2oP z^m1L)Q!shQby-Ugd@1x?PIL6x9u$;gm=>-OIC+)3>)o24L7Me-67J zm5{2dGL?_B&M?yg6p_!RD2pyAT6Hsfy|P-m06yso>C6_URRPZx$TOWqcsC@S5GGO1 z9hBDtWe=t$*aAV5C0WuXl@AMHjQV#H^;^gOWCP^?5I=w|OEMvvPLMdI5W>Wn zxJzac#+_>iy8{Apv7BCxH9KuHxd)!c9o8jpdr3T##Z=qONlc0f`n@+NgVUbzti?Rb z6a=HwJS$7V(|VeV^rFyXxr-b(Wwi0V!b+ujoUKk=7bulN%|xzMYB{8kPRPwZ_$%}x zUW@rOypxEuADeJEFG^iZm1!K!7U_fbJe@7@{LYFIv|>k!3t4Dkq6f$X_gD|FC=V;d?h?>At<*V#HRF5v|T#FASQXJwFQBPqxU?FSJhU3#c7iPdHe1HD|x(5@A diff --git a/tests/actions/test_scan_action.py b/tests/actions/test_scan_action.py index 1947d47c..515656e5 100644 --- a/tests/actions/test_scan_action.py +++ b/tests/actions/test_scan_action.py @@ -6,7 +6,17 @@ from secureli.actions.action import ActionDependencies from secureli.actions.scan import ScanAction from secureli.repositories.secureli_config import SecureliConfig -from secureli.services.scanner import ScanMode, ScanResult +from secureli.repositories.settings import ( + SecureliFile, + PreCommitSettings, + PreCommitRepo, + PreCommitHook, + EchoSettings, + EchoLevel, + LanguageSupportSettings, + RepoFilesSettings, +) +from secureli.services.scanner import ScanMode, ScanResult, Failure, OutputParseErrors test_folder_path = Path("does-not-matter") @@ -14,7 +24,7 @@ @pytest.fixture() def mock_scanner() -> MagicMock: mock_scanner = MagicMock() - mock_scanner.scan_repo.return_value = ScanResult(successful=True) + mock_scanner.scan_repo.return_value = ScanResult(successful=True, failures=[]) return mock_scanner @@ -24,6 +34,69 @@ def mock_updater() -> MagicMock: return mock_updater +@pytest.fixture() +def mock_default_settings(mock_settings_repository: MagicMock) -> MagicMock: + mock_echo_settings = EchoSettings(EchoLevel.info) + mock_settings_file = SecureliFile(echo=mock_echo_settings) + mock_settings_repository.load.return_value = mock_settings_file + + return mock_settings_repository + + +@pytest.fixture() +def mock_default_settings_populated(mock_settings_repository: MagicMock) -> MagicMock: + mock_failure = Failure( + repo="some-repo", id="some-hook-id", file="some-failed-file.py" + ) + mock_echo_settings = EchoSettings(EchoLevel.info) + mock_pre_commit_hook_settings = PreCommitHook(id=mock_failure.id) + mock_pre_commit_repo_settings = PreCommitRepo( + url=mock_failure.repo, hooks=[mock_pre_commit_hook_settings] + ) + mock_pre_commit_settings = PreCommitSettings(repos=[mock_pre_commit_repo_settings]) + mock_settings_file = SecureliFile( + echo=mock_echo_settings, pre_commit=mock_pre_commit_settings + ) + mock_settings_repository.load.return_value = mock_settings_file + + return mock_settings_repository + + +@pytest.fixture() +def mock_default_settings_ignore_already_exists( + mock_settings_repository: MagicMock, +) -> MagicMock: + mock_failure = Failure( + repo="some-repo", id="some-hook-id", file="some-failed-file.py" + ) + mock_echo_settings = EchoSettings(EchoLevel.info) + mock_pre_commit_hook_settings = PreCommitHook( + id=mock_failure.id, exclude_file_patterns=[mock_failure.file] + ) + mock_pre_commit_repo_settings = PreCommitRepo( + url=mock_failure.repo, + hooks=[mock_pre_commit_hook_settings], + suppressed_hook_ids=[mock_failure.id], + ) + mock_pre_commit_settings = PreCommitSettings(repos=[mock_pre_commit_repo_settings]) + mock_settings_file = SecureliFile( + echo=mock_echo_settings, pre_commit=mock_pre_commit_settings + ) + mock_settings_repository.load.return_value = mock_settings_file + + return mock_settings_repository + + +@pytest.fixture() +def mock_pass_install_verification( + mock_secureli_config: MagicMock, mock_language_support: MagicMock +): + mock_secureli_config.load.return_value = SecureliConfig( + overall_language="RadLang", version_installed="abc123" + ) + mock_language_support.version_for_language.return_value = "abc123" + + @pytest.fixture() def action_deps( mock_echo: MagicMock, @@ -49,12 +122,14 @@ def action_deps( def scan_action( action_deps: ActionDependencies, mock_logging_service: MagicMock, + mock_settings_repository: MagicMock, ) -> ScanAction: return ScanAction( action_deps=action_deps, echo=action_deps.echo, logging=mock_logging_service, scanner=action_deps.scanner, + settings_repository=mock_settings_repository, ) @@ -64,7 +139,7 @@ def test_that_scan_repo_errors_if_not_successful( mock_echo: MagicMock, ): mock_scanner.scan_repo.return_value = ScanResult( - successful=False, output="Bad Error" + successful=False, output="Bad Error", failures=[] ) scan_action.scan_repo(test_folder_path, ScanMode.STAGED_ONLY, False) @@ -119,3 +194,271 @@ def test_that_scan_repo_does_not_scan_if_not_installed( scan_action.scan_repo(test_folder_path, ScanMode.STAGED_ONLY, False) mock_scanner.scan_repo.assert_not_called() + + +def test_that_scan_repo_handles_declining_to_add_ignore_for_failures( + scan_action: ScanAction, + mock_scanner: MagicMock, + mock_echo: MagicMock, + mock_settings_repository: MagicMock, + mock_default_settings: MagicMock, + mock_pass_install_verification: MagicMock, +): + mock_failures = [ + Failure(repo="some-repo", id="some-hook-id", file="some-failed-file.py") + ] + mock_scanner.scan_repo.return_value = ScanResult( + successful=True, output="some-output", failures=mock_failures + ) + mock_echo.confirm.return_value = False + + scan_action.scan_repo(test_folder_path, ScanMode.STAGED_ONLY, False) + + mock_settings_repository.load.assert_called_once() + mock_settings_repository.save.assert_not_called() + + +def test_that_scan_repo_adds_ignore_for_all_files_when_prompted( + scan_action: ScanAction, + mock_scanner: MagicMock, + mock_echo: MagicMock, + mock_settings_repository: MagicMock, + mock_default_settings: MagicMock, + mock_pass_install_verification: MagicMock, +): + mock_failure = Failure( + repo="some-repo", id="some-hook-id", file="some-failed-file.py" + ) + mock_scanner.scan_repo.return_value = ScanResult( + successful=True, output="some-output", failures=[mock_failure] + ) + mock_echo.confirm.side_effect = [True, True, True] + + scan_action.scan_repo(test_folder_path, ScanMode.STAGED_ONLY, False) + + saved_settings = mock_settings_repository.save.call_args.kwargs["settings"] + saved_suppressed_id = saved_settings.pre_commit.repos[0].suppressed_hook_ids[0] + expected_suppressed_id = mock_failure.id + + assert saved_suppressed_id is expected_suppressed_id + + +def test_that_scan_repo_adds_ignore_for_all_files_when_settings_exist_when_prompted( + scan_action: ScanAction, + mock_scanner: MagicMock, + mock_echo: MagicMock, + mock_settings_repository: MagicMock, + mock_default_settings_populated: MagicMock, + mock_pass_install_verification: MagicMock, +): + mock_failure = Failure( + repo="some-repo", id="some-hook-id", file="some-failed-file.py" + ) + mock_scanner.scan_repo.return_value = ScanResult( + successful=True, output="some-output", failures=[mock_failure] + ) + mock_echo.confirm.side_effect = [True, True, True] + + scan_action.scan_repo(test_folder_path, ScanMode.STAGED_ONLY, False) + + saved_settings = mock_settings_repository.save.call_args.kwargs["settings"] + saved_suppressed_id = saved_settings.pre_commit.repos[0].suppressed_hook_ids[0] + expected_suppressed_id = mock_failure.id + + assert saved_suppressed_id is expected_suppressed_id + + +def test_that_scan_repo_skips_ignore_for_all_files_when_ignore_already_exists( + scan_action: ScanAction, + mock_scanner: MagicMock, + mock_echo: MagicMock, + mock_settings_repository: MagicMock, + mock_default_settings_ignore_already_exists: MagicMock, + mock_pass_install_verification: MagicMock, +): + mock_failure = Failure( + repo="some-repo", id="some-hook-id", file="some-failed-file.py" + ) + mock_scanner.scan_repo.return_value = ScanResult( + successful=True, output="some-output", failures=[mock_failure] + ) + mock_echo.confirm.side_effect = [True, True, True] + + scan_action.scan_repo(test_folder_path, ScanMode.STAGED_ONLY, False) + + saved_settings = mock_settings_repository.save.call_args.kwargs["settings"] + saved_suppressed_id = saved_settings.pre_commit.repos[0].suppressed_hook_ids[0] + expected_suppressed_id = mock_failure.id + + assert saved_suppressed_id is expected_suppressed_id + + +def test_that_scan_repo_adds_ignore_for_one_file_when_prompted( + scan_action: ScanAction, + mock_scanner: MagicMock, + mock_echo: MagicMock, + mock_settings_repository: MagicMock, + mock_default_settings: MagicMock, + mock_pass_install_verification: MagicMock, +): + mock_failure = Failure( + repo="some-repo", id="some-hook-id", file="some-failed-file.py" + ) + mock_scanner.scan_repo.return_value = ScanResult( + successful=True, output="some-output", failures=[mock_failure] + ) + mock_echo.confirm.side_effect = [True, True, False, True] + + scan_action.scan_repo(test_folder_path, ScanMode.STAGED_ONLY, False) + + saved_settings = mock_settings_repository.save.call_args.kwargs["settings"] + saved_excluded_file = ( + saved_settings.pre_commit.repos[0].hooks[0].exclude_file_patterns[0] + ) + expected_excluded_file = mock_failure.file + + assert saved_excluded_file is expected_excluded_file + + +def test_that_scan_repo_adds_ignore_for_one_file_when_settings_exist_when_prompted( + scan_action: ScanAction, + mock_scanner: MagicMock, + mock_echo: MagicMock, + mock_settings_repository: MagicMock, + mock_default_settings_populated: MagicMock, + mock_pass_install_verification: MagicMock, +): + mock_failure = Failure( + repo="some-repo", id="some-hook-id", file="some-failed-file.py" + ) + mock_scanner.scan_repo.return_value = ScanResult( + successful=True, output="some-output", failures=[mock_failure] + ) + mock_echo.confirm.side_effect = [True, True, False, True] + + scan_action.scan_repo(test_folder_path, ScanMode.STAGED_ONLY, False) + + saved_settings = mock_settings_repository.save.call_args.kwargs["settings"] + saved_excluded_file = ( + saved_settings.pre_commit.repos[0].hooks[0].exclude_file_patterns[0] + ) + expected_excluded_file = mock_failure.file + + assert saved_excluded_file is expected_excluded_file + + +def test_that_scan_repo_skips_ignore_for_one_file_when_ignore_already_present( + scan_action: ScanAction, + mock_scanner: MagicMock, + mock_echo: MagicMock, + mock_settings_repository: MagicMock, + mock_default_settings_ignore_already_exists: MagicMock, + mock_pass_install_verification: MagicMock, +): + mock_failure = Failure( + repo="some-repo", id="some-hook-id", file="some-failed-file.py" + ) + mock_scanner.scan_repo.return_value = ScanResult( + successful=True, output="some-output", failures=[mock_failure] + ) + mock_echo.confirm.side_effect = [True, True, False, True] + + scan_action.scan_repo(test_folder_path, ScanMode.STAGED_ONLY, False) + + saved_settings = mock_settings_repository.save.call_args.kwargs["settings"] + saved_excluded_file = ( + saved_settings.pre_commit.repos[0].hooks[0].exclude_file_patterns[0] + ) + expected_excluded_file = mock_failure.file + + assert saved_excluded_file is expected_excluded_file + + +def test_that_scan_repo_handles_missing_repo_while_adding_ignore_rule( + scan_action: ScanAction, + mock_scanner: MagicMock, + mock_echo: MagicMock, + mock_settings_repository: MagicMock, + mock_default_settings_populated: MagicMock, + mock_pass_install_verification: MagicMock, +): + mock_failure = Failure( + repo=OutputParseErrors.REPO_NOT_FOUND, + id="some-hook-id", + file="some-failed-file.py", + ) + mock_scanner.scan_repo.return_value = ScanResult( + successful=True, output="some-output", failures=[mock_failure] + ) + mock_echo.confirm.side_effect = [True, True] + + scan_action.scan_repo(test_folder_path, ScanMode.STAGED_ONLY, False) + + mock_echo.print.assert_any_call( + "Unable to add an ignore for some-hook-id, SeCureLI was unable to identify the repo it belongs to." + ) + + +def test_that_scan_repo_does_not_add_ignore_if_both_ignore_types_declined( + scan_action: ScanAction, + mock_scanner: MagicMock, + mock_echo: MagicMock, + mock_settings_repository: MagicMock, + mock_default_settings: MagicMock, + mock_pass_install_verification: MagicMock, +): + mock_failure = Failure( + repo="some-repo", id="some-hook-id", file="some-failed-file.py" + ) + mock_scanner.scan_repo.return_value = ScanResult( + successful=True, output="some-output", failures=[mock_failure] + ) + mock_echo.confirm.side_effect = [True, True, False, False] + + scan_action.scan_repo(test_folder_path, ScanMode.STAGED_ONLY, False) + + saved_settings = mock_settings_repository.save.call_args.kwargs["settings"] + + assert saved_settings is mock_settings_repository.load.return_value + + +def test_that_scan_repo_does_not_add_ignore_if_all_failures_declined( + scan_action: ScanAction, + mock_scanner: MagicMock, + mock_echo: MagicMock, + mock_settings_repository: MagicMock, + mock_default_settings: MagicMock, + mock_pass_install_verification: MagicMock, +): + mock_failure = Failure( + repo="some-repo", id="some-hook-id", file="some-failed-file.py" + ) + mock_scanner.scan_repo.return_value = ScanResult( + successful=True, output="some-output", failures=[mock_failure] + ) + mock_echo.confirm.side_effect = [True, False] + + scan_action.scan_repo(test_folder_path, ScanMode.STAGED_ONLY, False) + + saved_settings = mock_settings_repository.save.call_args.kwargs["settings"] + + assert saved_settings is mock_settings_repository.load.return_value + + +def test_that_scan_repo_does_not_add_ignore_if_always_yes_is_true( + scan_action: ScanAction, + mock_scanner: MagicMock, + mock_settings_repository: MagicMock, + mock_default_settings: MagicMock, + mock_pass_install_verification: MagicMock, +): + mock_failure = Failure( + repo="some-repo", id="some-hook-id", file="some-failed-file.py" + ) + mock_scanner.scan_repo.return_value = ScanResult( + successful=True, output="some-output", failures=[mock_failure] + ) + + scan_action.scan_repo(test_folder_path, ScanMode.STAGED_ONLY, True) + + mock_settings_repository.save.assert_not_called() diff --git a/tests/application/__pycache__/__init__.cpython-39.pyc b/tests/application/__pycache__/__init__.cpython-39.pyc deleted file mode 100644 index a04da8525b396d094df96b123024b5ae38c8ef09..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 149 zcmYe~<>g`kf*r4-l0o!i5P=LBfgA@QE@lA|DGb33nv8xc8Hzx{2;!HOerR!OQL%n; zVsUatqFz#BNh*+&np|3xnvlIYq;;_lh PPbtkwwF8;?8HgDGS}Y_1 diff --git a/tests/application/__pycache__/test_container.cpython-39-pytest-7.1.3.pyc b/tests/application/__pycache__/test_container.cpython-39-pytest-7.1.3.pyc deleted file mode 100644 index 8c4d68f1ab05bba4af989e862c9b270266c86326..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1129 zcmYjQ&2Q5%6!%NqCh6C@9S{>BO-M-e(oGx@LX!p@cL9Wm1d$heo4Yj0Y@uD+T4u@bq?lB%2Rjl`T|sTaO*k} zL#711!LIUA`$d!kD{`^D`BDp|H#O7gA?xokBTy0PNQq2tqGI$WE6Yr#%*Z0IkR)9s z&@aa}T56Gt=OH1U3X%4pVyVkzv1%wZaaXzhL}5Z^WR4v~4s-H^y!mnt#+$$tj6Wi( zd)7c1?XSpjml8wAGa%<4kh8a@X6mzG0)uc6&0)~s_#z*@buxu>-_#Iqx-O$eCv@T; z`RcQ2&pI4^)_tl} zp*EDzMRp?dgN?I$4>y=P&qn;*r$=IpS~+%4Y3V+bnLC@ap}@1gHmX;vUMUX$rK&85cy&=rf?g6ACT=fE zaghE$LMm|St~>vUIb`PlbR=29xbC1?-$p{bkhY)&9yFkX^u{IiJ6;GBZeFTeXz#7q zaBn0tgO%FWb~#R#+In(TL7!0j5eG86y+ diff --git a/tests/application/__pycache__/test_main.cpython-39-pytest-7.1.3.pyc b/tests/application/__pycache__/test_main.cpython-39-pytest-7.1.3.pyc deleted file mode 100644 index 65c727c667af52688d9d2b5f93a9c94eda1a4de5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1963 zcmb_d&2HQ_5GE--wbDF#c0$g@I3tcB*}e3{zb6) zfQ~$d*Zc!Q5=k=>Q#Z2QV}z32k2@4sSQg~nxa;OL3*!)UUe?R|alh3K;-UL;6c1H+ zHC_?bS6%79VzMKHm;NP|2eaYSwJiTE>3p3IZy07f_}{d;;=EwqZ54RWWjfmWa?5{jR-4}%dS@GHFL z9*Bxuk|nLl6_vE6D@I9;XR+gpeWMaEn5ENPq{TFu7Zr?3M@*v?+bKnroB~37@jf2P zZ#MC?FqJ40^_u7|tiS#7;18p;IWWQ`r(${{Dg~fD;Q&Bo4n$dIX(Fn0UN{YR2bbq| zylosW*aRR53+RyQ9?V`h!}wpOefa4K;Q#mNV_1EcN9czhz+%+VLm2c3g!PfUouEubmR=~$ zMS`K$JGkdvj1Sk0o#%;Gcx`aEw{37yNUqM{I$?bu>={f7LicJ3#qf)_d!ayZx!*)rtIki`%ESxnU__k-0pt zV6x5%NtaOAOIFjGJ!ea=_Kd6`^Chdj8I>ODXKdwzt^?&y-N;rHo7g?F^s$lEeoA59 zsRCH-)}0l-q<=xn^r3OU>n#~(T+}^&g9R7Sx9sRBq8F1dzADeZjm-U{qx(306d7nv z9_Ua!x)?Xr13wwY|${bDcxMsk1T@h3hO);QECffK}d5_+}G)w+S9Lpt17(CivlYdGD`p zuHns_yNe(u>uUHKBeZA3lSP_U&?D^-Y22Vsc;o7{AqI|2wR)jbh(hm1WxK6TV^6cA z1(YKmKE*EC^yZ*3V+U>I+=_?0C)cfQ-OydgEQlkzGNXC!W()4odo0d_a8D-@P_~Z diff --git a/tests/application/__pycache__/test_patterns.cpython-39-pytest-7.1.3.pyc b/tests/application/__pycache__/test_patterns.cpython-39-pytest-7.1.3.pyc deleted file mode 100644 index cc2b9e1bf17bf10d62620ca9c70c58aac9acd8ce..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2068 zcmb7F&yU+g6rQo2#L4~;rBHSO(Sll(^pJG7v_h!cEiG`UP*e$3LQz%AoAG9?Ixrl;heF7D{7*SwkaCIMcWch z(L%c>_z$eJ_7}d#`W>FXGmOUv;Uw@9U+X|l)Q~deRfE&O^9YYRgVAinUh*ksobrri zJY`cWv(&NXuPnhc>yQhJ`VXy{BW!KIa%Su&rXC_+%h-&|@8Q+Sn^?$`&u3gXe^^)8 z)TT9OcF5s7x`C{lIWzu}|AIX`m}!a@t8dW4tx$FN>AN@D$;qvbx)!Qynj9$3foolz$k|7xK-o&8%TO`ButU=9j(0 zSdM+|1;8C zag0y&sV(SJ7mXF@5^%Zp`w+S$q@d9 z*@l-tTmeYmQ{iM3RTYOr_w-Q}(gX`oTU=g6^mb5xLzX}l*2prj+81EW46KfU74R*p zvR=XZT?OltC0Oeftc?Y%u4t|RYpX9mA`C8IyL)=|dHVv`*>beoxh_A(Ao&RyqSDGx zKVAXqF=QZ2UPG-2Yro{UC<(BMc^8ncbNMNv3y_llRwDR20ajMKyml}ZCD zP(?+{xPu!<`x*SsdhV8LE;rDTZ+nIQcMJ7}CU<4 MagicMock: return mock_secureli_config +@pytest.fixture() +def mock_settings_repository() -> MagicMock: + mock_settings_repository = MagicMock() + return mock_settings_repository + + @pytest.fixture() def mock_open(mocker: MockerFixture) -> MagicMock: mock_open = mocker.mock_open() diff --git a/tests/repositories/__pycache__/__init__.cpython-39.pyc b/tests/repositories/__pycache__/__init__.cpython-39.pyc deleted file mode 100644 index 13dd10295ff6abc2350ccb17edb4366ea7dd04ca..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 150 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 diff --git a/tests/repositories/__pycache__/test_secureli_config.cpython-39-pytest-7.1.3.pyc b/tests/repositories/__pycache__/test_secureli_config.cpython-39-pytest-7.1.3.pyc deleted file mode 100644 index 607069e98f9f020c6b15c95033df137df1dad1fd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2840 zcmb7G&2Jk;6yKTsvK>2dlD70i0wZYEMbv3azf=*_3Q<6!5lZ3W%V@hZjyGNJS~Kgk z3F`~3xN+r_LwxLiz+b?b^ISQ>pAdg3Nq#fVl^+`UwcPI7zGyF<;uZsYR^5({TxEXyWz#j&J%TsdOqJvy*DS)~Okp zlPvY?oqE5~X%I_Pj+V!ks0p9Dhm?EVf8jhQos|W7g;$NdIg!_S?U3*#UN_QJ;fp$N z!1!gp^1|t?8NLbpDqjPB9eA`@KeYKNei|gFIC*3RXMP1=tgV3P>cem+jvi*w6JQ$n z74kv+EFVe{P<{51h@h9mA7^Pd-q{v|OvQO7_x18g4cK~pGgy04!00q0xWuHn@Wpfd z7+$pnB)6VhBf?2Rb_t{AYcp!3v>*lDbtar$(88W^PGJ|IDXb%x)4~~3Vigqpv9}%^ zzo#a9ZOiF4W1-qpDT|B??JvW;7kHAwCbTD>#VS|2UT&G~g~?E8FIJ4lQZF%<%c0=$ z9%I@agn85pTFFkV&$?N{g)AlKjAXX_ylTa$a2$)0P^p)eT!U$k zKi&9R38^+zsG?rjejMfkn2B2(z~pLU-Z>*;Q-i68gMGc4W+^kyi8MFcsl)v03=oTY zqyi66?7{2NI+dqDb0TEu=PYDhU=~6KVcLDIknOtM=FazI9_rQmm4c)V98@+&bnrigVZ zLe6-YhX;+v!#K%dNIM&dR93*hz&D_fPy;N9B|$DB!GqIP6EI`i03Psz$0L`KtRQJ3 zSw(_pY@%6`bzpVlL@3?@k$MxzKZU{}^iR*LV@Tcri-1TUJ`q|^8zvUMCYf-fCNfrG zK_U>yX^CM2cUKmGv9K$77R>I#tI!_zVkAL{1=Ei(OQsFa2=`)^{Q)`!IkLvqh(flK zkP}(aK{H3jJFh_D7$<5T*&VRtp`5Wxq3GCon{-3a`Y-xW| zngVi6H#c7&f14#b0e>b~@&XN8%I;J>1<#1hW}# zX>vrzP_;~k+FPBa`%i_+`T{FCRIGPk7_5T73EENNp3rM0NKMKJ&ayNTfwQoU@&Fe6 zys+RW&NlUP8|GJ7xh=OwYE<5aUT|7hrWq(Nfk2;QWzM$CEkGW@AH`D{=a7Fhk@hDt z`7VxW4AVHrmD_kzO#Uv)Y4p;h1Q-pTl)gBv%>VkUV(NH#CVsd4B`eeU6#_7GB! MagicMock: + config_file_path = MagicMock() + config_file_path.exists.return_value = False + config_file_path.is_dir.return_value = False + mock_folder_path = MagicMock() + mock_folder_path.__truediv__.return_value = config_file_path + + mock_path_class = MagicMock() + mock_path_class.return_value = mock_folder_path + + mocker.patch("secureli.repositories.settings.Path", mock_path_class) + + return mock_folder_path + + +@pytest.fixture() +def existent_path(mocker: MockerFixture) -> MagicMock: + config_file_path = MagicMock() + config_file_path.exists.return_value = True + config_file_path.is_dir.return_value = False + mock_folder_path = MagicMock() + mock_folder_path.__truediv__.return_value = config_file_path + + mock_path_class = MagicMock() + mock_path_class.return_value = mock_folder_path + + mock_open = mocker.mock_open( + read_data=""" + echo: + level: ERROR + repo_files: + exclude_file_patterns: + - .idea/ + ignored_file_extensions: + - .pyc + - .drawio + - .png + - .jpg + max_file_size: 1000000 + """ + ) + mocker.patch("builtins.open", mock_open) + + mocker.patch("secureli.repositories.settings.Path", mock_path_class) + + return mock_folder_path + + +@pytest.fixture() +def settings_repository() -> SecureliRepository: + settings_repository = SecureliRepository() + return settings_repository + + +def test_that_settings_file_loads_settings_when_present( + existent_path: MagicMock, + settings_repository: SecureliRepository, +): + secureli_file = settings_repository.load() + + assert secureli_file.echo.level == EchoLevel.error + + +def test_that_settings_file_created_when_not_present( + non_existent_path: MagicMock, + settings_repository: SecureliRepository, +): + secureli_file = settings_repository.load() + + assert secureli_file is not None + + +def test_that_repo_saves_config( + existent_path: MagicMock, + mock_open: MagicMock, + settings_repository: SecureliRepository, +): + echo_level = EchoSettings(level=EchoLevel.info) + settings_file = SecureliFile(echo=echo_level) + settings_repository.save(settings_file) + + mock_open.assert_called_once() diff --git a/tests/resources/__pycache__/__init__.cpython-39.pyc b/tests/resources/__pycache__/__init__.cpython-39.pyc deleted file mode 100644 index 303ac7aeabd1506b2e63b3855d449111d95455cd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 147 zcmYe~<>g`kf*r4-l0o!i5P=LBfgA@QE@lA|DGb33nv8xc8Hzx{2;!H8erR!OQL%n; zVsUatqFz#BNh*+&np|3xnv^8dRFb`3d%u!wN=asV=nPQm%ciBYr-mu*z=`I<0D~_c#nL8j{WX%dO)|R| zk}Ja>=plRTKi~)OD|F=qKcScMp1fO9;D9}PdSBN2michlBk=tCXObz8kl&HG`gDMJ z1WW%0KoCI-l2Es^(qn{@(oZ@RC9DX_ZqjwQSA=DhL}f4OIhq#zqz^iNF(`-0aK#%X z8?t}7xg>HZBhh)qL?F5s{y9yyR{ThI<@kuo@s#Zn5rRe}dQM~essz67gKQv%7k+Z> zKO`fu0g?{{{g%X=e?TPUAf`5a!i$0*7BXfwddiRT>}j1nw^0c&ts7Z^&JbBreVw0} znL@8dxKhq@x^Zvov@NkwFCa?4ajo7~C zWmCvhaKpi3q$C$D!EC4DCOcL>oE1}rXTK(@58Io^d*5lP^q%HAJLZ!^ZX^&g122nw z4+x|8l+^W1WgtL0bqB9`ljhWp-$e?tRu#KJGXR9CA;7rZR2T4n#ftH|SVKSzc}AaM zg!CZxY`As><5_uOhwpg+0igvKP74M|ZUWeVwe|w(*UYhH8? zKc;8nPi{A-cVc~WfBz<;_hOBmq-TYJ-ts_(;NIDYYpAYqI`=^bnD^t*_8v8-u9RHa ztu%x7g~Bx|tILKfX$K%q*Ue>vq6Q6ZcT#;^&kEsG)1#t3*{A$iMuV)9`X5A;=w=4Z6CUL z%rTgHuRGiT>Qpx}%cr>%=`@EK$t$;O@U9S2T>~dH4hQivX_eWT+p6uYPpI0w?QUmnXg#g`kf*r4-l0o!i5P=LBfgA@QE@lA|DGb33nv8xc8Hzx{2;!HyerR!OQL%n; zVsUatqFz#BNh*+&np|3xnvX0|4eABF+E+ diff --git a/tests/services/__pycache__/test_git_ignore.cpython-39-pytest-7.1.3.pyc b/tests/services/__pycache__/test_git_ignore.cpython-39-pytest-7.1.3.pyc deleted file mode 100644 index 57e0c09c252828616df27393ae83affc5db8a815..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5891 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 Kor~+?sQwpAqMHi< diff --git a/tests/services/__pycache__/test_language_analyzer.cpython-39-pytest-7.1.3.pyc b/tests/services/__pycache__/test_language_analyzer.cpython-39-pytest-7.1.3.pyc deleted file mode 100644 index 80e708dd526f28046257d36b8730063751c4985f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4132 zcmbuC&u<&Y6~}k>7s(|h*|IHLcG55jl6sp)mMmL#invMIxTi!gYSafJ=wiiLNgFSh z#LQBb)MXEKP!t9FANU|$3g}(UD zO52bNti3qWMzyFR7hhO%NnYTV<*Am7Xjzt*xaHDR%Vo4&kt^JCdDOyQUX|BadnIbX z@6{Kkye@BiWw%zi{u=79%O9bBmFsVy-jr{meoczUhX2;r6wz&86!xPq+dmUU?elOi zZhziwKSky0XJNY65BH)^(l9wXj@08w9mMSj_O6PuzDnEFfAqmR7&e|?;?X}vkr~g8 zfsqL*o(q&Fm)3K!XXVD1W?)GhzjlBcd~vp4)^_70+E~jDvr@c07B@*O7RVrIchf9N zGwnNyrc)-0VkwDr7O1G#4ai6rjnc#5AWZsEQA@g^#+Y%LCb6jED<;-aEF0YRrFIdrJ_A^yhyG#qr-O8%i?b8SH^Kt4YFE5 zQFu=%G7pvNDn*_absftnh<10QcBYodj-pZ)%Vcn4CT?YHgNTN>0)x31ZV1-Ty)dm9 zQ$oX*FD&VmL}2nOncGF>7ojBl&MbDmsPFY7t%D>w#B}8UnP@GuKdcJ48BMQrdCDZ% z0SUBlZ<5O{3apX9+FXb9zqsl?I?DFDsk(~(YL$u^CsybE40_z>Cafo2cnx*mVoKN@ zRe_>S;TnBNKHp@tn3<6qIkwne%*V!=x$T>UH?a#Are0l9()7)V@n<}}J_q!9v~HoR zCeRGW6!`vAH6f3jCCC3uXVvS}XVN1cWX7Xw^B$F<`x$yq`$Ovqe~3i&|N1i?WX7L2 z=KXmVXZyjkP^EFYrytI_^%gX9SW&Lz9Y7nl+yP9!*j5%a$guU@ZXzQU^pKcmfKqS6 z5s<;@m!z7_&K()J_UZ`+N z1UlS}NK-Um8SM1qB#TpBxJehr@FLLr-F_muSFo3Kcfuq{!%kEze!?>&-Eu}PE(NPP3FguTNHPmz8peJCl{I85W8U_dZdc$*7F;|`0tw~(l@*23j#K(YB?g!04rr^F;!hLTK4fuTX(V2K@K6ro)v^;X+K{?|cUt?KmDB2BorbgK+N7?c{<&(kX zSGlHsf+qDI6?Can?^Cfx1sw^B?ks8pgt)X{VbMophWp{|;eHJ25t} zGBd|D7FkSyFFSX@nVY-%Z<%#sgL;>#hn_eC&12fFlS)Qe^9xvl<5AW;x5%I`_rveW zVz?h1ma%jA7yE*L9Wd*@aIilrvLdT_g_*5pHMDv;h#u!#><=4{i_A=Ux3>K?9WsiJ zP!RKYi0CY<>%P88O-R3G6NDkgNYHEgY251}pYV0t8~EO!IYjP0VUe8;N$CqP|*GM~4eBBl0zf?#1PWs!0-`5a%0*&kf#A{9+S<;)@giZ>WCQ>B@c*{hV$+ on%41M(J-rK!*a!i`tWh_>$)c2Ex%jzUMa!C{#T=h$DV!Z diff --git a/tests/services/__pycache__/test_language_support.cpython-39-pytest-7.1.3.pyc b/tests/services/__pycache__/test_language_support.cpython-39-pytest-7.1.3.pyc deleted file mode 100644 index b5d9cbe7ca0d9cc8dad8c65bd14da8d27e040af1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2374 zcmaKt&2QX96u>>>&vkb5)v663q^QtRBX!dzEv*oNBH(~TBh*q3dohk@HgRgNS2MF| zQ@j^iaq9(f%pogr>5acsU}$_TSxP_Uht zG6&vn3>HM>?r1OBO>b;&eHT$3%2AqfDf^>buN-nAvvLq;b#R=$>g*+ zPuQ@SK0h1SFGZYB1J~_3(D*RPM_gcd+Mmv(*CbD&J7!$*RN)%?W%(%182sS*0&)@$ z1&`CRC^BWbURq)WW_b1G#zP5FZOBBX`$=~{dn37+V3S;Avreu_9@!BTJP`GgiFyyh&GrVR;3nK! ztilL!4#atA5)*+-8c$@K^^7ij=tyz8*@OCgTiakLtFkJ>$O{S^M|WVqmd@l!hm8=h zR3h3~0aN%oCcQS>t66U1Y3kmFaT1CTmdCt^3!uS7_^R{Mj4}W(hHmon8z_}^0_aW1 zm{#O~oB({NE;JfdT_a^16R1(?Rh7V9|qPX>RF_Dm3WhS}}L&14FYU@ri9Og%}>*cen>+tUI2$7Eyp;ycYvEV}iWiw0!*KO#IcSl*SpzO4lmoTN&Yq8uf zM>#V?%Kv1q7*NNO2(?3olj@a4EY`eHN`lj zZ6Y?nK2>0d?Ht`-z&M4jbpoY^=mNBMMJpSibt*^xtmugi&^iWM@K2!ylnu1sLceFA z?Sbacp+y~_1z0Hn>Ka&G1FNS37}=`42CNPgOAV^H2$2aaGqDB&7+swLIiqh2kn*R8 zFx2TgFbWadI>j9zD&EEKLxE?BSVwUQ1?EY-kK!_lD=0odaTP@bBJvx1eH|Rl9o-ao zp~KJ)LoGa6)ol2lzh!0Y?WtEbWHvx}vh>`EmPb5qIg>m#?m)h^A4&2FCV zSzHAkrrPgkPvGw6jT6}%smP_kUutB`}$R6Vp2qUyy$TCI1+_PT4YHM0&) zSYK$b`3F!A@dYXTJ)GvsiGKkQ@ZPNLI;IH0u4eYVH?wbMe(!7ia=Bo@^TV+Segf)e zWELL>n7hzqjTiSG~(O=p(sX>gs)wBtY(8%e#O;^)qlu{^V9NOA;(qun83_)8>Vr6bx)%@HVP_=# zB#Z|jtUTrc3`b!$P^YT$Fz$51LFX|S2VuZf9{78Ko=%n1*GiQmIA|RNV43-20cAql zgC=*OON{5nm?VUe=OiT_)jUl}N_(b9Q{&WPbV9~vN>k+VOy>U4<%XPYX(x(1a;NPN zg2VS)0zTcNH)dlnNpDX!U=nV(p)s%p?m$rJR4Ln|ci=}Ou8N+Qh!JPuf#)fE=qEw9 zZizfjtl*)%b{KIF z2WEBvU=n1rt?nS}P50|frH$dC%FmUTK&PxgXHbU}pyg~GH<`Lz}Q4nw`1)g15;Q)-}41K`| ziL(1oStwKuf`$wD5tir7iy^4Q>!@=H(mfp{N!R~dqhBTT`0ji-1EMm3zuZD%7IHt{yfuVGim z?j7vVq5cCxQ+@xA(0$M=b@_EPLRN(eJnK0ma2z8*=a?UX_$MSFiCf^4&e&$g*h%fw z85v_Yb>;U7Aj4?twh1#)-lh|aS&8=414Didnrr8y@b~W#QW? zvHj^bQrGLU(S^D=b?Cx)JCu6hR$Zz*c$f!1EDj+HnJ^ichRWg6pbML@h5Zn20`v6n{2^(9ep0;Z>m^uEUO1 z5tq&mcw>?IV3E0*F`4Gui_C}1F1!ElL9K&%5Dz$R)mwmR7TTCpLO%+RIC}zn%)B>? zZwsRPbCA_9tK1AQ@ev+m%gZ2cWQPGj8iIc#!UW)&iPW>+3vo-*%p+u1d5p~hh4(K$~Cw#H|{EIR9AfmgXt`Y{_t{4>1E$-B^9 ng>0FFRw5;`YC5D=hL+QfT2Ry~1zp^*!2MRLPU+uEt262^XE7eE diff --git a/tests/services/__pycache__/test_scanner_service.cpython-39-pytest-7.1.3.pyc b/tests/services/__pycache__/test_scanner_service.cpython-39-pytest-7.1.3.pyc deleted file mode 100644 index b6fce399fcc1a11b2ebdd72a2948c69b9cf1dbc7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1216 zcmZ8g&u`N(6tBAVJY@|-*f!F_dc5q1|fnmI+y83g3wRv+#EhQd+@1Km@<@M zffDRiQg{SmRQO2?+XxZuqLXx-jYW_IfO(=@gh}WyU-XK8(k}+d03#kO)|QBOct2}> zCs{k|eDg1Hvd;UwlLdfuv+$drY-IQtMZI5;8y!X1bPw65JUy(^6L1Fm=RB=7f6rAd zbVSVBamvb)%VRE2^OT#w`iE7lpqg4q)hu3LLe{Xt0hjlJ=UMkZT z6>L-{1k|cQnKXB)8dtT*fXP(SPof$!K}%L})1``yxP{HyD_2`ymHSdw(rnRDB@3o0 zKc5O#G7XWa<>Ya?$eyL$v&kERj~l1?>#4gf&1BUz&m9>(h6Dc--}%aNt5mKlnR7+Y zay`Crch}$qWp5$!`y|BjK3LH$6I@j-w*WBrXjAe^CEy!NekOAbbv6_MGMyDBZ`*$c zb(w2REw;}^+Ph}j%`s;C>RQKasI+9MZGnofpOo}m+JD=w?tQ$f(7m5V-p(jLhu6oO o{#pJv8e|`qbX^Lb7gbgZ{?a~7wE@#Z5|BRL@Y;B@MKJmE7vE@GM*si- diff --git a/tests/services/__pycache__/test_secureli_ignore.cpython-39-pytest-7.1.3.pyc b/tests/services/__pycache__/test_secureli_ignore.cpython-39-pytest-7.1.3.pyc deleted file mode 100644 index 61da295306dca38efcd3c77989803d44daac7b7c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2165 zcmZ`)OK%%D5GJ`#$tyqPBu)B)gEofO1>`o59*Wpb5i~##5dt^?3N_7Qy`-#7Rx5#A zr?#-V)VUVux$wa{=GvdbYfruQ7@$RGxRR47%?dl5`8XnnGv9FIMx##P`TG%1es>7@ z6OA_?8;m>fsy{$*!f8f2)QqfeF+xe-?l=@@SmyS)Td{snQ9c1U&YeteK7_Xo){Fd8@tc`rRG4yAZW^~F7rfR?4- zALIim_Jn+%CL(0o?Hf<+N&&8ULUFz6W5I~;U3k@pAWCuuDVoqRE$9guvyzoG0ttbz z_Gn#A_h_kVc?C+fYm}O8TMy!*7h2jG#zoST43^YEwQ=-3&JIOrNf*H-j4A@JUX*09 zQo8vXNOM)hc_LmC8G!GryIYT7KeeS|mGt7)eq0DJrW0#?jb~nSfX85PTGFQ^!rEO=rIGW{5yV)R#Go|Z*BEh0q>dTSp z4GuFNNimesK{nWrvnY@ILf0Y{cLl^71av=3;)E&rvTtwn%n8kTqz)a}8 zPNQ7Km=O^04j8XmT`dnr7wh5-_!Gd!Hi+XX+Gohk>s#KaEoX=OsjqtRez+P2N zZJ+r}ehM};KP;%GkP-^a-GAO>weO@bFLjb*csb~ z4E6TI(X}TVyWh0F*a~-le6sb^=3{6M05*0X+#afk?`(yw&2abg(fYg_w9o(eh4dBM z-VW)gIVING_O{{JjZm3#K^rt7709<6qs1u^(f3wG!du~jyZ{b{Z}K8)m(hPE0#0Ry zKG3tnSPF?JQ-jd>HnHJC(=aBZMSe1&#VV**J zs-lJ0a`l~Xt7Y`UwB^hUl2Kosas9pD9f>!P_PW*Qu)4_&tDjbt MagicMock: + mock_data = r""" + exclude: ^(?:.+/)?\.idea(?P/).*$ + repos: + - hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.3.0 + - hooks: + - id: black + repo: https://github.com/psf/black + rev: 22.10.0 + """ + mock_open = mocker.mock_open(read_data=mock_data) + mocker.patch("builtins.open", mock_open) + return mock_open + + +@pytest.fixture() +def mock_config_no_black(mocker: MockerFixture) -> MagicMock: + mock_data = r""" + exclude: ^(?:.+/)?\.idea(?P/).*$ + repos: + - hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.3.0 + """ + mock_open = mocker.mock_open(read_data=mock_data) + mocker.patch("builtins.open", mock_open) + return mock_open @pytest.fixture() @@ -19,3 +119,59 @@ def test_that_scanner_service_scans_repositories_with_pre_commit( mock_pre_commit.execute_hooks.assert_called_once() assert scan_result.successful + + +def test_that_scanner_service_parses_failures( + scanner_service: ScannerService, + mock_pre_commit: MagicMock, + mock_scan_output_single_failure: MagicMock, + mock_config_all_repos: MagicMock, +): + mock_pre_commit.execute_hooks.return_value = ExecuteResult( + successful=True, output=mock_scan_output_single_failure + ) + scan_result = scanner_service.scan_repo(ScanMode.ALL_FILES) + + assert len(scan_result.failures) is 1 + + +def test_that_scanner_service_parses_multiple_failures( + scanner_service: ScannerService, + mock_pre_commit: MagicMock, + mock_scan_output_double_failure: MagicMock, + mock_config_all_repos: MagicMock, +): + mock_pre_commit.execute_hooks.return_value = ExecuteResult( + successful=True, output=mock_scan_output_double_failure + ) + scan_result = scanner_service.scan_repo(ScanMode.ALL_FILES) + + assert len(scan_result.failures) is 2 + + +def test_that_scanner_service_parses_when_no_failures( + scanner_service: ScannerService, + mock_pre_commit: MagicMock, + mock_scan_output_no_failure: MagicMock, + mock_config_all_repos: MagicMock, +): + mock_pre_commit.execute_hooks.return_value = ExecuteResult( + successful=True, output=mock_scan_output_no_failure + ) + scan_result = scanner_service.scan_repo(ScanMode.ALL_FILES) + + assert len(scan_result.failures) is 0 + + +def test_that_scanner_service_handles_error_in_missing_repo( + scanner_service: ScannerService, + mock_pre_commit: MagicMock, + mock_scan_output_double_failure: MagicMock, + mock_config_no_black: MagicMock, +): + mock_pre_commit.execute_hooks.return_value = ExecuteResult( + successful=True, output=mock_scan_output_double_failure + ) + scan_result = scanner_service.scan_repo(ScanMode.ALL_FILES) + + assert scan_result.failures[1].repo == OutputParseErrors.REPO_NOT_FOUND diff --git a/tests/utilities/__pycache__/__init__.cpython-39.pyc b/tests/utilities/__pycache__/__init__.cpython-39.pyc deleted file mode 100644 index 949d38fdc8e3919cf5637b8643d63cf7e2cec4e2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 147 zcmYe~<>g`kg0zaFWDxxrL?8o3AjbiSi&=m~3PUi1CZpdo zpjr34e-bnM3&A|XqyG&eJi$`0&)nP30^eg^7WONQG=3Uo)qd5jgS3{_`*pWwX`|nO zK1}DbX20oVm2^I9^;_9Oe<54!FEURyc9-@%*_3q=z41j=)V>Q}v;OiKd0jM|eC3RM zPBfkT${G2*XgT>+oV)ObiAAyWUD&?{ZW-K)xB~7vxU1ll&SZ1FtKWZSn<=FYH@k~kzEFuMKl=i?pWd5lNj z0-1WdNXj1DVTO&61QG1|BHUx5@_jI6PXZoJ$%Ux5eM?K+C)1=eF-e-3MCuM1mq`D@tTQOGh7BiX_zSoe#%iQ(M*iiv4g zY-Jpq!ImO(Z6xynX#+LMZL{3YpT+3}&Z-*iSRjx2i4FUnT7mpvqx&r)r@K1VgRQu; z85;@a(2Xt_qr1n>IG!JSG=8a;aR#AVJBa%Y=CkJ8M&RV9q8eZmJua%Zv6zkOYj4LF z{uGg2^`7`VnEHroNN>b7>}}sZGU%eX4vKu3jK;Bo9|s?988g-oyWL^@RNmQ~B&nc? zc42c@$#Jp0pk!7Uxqh<*$<1~}wP1}R+iWnBMlI2*`PiNMjXSj+lrda#F`T`xzjjW< zlUy5MO}hA&GF z2p$sv-=5Dr2R;Pp-bdtf3NQgL!-wl2y1OM~p}Ru>4Y=$+Pf$7Hj>hXwSrAlQfT7?b zHu1qc1ZI=6BgRGP2u#bzVh0AguY7IJMg0f|+fB5=$N)^h^=|o5_heJ6q7g(1RT++U?)_sUWevC&`p}Sl# zMY1+Kk6hYP#p4q3cPxwtydnip*si~a_AJ;AjGX#U{heSM=)aj@Hw2o4p-a)!ANqTt z2u)=-+Vh@y`U~i*NF7su#;QiE9R0+ted;Q2{A=D$C?Zjvh6%&EV(QQ~rjKWXdrjg56$-fe5@eR`6becuMq5xINN ziZ!apxIOo=2g3X9IaPtziePJ(%ME-Gr>PXY0M2dW(fB1b3LY!F}!|z@^@!xzpki@{SRrNki2&q9=Ni(;A@KylN_~!Y2?bA z4^@$Inix=|@^jpwY0P;+_QiX^JOAa>P7j}ym+2-F_DDvqe1su+;tMq4Z3;F){eTbt312P%YNM{D-}JoqvUgy z7-{RrB3nI_Il$P)Nuw*jIOivMVkqYw`W7fZpQ7?yJ9jt_oQ9HXKX}InTsd8++#En{ zby$Acmu=nk0*6FGh3l%WWF1x%IXKf0Tx)P$q4s%Jh)F7cO`qDD`qeGg^lNyo1QA=V T;b|PcqgJ(5^WQe>{@eco^DlVb diff --git a/tests/utilities/__pycache__/test_patterns.cpython-39-pytest-7.1.3.pyc b/tests/utilities/__pycache__/test_patterns.cpython-39-pytest-7.1.3.pyc deleted file mode 100644 index 03827616f01ef85834ab013ce0295a873ae10fdb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2076 zcmb7FU60#D6rHi1#L0dLD3o1_Xh8+7`;c_Ev_h!qmh$mX!KxBMLMv3uoAG9?IkXdL=!pmiw6YCxIlX~1dW{SvP_htX`vp7SYIobsII zJY!QUx74ZTFD$`x>zE6R`j4%dBW!KIaAxdhrXC^R$k~j`Z{an`8(YYe&u3gXe_B`B z)TTY>cEsU3x`nKpJ2U>A|B5p^SZRtDyKm9Ptw?!%^47;Y>Dg^h-H6l;a_@MmXv$D! zv7TFnjU1Uf7n^}nq10x~U5r8J-FaIQzD4I@dZt6A{XiCP`hy^jeHo?_z@&i;i*=;^ zqb!PbG{(o{1S3?j<*Sn)@Ye>mwU^>ZW)vlKV1&)&6(W7)Qf)a2OLiIRG3Q<_}w|bu=@S^5|{e@ic98dBvpZQ$+XE($;vBa2>sG* z!z&)G0A%2+XgrMTdc&3b`lJqNfd!}?F0UhcJ*dAUOP~sCXc<`TOR(k!R>!~!_!c$U ztYQ78hV{u3tj!wM)&f>nv{!(&b0Dt~2A8nin_Pdoa|!HxIok7X%J(rzen5t(v@+C> zSAcp15s1>4P%FYZs5mYv0&HU41?1~oeu(H22aIk|zmdb2&=Zf*4>|nNzzq*_SVC`u(IxZ^LC`mh95xqn*j$oB2ZBj* z*g2SN-TDcYytDuL&i<48sq)2xp4YwQ?cbbiBco6Cf2BTqy7TI^+ts}MU(pCG$TjDG&kzuR@$MOXFw&wS#D<(5cWL$|OtuNU1EO z0##JBj63+@YCnSStru>k<#GcZ#kOCnf49_LC~{YZzsN{K$?LA7wdj*;H0N3+uH9Nz qyOpAv{}dyKrhE;d%+gC|G^aI^;ocu5B8$TZ9N#tD{ls<~^!o?8dLz~V diff --git a/tests/utilities/__pycache__/test_secureli_meta.cpython-39-pytest-7.1.3.pyc b/tests/utilities/__pycache__/test_secureli_meta.cpython-39-pytest-7.1.3.pyc deleted file mode 100644 index d50afd29685713fc1a8e4ac08d111d2cc3fecb83..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 934 zcmZ8fzi$&U6t;cI{YpYnC4@i>T|mlmnOLd{RUk3Z2!U9#Snk{n6-1YuNmCSB|pBEvG8M1GHDJc$7dWKyP+6eAJe^fpMOBI2Fzgm-!PrE`rZ{r~WY z#~vSW{2mRHUl0?Ghu9`(Y*yrFb^ZyQfm?-oU0j+*i6OCJ=>Z!m0Z5f&cWpbMCgf^( z1yer(vqm4G3QoS_E4)TGmHhuxznZ2|N_R z)V{%_gUO#T=wpxHktl#eZhDCx!^WU%v=s+$@D*9(HKAZZATnM(-9^gw8Du-C4gQvy z`7lsUdKH7Q{kAdmg2_ez;}tmc3VaY)nw?1%SkylaT(Z6rIRg#AVL}FYNo_zRTyEDJ!GRDmcnJPGE;WY zRE6<(T{6B(8@9CVZI5}zX=