From 4aea1d38babff9bc33e9cb4cb967ef09b573a42e Mon Sep 17 00:00:00 2001 From: Hartono Ramli Date: Tue, 27 Jun 2023 11:38:25 -0700 Subject: [PATCH] feat: adding optional directory command for init, scan, and update (#147) This will add the ability to the 3 commands `init` `scan` `update` with `--directory` example: `secureli scan -m all-files --directory ~/src/testrepo` --- secureli/abstractions/pre_commit.py | 33 +++++++----- secureli/actions/action.py | 32 +++++++---- secureli/actions/build.py | 3 +- secureli/actions/initializer.py | 4 +- secureli/actions/scan.py | 3 +- secureli/actions/update.py | 15 +++--- secureli/main.py | 26 +++++++-- secureli/repositories/secureli_config.py | 16 +++--- secureli/services/language_support.py | 5 +- secureli/services/logging.py | 26 +++++---- secureli/services/updater.py | 19 ++++--- secureli/utilities/git_meta.py | 10 ++-- tests/abstractions/test_pre_commit.py | 63 ++++++++++++---------- tests/actions/test_action.py | 6 +-- tests/actions/test_initializer_action.py | 8 ++- tests/actions/test_update_action.py | 11 ++-- tests/application/test_main.py | 6 +-- tests/repositories/test_secureli_config.py | 13 +++-- tests/services/test_language_support.py | 7 ++- tests/services/test_logging_service.py | 10 ++-- tests/services/test_updater_service.py | 13 +++-- tests/utilities/test_git_meta.py | 11 ++-- 22 files changed, 214 insertions(+), 126 deletions(-) diff --git a/secureli/abstractions/pre_commit.py b/secureli/abstractions/pre_commit.py index ba657094..ddf115f4 100644 --- a/secureli/abstractions/pre_commit.py +++ b/secureli/abstractions/pre_commit.py @@ -189,15 +189,14 @@ def secret_detection_hook_id(self, language: str) -> Optional[str]: return None - def install(self, language: str) -> InstallResult: + def install(self, folder_path: Path, language: str) -> InstallResult: """ Identifies the template we hold for the specified language, writes it, installs it, and cleans up :param language: The language to identify a template for :raises LanguageNotSupportedError if a pre-commit template cannot be found for the specified language :raises InstallFailedError if the template was found, but an error occurred installing it """ - - path_to_pre_commit_file = Path(".pre-commit-config.yaml") + path_to_pre_commit_file = Path(folder_path / ".pre-commit-config.yaml") # Raises a LanguageNotSupportedError if language doesn't resolve to a yaml file language_config = self._get_language_config(language) @@ -205,13 +204,13 @@ def install(self, language: str) -> InstallResult: with open(path_to_pre_commit_file, "w") as f: f.write(language_config.config_data) - completed_process = subprocess.run(["pre-commit", "install"]) + completed_process = subprocess.run(["pre-commit", "install"], cwd=folder_path) if completed_process.returncode != 0: raise InstallFailedError( f"Installing the pre-commit script for {language} failed" ) - install_configs_result = self._install_pre_commit_configs(language) + install_configs_result = self._install_pre_commit_configs(folder_path, language) return InstallResult( successful=True, @@ -327,6 +326,7 @@ def execute_hooks( def autoupdate_hooks( self, + folder_path: Path, bleeding_edge: bool = False, freeze: bool = False, repos: Optional[list] = None, @@ -334,6 +334,7 @@ def autoupdate_hooks( """ Updates the precommit hooks but executing precommit's autoupdate command. Additional info at https://pre-commit.com/#pre-commit-autoupdate + :param folder path: specified full path directory (default to current directory) :param bleeding edge: True if updating to the bleeding edge of the default branch instead of the latest tagged version (which is the default behavior) :param freeze: Set to True to store "frozen" hashes in rev instead of tag names. @@ -367,7 +368,9 @@ def autoupdate_hooks( subprocess_args.extend(repo_args) - completed_process = subprocess.run(subprocess_args, stdout=subprocess.PIPE) + completed_process = subprocess.run( + subprocess_args, cwd=folder_path, stdout=subprocess.PIPE + ) output = ( completed_process.stdout.decode("utf8") if completed_process.stdout else "" ) @@ -376,14 +379,16 @@ def autoupdate_hooks( else: return ExecuteResult(successful=True, output=output) - def update(self) -> ExecuteResult: + def update(self, folder_path: Path) -> ExecuteResult: """ Installs the hooks defined in pre-commit-config.yml. :return: ExecuteResult, indicating success or failure. """ subprocess_args = ["pre-commit", "install-hooks", "--color", "always"] - completed_process = subprocess.run(subprocess_args, stdout=subprocess.PIPE) + completed_process = subprocess.run( + subprocess_args, cwd=folder_path, stdout=subprocess.PIPE + ) output = ( completed_process.stdout.decode("utf8") if completed_process.stdout else "" ) @@ -392,7 +397,7 @@ def update(self) -> ExecuteResult: else: return ExecuteResult(successful=True, output=output) - def remove_unused_hooks(self) -> ExecuteResult: + def remove_unused_hooks(self, folder_path: Path) -> ExecuteResult: """ Removes unused hook repos from the cache. Pre-commit determines which flags are "unused" by comparing the repos to the pre-commit-config.yaml file. Any cached hook repos that are not in the config file @@ -401,7 +406,9 @@ def remove_unused_hooks(self) -> ExecuteResult: """ subprocess_args = ["pre-commit", "gc", "--color", "always"] - completed_process = subprocess.run(subprocess_args, stdout=subprocess.PIPE) + completed_process = subprocess.run( + subprocess_args, cwd=folder_path, stdout=subprocess.PIPE + ) output = ( completed_process.stdout.decode("utf8") if completed_process.stdout else "" ) @@ -819,7 +826,7 @@ def _load_language_config_file(self, language: str) -> LoadLanguageConfigsResult return LoadLanguageConfigsResult(success=False, config_data=list()) def _install_pre_commit_configs( - self, language: str + self, folder_path: Path, language: str ) -> LanguagePreCommitConfigInstallResult: """ Install any config files for given language to support any pre-commit commands. @@ -841,13 +848,13 @@ def _install_pre_commit_configs( try: for key in config: config_name = f"{slugify(language)}.{key}.yaml" - path_to_config_file = Path(f".secureli/{config_name}") + path_to_config_file = folder_path / f".secureli/{config_name}" with open(path_to_config_file, "w") as f: f.write(yaml.dump(config[key])) completed_process = subprocess.run( - ["pre-commit", "install-language-config"] + ["pre-commit", "install-language-config"], cwd=folder_path ) if completed_process.returncode != 0: diff --git a/secureli/actions/action.py b/secureli/actions/action.py index 82f19d62..0c692663 100644 --- a/secureli/actions/action.py +++ b/secureli/actions/action.py @@ -87,7 +87,11 @@ def verify_install( :param always_yes: Assume "Yes" to all prompts """ - config = SecureliConfig() if reset else self.action_deps.secureli_config.load() + config = ( + SecureliConfig() + if reset + else self.action_deps.secureli_config.load(folder_path=folder_path) + ) if not config.overall_language or not config.version_installed: return self._install_secureli(folder_path, always_yes) @@ -98,7 +102,9 @@ def verify_install( # Check for a new version and prompt for upgrade if available if available_version != config.version_installed: - return self._upgrade_secureli(config, available_version, always_yes) + return self._upgrade_secureli( + folder_path, config, available_version, always_yes + ) # Validates the current .pre-commit-config.yaml against the generated config config_validation_result = self.action_deps.pre_commit.validate_config( @@ -108,7 +114,7 @@ def verify_install( # If config mismatch between available version and current version prompt for upgrade if not config_validation_result.successful: self.action_deps.echo.print(config_validation_result.output) - return self._update_secureli(always_yes) + return self._update_secureli(folder_path, always_yes) self.action_deps.echo.print( f"SeCureLI is installed and up-to-date (language = {config.overall_language})" @@ -119,7 +125,11 @@ def verify_install( ) def _upgrade_secureli( - self, config: SecureliConfig, available_version: str, always_yes: bool + self, + folder_path: Path, + config: SecureliConfig, + available_version: str, + always_yes: bool, ) -> VerifyResult: """ Installs SeCureLI into the given folder path and returns the new configuration @@ -144,12 +154,12 @@ def _upgrade_secureli( try: metadata = self.action_deps.language_support.apply_support( - config.overall_language + folder_path, config.overall_language ) # Update config with new version installed and save it config.version_installed = metadata.version - self.action_deps.secureli_config.save(config) + self.action_deps.secureli_config.save(folder_path, config) self.action_deps.echo.print("SeCureLI has been upgraded successfully") return VerifyResult( outcome=VerifyOutcome.UPGRADE_SUCCEEDED, @@ -205,7 +215,9 @@ def _install_secureli(self, folder_path: Path, always_yes: bool) -> VerifyResult f"Overall Detected Language: {overall_language}" ) - metadata = self.action_deps.language_support.apply_support(overall_language) + metadata = self.action_deps.language_support.apply_support( + folder_path, overall_language + ) except (ValueError, LanguageNotSupportedError, InstallFailedError) as e: self.action_deps.echo.error( @@ -219,7 +231,7 @@ def _install_secureli(self, folder_path: Path, always_yes: bool) -> VerifyResult overall_language=overall_language, version_installed=metadata.version, ) - self.action_deps.secureli_config.save(config) + self.action_deps.secureli_config.save(folder_path, config) if secret_test_id := metadata.security_hook_id: self.action_deps.echo.print( @@ -243,7 +255,7 @@ def _install_secureli(self, folder_path: Path, always_yes: bool) -> VerifyResult analyze_result=analyze_result, ) - def _update_secureli(self, always_yes: bool): + def _update_secureli(self, folder_path: Path, always_yes: bool): """ Prompts the user to update to the latest secureli install. :param always_yes: Assume "Yes" to all prompts @@ -260,7 +272,7 @@ def _update_secureli(self, always_yes: bool): self.action_deps.echo.print("\nUpdate declined.\n") return VerifyResult(outcome=VerifyOutcome.UPDATE_CANCELED) - update_result = self.action_deps.updater.update() + update_result = self.action_deps.updater.update(folder_path) details = update_result.output self.action_deps.echo.print(details) diff --git a/secureli/actions/build.py b/secureli/actions/build.py index 7a49b961..4e1b039e 100644 --- a/secureli/actions/build.py +++ b/secureli/actions/build.py @@ -1,3 +1,4 @@ +from pathlib import Path from secureli.abstractions.echo import EchoAbstraction, Color from secureli.services.logging import LoggingService, LogAction @@ -20,4 +21,4 @@ def print_build(self, color: Color): """ self.echo.print(self.build_data, color=color, bold=True) - self.logging.success(LogAction.build) + self.logging.success(Path("."), LogAction.build) diff --git a/secureli/actions/initializer.py b/secureli/actions/initializer.py index d02010c9..fcf12f08 100644 --- a/secureli/actions/initializer.py +++ b/secureli/actions/initializer.py @@ -25,6 +25,6 @@ def initialize_repo(self, folder_path: Path, reset: bool, always_yes: bool): """ verify_result = self.verify_install(folder_path, reset, always_yes) if verify_result.outcome in ScanAction.halting_outcomes: - self.logging.failure(LogAction.init, verify_result.outcome) + self.logging.failure(folder_path, LogAction.init, verify_result.outcome) else: - self.logging.success(LogAction.init) + self.logging.success(folder_path, LogAction.init) diff --git a/secureli/actions/scan.py b/secureli/actions/scan.py index 764c85c8..3d06d2ac 100644 --- a/secureli/actions/scan.py +++ b/secureli/actions/scan.py @@ -85,6 +85,7 @@ def scan_repo( if not scan_result.successful: log_data = self.logging.failure( + folder_path, LogAction.scan, scan_result_failures_json_string, failure_count, @@ -94,7 +95,7 @@ def scan_repo( post_log(log_data.json(exclude_none=True)) else: self.echo.print("Scan executed successfully and detected no issues!") - log_data = self.logging.success(LogAction.scan) + log_data = self.logging.success(folder_path, LogAction.scan) post_log(log_data.json(exclude_none=True)) diff --git a/secureli/actions/update.py b/secureli/actions/update.py index c2a5bb25..bf246e1e 100644 --- a/secureli/actions/update.py +++ b/secureli/actions/update.py @@ -1,5 +1,6 @@ from typing import Optional +from pathlib import Path from secureli.abstractions.echo import EchoAbstraction from secureli.services.logging import LoggingService, LogAction from secureli.services.updater import UpdaterService @@ -19,7 +20,7 @@ def __init__( self.logging = logging self.updater = updater - def update_hooks(self, latest: Optional[bool] = False): + def update_hooks(self, folder_path: Path, latest: Optional[bool] = False): """ Installs the hooks defined in pre-commit-config.yml. :param latest: Indicates whether you want to update to the latest versions @@ -28,7 +29,7 @@ def update_hooks(self, latest: Optional[bool] = False): """ if latest: self.echo.print("Updating hooks to the latest version...") - update_result = self.updater.update_hooks() + update_result = self.updater.update_hooks(folder_path) details = ( update_result.output or "Unknown output while updating hooks to latest version" @@ -36,18 +37,18 @@ def update_hooks(self, latest: Optional[bool] = False): self.echo.print(details) if not update_result.successful: self.echo.print(details) - self.logging.failure(LogAction.update, details) + self.logging.failure(folder_path, LogAction.update, details) else: self.echo.print("Hooks successfully updated to latest version") - self.logging.success(LogAction.update) + self.logging.success(folder_path, LogAction.update) else: self.echo.print("Beginning update...") - install_result = self.updater.update() + install_result = self.updater.update(folder_path) details = install_result.output or "Unknown output during hook installation" self.echo.print(details) if not install_result.successful: self.echo.print(details) - self.logging.failure(LogAction.update, details) + self.logging.failure(folder_path, LogAction.update, details) else: self.echo.print("Update executed successfully.") - self.logging.success(LogAction.update) + self.logging.success(folder_path, LogAction.update) diff --git a/secureli/main.py b/secureli/main.py index 423a81ca..189d93d7 100644 --- a/secureli/main.py +++ b/secureli/main.py @@ -48,11 +48,17 @@ def init( "-y", help="Say 'yes' to every prompt automatically without input", ), + directory: Optional[str] = Option( + ".", + "--directory", + "-d", + help="Run seCureLI on specified full path directory (default to current directory)", + ), ): """ Detect languages and initialize pre-commit hooks and linters for the project """ - container.initializer_action().initialize_repo(Path("."), reset, yes) + container.initializer_action().initialize_repo(Path(directory), reset, yes) @app.command() @@ -75,11 +81,17 @@ def scan( "-t", help="Limit the scan to a specific hook ID from your pre-commit config", ), + directory: Optional[str] = Option( + ".", + "--directory", + "-d", + help="Run seCureLI on specified full path directory (default to current directory)", + ), ): """ Performs an explicit check of the repository to detect security issues without remote logging. """ - container.scan_action().scan_repo(Path("."), mode, yes, specific_test) + container.scan_action().scan_repo(Path(directory), mode, yes, specific_test) @app.command(hidden=True) @@ -97,12 +109,18 @@ def update( "--latest", "-l", help="Update the installed pre-commit hooks to their latest versions", - ) + ), + directory: Optional[str] = Option( + ".", + "--directory", + "-d", + help="Run seCureLI on specified full path directory (default to current directory)", + ), ): """ Update linters, configuration, and all else needed to maintain a secure repository. """ - container.update_action().update_hooks(latest) + container.update_action().update_hooks(Path(directory), latest) if __name__ == "__main__": diff --git a/secureli/repositories/secureli_config.py b/secureli/repositories/secureli_config.py index c5b65876..a62fee6c 100644 --- a/secureli/repositories/secureli_config.py +++ b/secureli/repositories/secureli_config.py @@ -13,23 +13,24 @@ class SecureliConfig(BaseModel): class SecureliConfigRepository: """Save and retrieve the SeCureLI configuration""" - def save(self, secureli_config: SecureliConfig): + def save(self, folder_path: Path, secureli_config: SecureliConfig): """ Save the specified configuration to the .secureli folder :param secureli_config: The populated configuration to save """ - secureli_folder_path = self._initialize_secureli_directory() + secureli_folder_path = self._initialize_secureli_directory(folder_path) secureli_config_path = secureli_folder_path / "repo-config.yaml" with open(secureli_config_path, "w") as f: yaml.dump(secureli_config.dict(), f) - def load(self) -> SecureliConfig: + def load(self, folder_path: Path) -> SecureliConfig: """ Load the SeCureLI config from the expected configuration file path or return a new configuration object, capable of being modified and saved via the `save` method """ - secureli_folder_path = self._initialize_secureli_directory() - secureli_config_path = secureli_folder_path / "repo-config.yaml" + secureli_folder_path = self._initialize_secureli_directory(folder_path) + secureli_config_path = Path(secureli_folder_path / "repo-config.yaml") + if not secureli_config_path.exists(): return SecureliConfig() @@ -37,11 +38,12 @@ def load(self) -> SecureliConfig: data = yaml.safe_load(f) return SecureliConfig.parse_obj(data) - def _initialize_secureli_directory(self): + def _initialize_secureli_directory(self, folder_path: Path): """ Creates the .secureli folder within the current directory if needed. :return: The folder path of the .secureli folder that either exists or was just created. """ - secureli_folder_path = Path(".") / ".secureli" + secureli_folder_path = Path(folder_path / ".secureli") + secureli_folder_path.mkdir(parents=True, exist_ok=True) return secureli_folder_path diff --git a/secureli/services/language_support.py b/secureli/services/language_support.py index 70d876b9..3b5b0eaa 100644 --- a/secureli/services/language_support.py +++ b/secureli/services/language_support.py @@ -2,6 +2,7 @@ import pydantic +from pathlib import Path from secureli.abstractions.pre_commit import PreCommitAbstraction from secureli.services.git_ignore import GitIgnoreService @@ -46,7 +47,7 @@ def version_for_language(self, language: str) -> str: # For now, just a passthrough to pre-commit hook abstraction return self.pre_commit_hook.version_for_language(language) - def apply_support(self, language: str) -> LanguageMetadata: + def apply_support(self, folder_path: Path, language: str) -> LanguageMetadata: """ Applies Secure Build support for the provided language :param language: The language to provide support for @@ -56,7 +57,7 @@ def apply_support(self, language: str) -> LanguageMetadata: """ # Start by identifying and installing the appropriate pre-commit template (if we have one) - install_result = self.pre_commit_hook.install(language) + install_result = self.pre_commit_hook.install(folder_path, language) # Add .secureli/ to the gitignore folder if needed self.git_ignore.ignore_secureli_files() diff --git a/secureli/services/logging.py b/secureli/services/logging.py index cf9ed7f1..4cf240bc 100644 --- a/secureli/services/logging.py +++ b/secureli/services/logging.py @@ -13,12 +13,12 @@ from secureli.utilities.secureli_meta import secureli_version -def generate_unique_id() -> str: +def generate_unique_id(folder_path: Path) -> str: """ A unique identifier representing the log entry, including various bits specific to the user and environment """ - origin_email_branch = f"{origin_url()}|{git_user_email()}|{current_branch_name()}" + origin_email_branch = f"{origin_url(folder_path)}|{git_user_email()}|{current_branch_name(folder_path)}" return f"{uuid4()}|{origin_email_branch}" @@ -47,7 +47,7 @@ class LogFailure(pydantic.BaseModel): class LogEntry(pydantic.BaseModel): """A distinct entry in the log captured following actions like scan and init""" - id: str = generate_unique_id() + id: str timestamp: datetime = datetime.utcnow() username: str = git_user_email() machineid: str = platform.uname().node @@ -72,29 +72,32 @@ def __init__( self.pre_commit = pre_commit self.secureli_config = secureli_config - def success(self, action: LogAction) -> LogEntry: + def success(self, folder_path: Path, action: LogAction) -> LogEntry: """ Capture that a successful conclusion has been reached for an action :param action: The action that succeeded """ - secureli_config = self.secureli_config.load() + secureli_config = self.secureli_config.load(folder_path=folder_path) hook_config = ( self.pre_commit.get_configuration(secureli_config.overall_language) if secureli_config.overall_language else None ) log_entry = LogEntry( + id=generate_unique_id(folder_path), status=LogStatus.success, action=action, hook_config=hook_config, primary_language=secureli_config.overall_language, ) - self._log(log_entry) + + self._log(folder_path, log_entry) return log_entry def failure( self, + folder_path: Path, action: LogAction, details: str, total_failure_count: Optional[int], @@ -105,13 +108,14 @@ def failure( :param action: The action that failed :param details: Details about the failure """ - secureli_config = self.secureli_config.load() + secureli_config = self.secureli_config.load(folder_path=folder_path) hook_config = ( None if not secureli_config.overall_language else self.pre_commit.get_configuration(secureli_config.overall_language) ) log_entry = LogEntry( + id=generate_unique_id(folder_path), status=LogStatus.failure, action=action, failure=LogFailure( @@ -122,14 +126,14 @@ def failure( hook_config=hook_config, primary_language=secureli_config.overall_language, ) - self._log(log_entry) + self._log(folder_path, log_entry) return log_entry - def _log(self, log_entry: LogEntry): + def _log(self, folder_path: Path, log_entry: LogEntry): """Commit a log entry to the branch log file""" - log_folder_path = Path(f".secureli/logs") - path_to_log = log_folder_path / f"{current_branch_name()}" + log_folder_path = Path(folder_path / ".secureli/logs") + path_to_log = log_folder_path / f"{current_branch_name(folder_path)}" # Do not simply mkdir the log folder path, in case the branch name contains # additional folder structure, like `bugfix/` or `feature/` diff --git a/secureli/services/updater.py b/secureli/services/updater.py index 8c64a407..4471532f 100644 --- a/secureli/services/updater.py +++ b/secureli/services/updater.py @@ -2,6 +2,7 @@ import pydantic +from pathlib import Path from secureli.abstractions.pre_commit import PreCommitAbstraction from secureli.repositories.secureli_config import SecureliConfigRepository @@ -30,6 +31,7 @@ def __init__( def update_hooks( self, + folder_path: Path, bleeding_edge: bool = False, freeze: bool = False, repos: Optional[list] = None, @@ -37,39 +39,42 @@ def update_hooks( """ Updates the precommit hooks but executing precommit's autoupdate command. Additional info at https://pre-commit.com/#pre-commit-autoupdate + :param folder path: specified full path directory (default to current directory) :param bleeding edge: True if updating to the bleeding edge of the default branch instead of the latest tagged version (which is the default behavior) :param freeze: Set to True to store "frozen" hashes in rev instead of tag names. :param repos: Dectionary of repos to update. This is used to target specific repos instead of all repos. :return: ExecuteResult, indicating success or failure. """ - update_result = self.pre_commit.autoupdate_hooks(bleeding_edge, freeze, repos) + update_result = self.pre_commit.autoupdate_hooks( + folder_path, bleeding_edge, freeze, repos + ) output = update_result.output if update_result.successful and not output: output = "No changes necessary.\n" if update_result.successful and update_result.output: - prune_result = self.pre_commit.remove_unused_hooks() + prune_result = self.pre_commit.remove_unused_hooks(folder_path) output = output + "\nRemoving unused environments:\n" + prune_result.output return UpdateResult(successful=update_result.successful, output=output) - def update(self): + def update(self, folder_path: Path): """ Updates secureli with the latest local configuration. :return: ExecuteResult, indicating success or failure. """ - secureli_config = self.config.load() + secureli_config = self.config.load(folder_path) output = "Updating .pre-commit-config.yaml...\n" install_result = self.pre_commit.install( - language=secureli_config.overall_language + folder_path=folder_path, language=secureli_config.overall_language ) if not install_result.successful: output += "Failed to update .pre-commit-config.yaml prior to hook install\n" return UpdateResult(successful=install_result.successful, output=output) - hook_install_result = self.pre_commit.update() + hook_install_result = self.pre_commit.update(folder_path) output += hook_install_result.output if ( @@ -79,7 +84,7 @@ def update(self): output += "No changes necessary.\n" if hook_install_result.successful and hook_install_result.output: - prune_result = self.pre_commit.remove_unused_hooks() + prune_result = self.pre_commit.remove_unused_hooks(folder_path) output += "\nRemoving unused environments:\n" + prune_result.output return UpdateResult(successful=hook_install_result.successful, output=output) diff --git a/secureli/utilities/git_meta.py b/secureli/utilities/git_meta.py index 87e2d15d..5f482185 100644 --- a/secureli/utilities/git_meta.py +++ b/secureli/utilities/git_meta.py @@ -1,3 +1,5 @@ +from pathlib import Path + import subprocess import configparser @@ -10,10 +12,10 @@ def git_user_email() -> str: return output -def origin_url() -> str: +def origin_url(folder_path: Path) -> str: """Leverage the git config file to determine the remote origin URL""" git_config_parser = configparser.ConfigParser() - git_config_parser.read(".git/config") + git_config_parser.read(f"{folder_path}/.git/config") return ( git_config_parser['remote "origin"'].get("url", "UNKNOWN") if git_config_parser.has_section('remote "origin"') @@ -21,10 +23,10 @@ def origin_url() -> str: ) -def current_branch_name() -> str: +def current_branch_name(folder_path: Path) -> str: """Leverage the git HEAD file to determine the current branch name""" try: - with open(".git/HEAD", "r") as f: + with open(f"{folder_path}/.git/HEAD", "r") as f: content = f.readlines() for line in content: if line[0:4] == "ref:": diff --git a/tests/abstractions/test_pre_commit.py b/tests/abstractions/test_pre_commit.py index 6b0fec41..f5f64d7b 100644 --- a/tests/abstractions/test_pre_commit.py +++ b/tests/abstractions/test_pre_commit.py @@ -1,3 +1,4 @@ +from pathlib import Path from subprocess import CompletedProcess from unittest.mock import MagicMock @@ -16,6 +17,8 @@ PreCommitHook, ) +test_folder_path = Path(".") + @pytest.fixture() def settings_dict() -> dict: @@ -109,9 +112,11 @@ def test_that_pre_commit_templates_are_loaded_for_supported_languages( pre_commit: PreCommitAbstraction, mock_subprocess: MagicMock, ): - pre_commit.install("Python") + pre_commit.install(test_folder_path, "Python") - mock_subprocess.run.assert_called_with(["pre-commit", "install"]) + mock_subprocess.run.assert_called_with( + ["pre-commit", "install"], cwd=test_folder_path + ) def test_that_pre_commit_templates_are_loaded_with_global_exclude_if_provided( @@ -121,7 +126,7 @@ def test_that_pre_commit_templates_are_loaded_with_global_exclude_if_provided( ): mock_data_loader.return_value = "yaml: data" pre_commit.ignored_file_patterns = ["mock_pattern"] - pre_commit.install("Python") + pre_commit.install(test_folder_path, "Python") assert ( "exclude: mock_pattern" @@ -136,7 +141,7 @@ def test_that_pre_commit_templates_are_loaded_without_exclude( ): mock_data_loader.return_value = "yaml: data" pre_commit.ignored_file_patterns = [] - pre_commit.install("Python") + pre_commit.install(test_folder_path, "Python") assert "exclude:" not in mock_open.return_value.write.call_args_list[0].args[0] @@ -151,7 +156,7 @@ def test_that_pre_commit_templates_are_loaded_with_global_exclude_if_provided_mu "mock_pattern1", "mock_pattern2", ] - pre_commit.install("Python") + pre_commit.install(test_folder_path, "Python") assert ( "exclude: ^(mock_pattern1|mock_pattern2)" @@ -175,7 +180,7 @@ def test_that_pre_commit_treats_missing_templates_as_unsupported_language( ): mock_data_loader.side_effect = ValueError with pytest.raises(LanguageNotSupportedError): - pre_commit.install("BadLang") + pre_commit.install(test_folder_path, "BadLang") def test_that_pre_commit_treats_missing_templates_as_unsupported_language_when_checking_versions( @@ -195,7 +200,7 @@ def test_that_pre_commit_treats_failing_process_as_install_failed_error( ): mock_subprocess.run.return_value = CompletedProcess(args=[], returncode=1) with pytest.raises(InstallFailedError): - pre_commit.install("Python") + pre_commit.install(test_folder_path, "Python") def test_that_pre_commit_executes_hooks_successfully( @@ -293,7 +298,7 @@ def mock_loader_side_effect(resource): mock_data_loader.side_effect = mock_loader_side_effect - pre_commit.install("RadLang") + pre_commit.install(test_folder_path, "RadLang") assert "arg_a" in mock_open.return_value.write.call_args_list[0].args[0] assert "value_a" in mock_open.return_value.write.call_args_list[0].args[0] @@ -329,7 +334,7 @@ def mock_loader_side_effect(resource): mock_data_loader.side_effect = mock_loader_side_effect - pre_commit.install("RadLang") + pre_commit.install(test_folder_path, "RadLang") assert "arg_a" not in mock_open.return_value.write.call_args_list[0].args[0] assert "value_a" not in mock_open.return_value.write.call_args_list[0].args[0] @@ -363,7 +368,7 @@ def mock_loader_side_effect(resource): mock_data_loader.side_effect = mock_loader_side_effect - pre_commit.install("RadLang") + pre_commit.install(test_folder_path, "RadLang") assert "arg_a" in mock_open.return_value.write.call_args_list[0].args[0] assert "value_a" in mock_open.return_value.write.call_args_list[0].args[0] @@ -395,7 +400,7 @@ def mock_loader_side_effect(resource): mock_data_loader.side_effect = mock_loader_side_effect - pre_commit.install("RadLang") + pre_commit.install(test_folder_path, "RadLang") assert "arg_a" in mock_open.return_value.write.call_args_list[0].args[0] assert "value_a" in mock_open.return_value.write.call_args_list[0].args[0] @@ -750,7 +755,7 @@ def test_that_pre_commit_autoupdate_hooks_executes_successfully( mock_subprocess: MagicMock, ): mock_subprocess.run.return_value = CompletedProcess(args=[], returncode=0) - execute_result = pre_commit.autoupdate_hooks() + execute_result = pre_commit.autoupdate_hooks(test_folder_path) assert execute_result.successful @@ -760,7 +765,7 @@ def test_that_pre_commit_autoupdate_hooks_properly_handles_failed_executions( mock_subprocess: MagicMock, ): mock_subprocess.run.return_value = CompletedProcess(args=[], returncode=1) - execute_result = pre_commit.autoupdate_hooks() + execute_result = pre_commit.autoupdate_hooks(test_folder_path) assert not execute_result.successful @@ -770,7 +775,7 @@ def test_that_pre_commit_autoupdate_hooks_executes_successfully_with_bleeding_ed mock_subprocess: MagicMock, ): mock_subprocess.run.return_value = CompletedProcess(args=[], returncode=0) - execute_result = pre_commit.autoupdate_hooks(bleeding_edge=True) + execute_result = pre_commit.autoupdate_hooks(test_folder_path, bleeding_edge=True) assert execute_result.successful assert "--bleeding-edge" in mock_subprocess.run.call_args_list[0].args[0] @@ -781,7 +786,7 @@ def test_that_pre_commit_autoupdate_hooks_executes_successfully_with_freeze( mock_subprocess: MagicMock, ): mock_subprocess.run.return_value = CompletedProcess(args=[], returncode=0) - execute_result = pre_commit.autoupdate_hooks(freeze=True) + execute_result = pre_commit.autoupdate_hooks(test_folder_path, freeze=True) assert execute_result.successful assert "--freeze" in mock_subprocess.run.call_args_list[0].args[0] @@ -793,7 +798,7 @@ def test_that_pre_commit_autoupdate_hooks_executes_successfully_with_repos( ): test_repos = ["some-repo-url"] mock_subprocess.run.return_value = CompletedProcess(args=[], returncode=0) - execute_result = pre_commit.autoupdate_hooks(repos=test_repos) + execute_result = pre_commit.autoupdate_hooks(test_folder_path, repos=test_repos) assert execute_result.successful assert "--repo some-repo-url" in mock_subprocess.run.call_args_list[0].args[0] @@ -805,7 +810,7 @@ def test_that_pre_commit_autoupdate_hooks_executes_successfully_with_multiple_re ): test_repos = ["some-repo-url", "some-other-repo-url"] mock_subprocess.run.return_value = CompletedProcess(args=[], returncode=0) - execute_result = pre_commit.autoupdate_hooks(repos=test_repos) + execute_result = pre_commit.autoupdate_hooks(test_folder_path, repos=test_repos) assert execute_result.successful assert "--repo some-repo-url" in mock_subprocess.run.call_args_list[0].args[0] @@ -818,7 +823,7 @@ def test_that_pre_commit_autoupdate_hooks_fails_with_repos_containing_non_string ): test_repos = [{"something": "something-else"}] mock_subprocess.run.return_value = CompletedProcess(args=[], returncode=0) - execute_result = pre_commit.autoupdate_hooks(repos=test_repos) + execute_result = pre_commit.autoupdate_hooks(test_folder_path, repos=test_repos) assert not execute_result.successful @@ -830,7 +835,7 @@ def test_that_pre_commit_autoupdate_hooks_ignores_repos_when_repos_is_a_dict( test_repos = {} test_repos_string = "string" mock_subprocess.run.return_value = CompletedProcess(args=[], returncode=0) - execute_result = pre_commit.autoupdate_hooks(repos=test_repos) + execute_result = pre_commit.autoupdate_hooks(test_folder_path, repos=test_repos) assert execute_result.successful assert "--repo {}" not in mock_subprocess.run.call_args_list[0].args[0] @@ -842,7 +847,7 @@ def test_that_pre_commit_autoupdate_hooks_converts_repos_when_repos_is_a_string( ): test_repos = "string" mock_subprocess.run.return_value = CompletedProcess(args=[], returncode=0) - execute_result = pre_commit.autoupdate_hooks(repos=test_repos) + execute_result = pre_commit.autoupdate_hooks(test_folder_path, repos=test_repos) assert execute_result.successful assert "--repo string" in mock_subprocess.run.call_args_list[0].args[0] @@ -854,7 +859,7 @@ def test_that_pre_commit_update_executes_successfully( mock_subprocess: MagicMock, ): mock_subprocess.run.return_value = CompletedProcess(args=[], returncode=0) - execute_result = pre_commit.update() + execute_result = pre_commit.update(test_folder_path) assert execute_result.successful @@ -864,7 +869,7 @@ def test_that_pre_commit_update_properly_handles_failed_executions( mock_subprocess: MagicMock, ): mock_subprocess.run.return_value = CompletedProcess(args=[], returncode=1) - execute_result = pre_commit.update() + execute_result = pre_commit.update(test_folder_path) assert not execute_result.successful @@ -875,7 +880,7 @@ def test_that_pre_commit_remove_unused_hookss_executes_successfully( mock_subprocess: MagicMock, ): mock_subprocess.run.return_value = CompletedProcess(args=[], returncode=0) - execute_result = pre_commit.remove_unused_hooks() + execute_result = pre_commit.remove_unused_hooks(test_folder_path) assert execute_result.successful @@ -885,7 +890,7 @@ def test_that_pre_commit_remove_unused_hooks_properly_handles_failed_executions( mock_subprocess: MagicMock, ): mock_subprocess.run.return_value = CompletedProcess(args=[], returncode=1) - execute_result = pre_commit.remove_unused_hooks() + execute_result = pre_commit.remove_unused_hooks(test_folder_path) assert not execute_result.successful @@ -911,9 +916,11 @@ def test_that_pre_commit_language_config_does_not_get_loaded( def test_that_pre_commit_language_config_gets_installed( pre_commit: PreCommitAbstraction, mock_subprocess: MagicMock ): - result = pre_commit._install_pre_commit_configs("JavaScript") + result = pre_commit._install_pre_commit_configs(test_folder_path, "JavaScript") - mock_subprocess.run.assert_called_with(["pre-commit", "install-language-config"]) + mock_subprocess.run.assert_called_with( + ["pre-commit", "install-language-config"], cwd=test_folder_path + ) assert result.num_successful > 0 assert result.num_non_success == 0 @@ -923,7 +930,7 @@ def test_that_pre_commit_language_config_gets_installed( def test_that_pre_commit_language_config_does_not_get_installed( pre_commit: PreCommitAbstraction, mock_subprocess: MagicMock ): - result = pre_commit._install_pre_commit_configs("RadLang") + result = pre_commit._install_pre_commit_configs(test_folder_path, "RadLang") assert not mock_subprocess.called @@ -937,7 +944,7 @@ def test_that_pre_commit_install_captures_error_if_cannot_install_config( ): mock_subprocess.run.return_value = CompletedProcess(args=[], returncode=1) - result = pre_commit._install_pre_commit_configs("JavaScript") + result = pre_commit._install_pre_commit_configs(test_folder_path, "JavaScript") assert result.num_successful == 0 assert result.num_non_success > 0 diff --git a/tests/actions/test_action.py b/tests/actions/test_action.py index 3582ae74..c36dd22b 100644 --- a/tests/actions/test_action.py +++ b/tests/actions/test_action.py @@ -11,7 +11,7 @@ from secureli.services.updater import UpdateResult from secureli.abstractions.pre_commit import ValidateConfigResult -test_folder_path = Path("does-not-matter") +test_folder_path = Path(".") @pytest.fixture() @@ -280,7 +280,7 @@ def test_that_update_secureli_handles_declined_update( mock_echo: MagicMock, ): mock_echo.confirm.return_value = False - update_result = action._update_secureli(always_yes=False) + update_result = action._update_secureli(test_folder_path, always_yes=False) assert update_result.outcome == "update-canceled" @@ -292,6 +292,6 @@ def test_that_update_secureli_handles_failed_update( mock_updater.update.return_value = UpdateResult( successful=False, outcome="update failed" ) - update_result = action._update_secureli(always_yes=False) + update_result = action._update_secureli(test_folder_path, always_yes=False) assert update_result.outcome == "update-failed" diff --git a/tests/actions/test_initializer_action.py b/tests/actions/test_initializer_action.py index 14858c54..071dd625 100644 --- a/tests/actions/test_initializer_action.py +++ b/tests/actions/test_initializer_action.py @@ -65,7 +65,9 @@ def test_that_initialize_repo_does_not_load_config_when_resetting( mock_secureli_config.load.assert_not_called() - mock_logging_service.success.assert_called_once_with(LogAction.init) + mock_logging_service.success.assert_called_once_with( + test_folder_path, LogAction.init + ) def test_that_initialize_repo_logs_failure_when_failing_to_verify( @@ -78,4 +80,6 @@ def test_that_initialize_repo_logs_failure_when_failing_to_verify( initializer_action.initialize_repo(test_folder_path, True, True) - mock_logging_service.failure.assert_called_once_with(LogAction.init, ANY) + mock_logging_service.failure.assert_called_once_with( + test_folder_path, LogAction.init, ANY + ) diff --git a/tests/actions/test_update_action.py b/tests/actions/test_update_action.py index 48135425..3a154b4c 100644 --- a/tests/actions/test_update_action.py +++ b/tests/actions/test_update_action.py @@ -1,3 +1,4 @@ +from pathlib import Path from unittest.mock import MagicMock import pytest @@ -6,6 +7,8 @@ from secureli.actions.update import UpdateAction from secureli.services.updater import UpdateResult +test_folder_path = Path(".") + @pytest.fixture() def mock_scanner() -> MagicMock: @@ -65,7 +68,7 @@ def test_that_update_action_executes_successfully( successful=True, output="Some update performed" ) - update_action.update_hooks() + update_action.update_hooks(test_folder_path) mock_echo.print.assert_called_with("Update executed successfully.") @@ -79,7 +82,7 @@ def test_that_update_action_handles_failed_execution( successful=False, output="Failed to update" ) - update_action.update_hooks() + update_action.update_hooks(test_folder_path) mock_echo.print.assert_called_with("Failed to update") @@ -88,7 +91,7 @@ def test_that_latest_flag_initiates_update( update_action: UpdateAction, mock_echo: MagicMock, ): - update_action.update_hooks(latest=True) + update_action.update_hooks(test_folder_path, latest=True) mock_echo.print.assert_called_with("Hooks successfully updated to latest version") @@ -101,6 +104,6 @@ def test_that_latest_flag_handles_failed_update( mock_updater.update_hooks.return_value = UpdateResult( successful=False, output="Update failed" ) - update_action.update_hooks(latest=True) + update_action.update_hooks(test_folder_path, latest=True) mock_echo.print.assert_called_with("Update failed") diff --git a/tests/application/test_main.py b/tests/application/test_main.py index d9504317..0127a897 100644 --- a/tests/application/test_main.py +++ b/tests/application/test_main.py @@ -21,7 +21,7 @@ def test_that_setup_wires_up_container(mock_container: MagicMock): def test_that_init_creates_initializer_action_and_executes(mock_container: MagicMock): - secureli.main.init() + secureli.main.init(directory=".") mock_container.initializer_action.assert_called_once() @@ -33,12 +33,12 @@ def test_that_build_creates_build_action_and_executes(mock_container: MagicMock) def test_that_scan_is_tbd(mock_container: MagicMock): - secureli.main.scan() + secureli.main.scan(directory=".") mock_container.scan_action.assert_called_once() def test_that_update_is_tbd(mock_container: MagicMock): - secureli.main.update() + secureli.main.update(directory=".") mock_container.update_action.assert_called_once() diff --git a/tests/repositories/test_secureli_config.py b/tests/repositories/test_secureli_config.py index 6c2aaa07..46b6a40c 100644 --- a/tests/repositories/test_secureli_config.py +++ b/tests/repositories/test_secureli_config.py @@ -1,3 +1,4 @@ +from pathlib import Path from unittest.mock import MagicMock import pytest @@ -8,6 +9,8 @@ SecureliConfig, ) +test_folder_path = Path(".") + @pytest.fixture() def non_existent_path(mocker: MockerFixture) -> MagicMock: @@ -15,9 +18,13 @@ def non_existent_path(mocker: MockerFixture) -> MagicMock: config_file_path.exists.return_value = False config_file_path.is_dir.return_value = False mock_folder_path = MagicMock() + mock_folder_path.exists.return_value = False + mock_folder_path.is_dir.return_value = False mock_folder_path.__truediv__.return_value = config_file_path mock_secureli_folder_path = MagicMock() + mock_secureli_folder_path.exists.return_value = False + mock_secureli_folder_path.is_dir.return_value = False mock_secureli_folder_path.__truediv__.return_value = mock_folder_path mock_path_class = MagicMock() @@ -65,7 +72,7 @@ def test_that_repo_synthesizes_default_config_when_missing( non_existent_path: MagicMock, secureli_config: SecureliConfigRepository, ): - config = secureli_config.load() + config = secureli_config.load(test_folder_path) assert config.overall_language is None @@ -74,7 +81,7 @@ def test_that_repo_loads_config_when_present( existent_path: MagicMock, secureli_config: SecureliConfigRepository, ): - config = secureli_config.load() + config = secureli_config.load(test_folder_path) assert config.overall_language == "RadLang" @@ -85,6 +92,6 @@ def test_that_repo_saves_config( secureli_config: SecureliConfigRepository, ): config = SecureliConfig(overall_language="AwesomeLang") - secureli_config.save(config) + secureli_config.save(test_folder_path, config) mock_open.assert_called_once() diff --git a/tests/services/test_language_support.py b/tests/services/test_language_support.py index 2c15abd6..9359872b 100644 --- a/tests/services/test_language_support.py +++ b/tests/services/test_language_support.py @@ -1,3 +1,4 @@ +from pathlib import Path from unittest.mock import MagicMock import pytest @@ -8,6 +9,8 @@ ) from secureli.services.language_support import LanguageSupportService +test_folder_path = Path(".") + @pytest.fixture() def mock_pre_commit_hook() -> MagicMock: @@ -47,9 +50,9 @@ def test_that_language_support_attempts_to_install_pre_commit_hooks( language_support_service: LanguageSupportService, mock_pre_commit_hook: MagicMock, ): - metadata = language_support_service.apply_support("RadLang") + metadata = language_support_service.apply_support(test_folder_path, "RadLang") - mock_pre_commit_hook.install.assert_called_once_with("RadLang") + mock_pre_commit_hook.install.assert_called_once_with(test_folder_path, "RadLang") assert metadata.security_hook_id == "baddie-finder" diff --git a/tests/services/test_logging_service.py b/tests/services/test_logging_service.py index d1a11380..202dcb73 100644 --- a/tests/services/test_logging_service.py +++ b/tests/services/test_logging_service.py @@ -8,6 +8,8 @@ from secureli.repositories.secureli_config import SecureliConfig from secureli.services.logging import LoggingService, LogAction +test_folder_path = Path(".") + @pytest.fixture() def mock_path(mocker: MockerFixture) -> MagicMock: @@ -62,7 +64,7 @@ def test_that_logging_service_success_creates_logs_folder_if_not_exists( overall_language="RadLang", version_installed="abc123" ) mock_pre_commit.get_configuration.return_value = HookConfiguration(repos=[]) - logging_service.success(LogAction.init) + logging_service.success(test_folder_path, LogAction.init) mock_path.parent.mkdir.assert_called_once() @@ -77,7 +79,9 @@ def test_that_logging_service_failure_creates_logs_folder_if_not_exists( overall_language=None, version_installed=None ) - logging_service.failure(LogAction.init, "Horrible Failure", None, None) + logging_service.failure( + test_folder_path, LogAction.init, "Horrible Failure", None, None + ) mock_path.parent.mkdir.assert_called_once() @@ -93,6 +97,6 @@ def test_that_logging_service_success_logs_none_for_hook_config_if_not_initializ overall_language=None, version_installed=None ) - log_entry = logging_service.success(LogAction.build) + log_entry = logging_service.success(test_folder_path, LogAction.build) assert log_entry.hook_config is None diff --git a/tests/services/test_updater_service.py b/tests/services/test_updater_service.py index b42f539d..1c5dafa3 100644 --- a/tests/services/test_updater_service.py +++ b/tests/services/test_updater_service.py @@ -1,3 +1,4 @@ +from pathlib import Path from unittest.mock import MagicMock import pytest @@ -5,6 +6,8 @@ from secureli.abstractions.pre_commit import ExecuteResult from secureli.services.updater import UpdaterService +test_folder_path = Path(".") + @pytest.fixture() def updater_service( @@ -23,7 +26,7 @@ def test_that_updater_service_update_updates_and_prunes_with_pre_commit( mock_pre_commit.remove_unused_hooks.return_value = ExecuteResult( successful=True, output=output ) - update_result = updater_service.update() + update_result = updater_service.update(test_folder_path) mock_pre_commit.update.assert_called_once() mock_pre_commit.remove_unused_hooks.assert_called_once() @@ -39,7 +42,7 @@ def test_that_updater_service_update_does_not_prune_if_no_updates( mock_pre_commit.remove_unused_hooks.return_value = ExecuteResult( successful=True, output=output ) - update_result = updater_service.update() + update_result = updater_service.update(test_folder_path) mock_pre_commit.update.assert_called_once() mock_pre_commit.remove_unused_hooks.assert_not_called() @@ -55,7 +58,7 @@ def test_that_updater_service_update_handles_failure_to_update_config( successful=False, output=output ) - update_result = updater_service.update() + update_result = updater_service.update(test_folder_path) mock_pre_commit.install.assert_called_once() mock_pre_commit.update.assert_not_called() @@ -75,7 +78,7 @@ def test_that_updater_service_update_hooks_updates_with_pre_commit( mock_pre_commit.remove_unused_hooks.return_value = ExecuteResult( successful=True, output=output ) - update_result = updater_service.update_hooks() + update_result = updater_service.update_hooks(test_folder_path) mock_pre_commit.autoupdate_hooks.assert_called_once() assert update_result.successful @@ -92,7 +95,7 @@ def test_that_updater_service_update_hooks_handles_no_updates_successfully( mock_pre_commit.remove_unused_hooks.return_value = ExecuteResult( successful=True, output=output ) - update_result = updater_service.update_hooks() + update_result = updater_service.update_hooks(test_folder_path) mock_pre_commit.autoupdate_hooks.assert_called_once() assert update_result.successful diff --git a/tests/utilities/test_git_meta.py b/tests/utilities/test_git_meta.py index d200df91..0012acd9 100644 --- a/tests/utilities/test_git_meta.py +++ b/tests/utilities/test_git_meta.py @@ -1,3 +1,4 @@ +from pathlib import Path from subprocess import CompletedProcess from unittest.mock import MagicMock @@ -6,6 +7,8 @@ from secureli.utilities.git_meta import git_user_email, origin_url, current_branch_name +test_folder_path = Path(".") + @pytest.fixture() def mock_subprocess(mocker: MockerFixture) -> MagicMock: @@ -51,16 +54,16 @@ def test_git_user_email_loads_user_email_via_git_subprocess(mock_subprocess: Mag def test_origin_url_parses_config_to_get_origin_url(mock_configparser: MagicMock): - result = origin_url() + result = origin_url(test_folder_path) - mock_configparser.read.assert_called_once_with(".git/config") + mock_configparser.read.assert_called_once_with(f"{test_folder_path}/.git/config") assert result == "https://fake-build.com/git/repo" def test_current_branch_name_finds_ref_name_from_head_file( mock_open_git_head: MagicMock, ): - result = current_branch_name() + result = current_branch_name(test_folder_path) assert result == "feature/wicked-sick-branch" @@ -68,6 +71,6 @@ def test_current_branch_name_finds_ref_name_from_head_file( def test_current_branch_name_yields_unknown_due_to_io_error( mock_open_io_error: MagicMock, ): - result = current_branch_name() + result = current_branch_name(test_folder_path) assert result == "UNKNOWN"