diff --git a/README.md b/README.md index f1c63a8c..65b727cc 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ https://marketplace.visualstudio.com/items?itemName=yzhang.markdown-all-in-one - - [Usage](#usage) - [Help](#help) - [Init](#init) + - [Preserving Pre-Commit Config](#preserving-pre-commit-config) - [Scan](#scan) - [Scanned Files](#scanned-files) - [PII Scan](#pii-scan) @@ -116,6 +117,10 @@ All you need to do is run: Running `secureli init` will allow seCureLI to detect the languages in your repo, install pre-commit, install all the appropriate pre-commit hooks for your local repo, run a scan for secrets in your local repo, and update the installed hooks. +#### Preserving Pre-Commit Config + +If you have an existing pre-commit config file you want to preserve when running `secureli init`, you can use the `--preserve-precommit-config` flag. This is useful for example when checking out a repo with an existing pre-commit config file. + ### Scan To manually trigger a scan, run: diff --git a/secureli/actions/action.py b/secureli/actions/action.py index 1b8267e6..d06b9e74 100644 --- a/secureli/actions/action.py +++ b/secureli/actions/action.py @@ -69,6 +69,7 @@ def verify_install( always_yes: bool, files: list[Path], action_source: install.ActionSource, + preserve_precommit_config: bool = False, ) -> install.VerifyResult: """ Installs, upgrades or verifies the current seCureLI installation @@ -77,6 +78,8 @@ def verify_install( :param always_yes: Assume "Yes" to all prompts :param files: A List of files to scope the install to. This allows language detection to run on only a selected list of files when scanning the repo. + :param action_source: The source of the action + :param preserve_precommit_config: If true, preserve the existing pre-commit configuration """ is_config_out_of_date = ( @@ -163,6 +166,7 @@ def verify_install( newly_detected_languages, always_yes, preferred_config_path if pre_commit_to_preserve else None, + preserve_precommit_config, ) else: self.action_deps.echo.print( @@ -195,6 +199,7 @@ def _install_secureli( install_languages: list[str], always_yes: bool, pre_commit_config_location: Path = None, + preserve_precommit_config: bool = False, ) -> install.VerifyResult: """ Installs seCureLI into the given folder path and returns the new configuration @@ -202,6 +207,8 @@ def _install_secureli( :param detected_languages: list of all languages found in the repo :param install_languages: list of specific langugages to install secureli features for :param always_yes: Assume "Yes" to all prompts + :param pre_commit_config_location: The location of the pre-commit config file + :param preserve_precommit_config: If true, preserve the existing pre-commit configuration :return: The new SecureliConfig after install or None if installation did not complete """ @@ -230,6 +237,7 @@ def _install_secureli( install_languages, language_config_result, new_install, + preserve_precommit_config, ) for error_msg in metadata.linter_config_write_errors: diff --git a/secureli/actions/initializer.py b/secureli/actions/initializer.py index 6d2afca6..0a74a4b6 100644 --- a/secureli/actions/initializer.py +++ b/secureli/actions/initializer.py @@ -17,13 +17,18 @@ def __init__( super().__init__(action_deps) def initialize_repo( - self, folder_path: Path, reset: bool, always_yes: bool + self, + folder_path: Path, + reset: bool, + always_yes: bool, + preserve_precommit_config: bool = False, ) -> VerifyResult: """ Initializes seCureLI for the specified folder path :param folder_path: The folder path to initialize the repo for :param reset: If true, disregard existing configuration and start fresh :param always_yes: Assume "Yes" to all prompts + :param preserve_precommit_config: If true, preserve the existing pre-commit configuration """ verify_result = self.verify_install( folder_path, @@ -31,6 +36,7 @@ def initialize_repo( always_yes, files=None, action_source=ActionSource.INITIALIZER, + preserve_precommit_config=preserve_precommit_config, ) if verify_result.outcome in ScanAction.halting_outcomes: self.action_deps.logging.failure(LogAction.init, verify_result.outcome) diff --git a/secureli/main.py b/secureli/main.py index bbdf02b6..c6eb6253 100644 --- a/secureli/main.py +++ b/secureli/main.py @@ -91,6 +91,11 @@ def init( help="Run secureli against a specific directory", ), ] = Path("."), + preserve_precommit_config: bool = Option( + False, + "--preserve-precommit-config", + help="Preserve the existing pre-commit configuration", + ), ): """ Detect languages and initialize pre-commit hooks and linters for the project @@ -98,7 +103,7 @@ def init( SecureliConfig.FOLDER_PATH = Path(directory) init_result = container.initializer_action().initialize_repo( - Path(directory), reset, yes + Path(directory), reset, yes, preserve_precommit_config ) if init_result.outcome in [ VerifyOutcome.UP_TO_DATE, diff --git a/secureli/modules/language_analyzer/language_support.py b/secureli/modules/language_analyzer/language_support.py index c6ce8d9d..a78d830d 100644 --- a/secureli/modules/language_analyzer/language_support.py +++ b/secureli/modules/language_analyzer/language_support.py @@ -37,12 +37,14 @@ def apply_support( languages: list[str], language_config_result: language.BuildConfigResult, overwrite_pre_commit: bool, + preserve_precommit_config: bool = False, ) -> language.LanguageMetadata: """ Applies Secure Build support for the provided languages :param languages: list of languages to provide support for :param language_config_result: resulting config from language hook detection :param overwrite_pre_commit: flag to determine if config should overwrite or append to config file + :param preserve_precommit_config: If true, preserve the existing pre-commit configuration :raises LanguageNotSupportedError if support for the language is not provided :return: Metadata including version of the language configuration that was just installed as well as a secret-detection hook ID, if present. @@ -58,14 +60,15 @@ def apply_support( language_config_result.linter_configs ) - pre_commit_file_mode = "w" if overwrite_pre_commit else "a" - with open(path_to_pre_commit_file, pre_commit_file_mode) as f: - data = ( - language_config_result.config_data - if overwrite_pre_commit - else language_config_result.config_data["repos"] - ) - f.write(yaml.dump(data)) + if not preserve_precommit_config: + pre_commit_file_mode = "w" if overwrite_pre_commit else "a" + with open(path_to_pre_commit_file, pre_commit_file_mode) as f: + data = ( + language_config_result.config_data + if overwrite_pre_commit + else language_config_result.config_data["repos"] + ) + f.write(yaml.dump(data)) # Add .secureli/ to the gitignore folder if needed self.git_ignore.ignore_secureli_files() diff --git a/tests/modules/language_analyzer/test_language_support.py b/tests/modules/language_analyzer/test_language_support.py index 37733dfa..16981d23 100644 --- a/tests/modules/language_analyzer/test_language_support.py +++ b/tests/modules/language_analyzer/test_language_support.py @@ -272,7 +272,56 @@ def mock_loader_side_effect(resource): ) metadata = language_support_service.apply_support( - ["RadLang"], build_config_result, overwrite_pre_commit=True + ["RadLang"], + build_config_result, + overwrite_pre_commit=True, + ) + + assert metadata.security_hook_id == "baddie-finder" + + +def test_that_language_support_preserves_precommit_config( + language_support_service: language_support.LanguageSupportService, + mock_language_config_service: MagicMock, + mock_data_loader: MagicMock, + mock_open: MagicMock, + mock_pre_commit_hook: MagicMock, +): + def mock_loader_side_effect(resource): + return """ + http://sample-repo.com/baddie-finder: + - baddie-finder + """ + + mock_language_config_service.get_language_config.return_value = language.LanguagePreCommitResult( + language="Python", + version="abc123", + linter_config=language.LoadLinterConfigsResult( + successful=True, + linter_data=[{"filename": "test.txt", "settings": {}}], + ), + config_data=""" + repos: + - repo: http://sample-repo.com/baddie-finder + hooks: + - id: baddie-finder + """, + ) + + mock_data_loader.side_effect = mock_loader_side_effect + + languages = ["RadLang"] + lint_languages = [*languages] + + build_config_result = language_support_service.build_pre_commit_config( + languages, lint_languages + ) + + metadata = language_support_service.apply_support( + ["RadLang"], + build_config_result, + overwrite_pre_commit=True, + preserve_precommit_config=True, ) assert metadata.security_hook_id == "baddie-finder"