git-secret-protector 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,126 @@
1
+ Metadata-Version: 2.1
2
+ Name: git-secret-protector
3
+ Version: 0.1.0
4
+ Summary: A tool for managing secrets in Git with AWS KMS integration.
5
+ Author: Duc Duong
6
+ Author-email: duc.duong@c0x12c.com
7
+ Requires-Python: >=3.10,<3.14
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.10
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Requires-Dist: boto3 (>=1.35.10,<2.0.0)
13
+ Requires-Dist: pycryptodome (>=3.20.0,<4.0.0)
14
+ Description-Content-Type: text/markdown
15
+
16
+ # git-secret-protector
17
+
18
+ `spartan-git_secret_protector` is a Python-based CLI tool designed to securely manage and protect sensitive files in your Git repositories. It integrates with AWS Parameter Store to encrypt and decrypt secrets, ensuring that your sensitive data remains secure throughout your development process.
19
+
20
+ ## Features
21
+
22
+ - **AES Key Management**: Securely create, manage, and rotate AES data keys using AWS Parameter Store.
23
+ - **File Encryption/Decryption**: Automatically encrypt and decrypt files in your repository based on patterns defined in the `.gitattributes` file.
24
+ - **Cache Management**: Cache AES data keys locally to improve performance and reduce redundant calls to AWS Parameter Store.
25
+ - **Git Hooks Integration**: Integrates with Git hooks to automatically manage secrets during Git operations.
26
+ - **Logging**: Configurable logging for detailed tracking of operations and errors.
27
+
28
+ ## Installation
29
+
30
+ You can install the `git_secret_protector` module via pip:
31
+
32
+ ```sh
33
+ pip install git_secret_protector
34
+ ```
35
+
36
+ ## Usage
37
+
38
+ ### 1. Initial Setup
39
+
40
+ Before using the tool, ensure you have the necessary AWS permissions to manage AWS MKS & SSM. Then, initialize your repository for secret protection by installing Git clean and smudge filter and setting up the module.
41
+
42
+ ```sh
43
+ git_secret_protector install
44
+ ```
45
+
46
+ ### 2. Pull AES Key
47
+
48
+ Before encrypting or decrypting files, you need to pull the relevant AES keys from AWS Parameter Store for a specific filter:
49
+
50
+ ```sh
51
+ git_secret_protector pull-aes-key <filter_name>
52
+ ```
53
+
54
+ This command will pull the latest AES data key from AWS Parameter Store for the specified filter and cache it locally.
55
+
56
+ This command will decrypt the files in your working directory for the specified filter, making them available for editing.
57
+
58
+ ### 3. Key Rotation
59
+
60
+ #### Command to Rotate Keys
61
+
62
+ ```sh
63
+ git_secret_protector rotate-key <filter_name>
64
+ ```
65
+
66
+ This command will generate a new AES data key in AWS Parameter Store, re-encrypt your files associated with the specified filter with the new key, and update the local cache.
67
+
68
+ #### Post-Rotation Code Reset
69
+ After rotating the keys, it is necessary to clear the Git cache and re-checkout all files. This step ensures that the smudge filters are triggered, allowing the files to be decrypted with the new key.
70
+
71
+ ```
72
+ # Remove all files from the index to clear the Git cache
73
+ git rm --cached -r .
74
+
75
+ # Force Git to re-checkout all files, triggering smudge filters
76
+ git reset --hard
77
+ ```
78
+
79
+ ### Configuration
80
+
81
+ All configurations are managed through a `config.ini` file located in the `.git_secret_protector` directory. You can customize the following settings:
82
+
83
+ - **AWS Configuration**: Set your AWS region, profile, and other credentials.
84
+ - **Logging**: Configure the log file path and rotation settings.
85
+ - **Module Name**: Specify a custom module name for organizing keys in AWS Parameter Store.
86
+
87
+ ### Example `.gitattributes` File
88
+
89
+ Define which files to encrypt in your `.gitattributes` file:
90
+
91
+ ```
92
+ secrets/*.tfstate filter=git-crypt-app diff=git-crypt-app
93
+ config/**/credentials/* filter=git-crypt-shared diff=git-crypt-shared
94
+ ```
95
+
96
+ ### Logging
97
+
98
+ Logs are stored in the `logs/` directory by default, and you can configure the log level and file rotation in the `config.ini` file.
99
+
100
+ ## Development
101
+
102
+ ### Running Tests
103
+
104
+ - **Unit Tests**: Located in the `tests/unit` directory, run them using `pytest`.
105
+ - **Integration Tests**: Located in the `tests/integration` directory, these tests interact with AWS Parameter Store and should be run manually.
106
+
107
+ ```sh
108
+ poetry run pytest tests/unit
109
+ ```
110
+
111
+ ### Contributing
112
+
113
+ We welcome contributions! Please read our [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines on how to contribute.
114
+
115
+ ## License
116
+
117
+ `git_secret_protector` is licensed under the MIT License. See the [LICENSE](LICENSE) file for more details.
118
+
119
+ ## Changelog
120
+
121
+ See [CHANGELOG.md](CHANGELOG.md) for a history of changes and updates.
122
+
123
+ ## Support
124
+
125
+ If you encounter any issues or have any questions, please open an issue on the GitHub repository or reach out to our support team.
126
+
@@ -0,0 +1,110 @@
1
+ # git-secret-protector
2
+
3
+ `spartan-git_secret_protector` is a Python-based CLI tool designed to securely manage and protect sensitive files in your Git repositories. It integrates with AWS Parameter Store to encrypt and decrypt secrets, ensuring that your sensitive data remains secure throughout your development process.
4
+
5
+ ## Features
6
+
7
+ - **AES Key Management**: Securely create, manage, and rotate AES data keys using AWS Parameter Store.
8
+ - **File Encryption/Decryption**: Automatically encrypt and decrypt files in your repository based on patterns defined in the `.gitattributes` file.
9
+ - **Cache Management**: Cache AES data keys locally to improve performance and reduce redundant calls to AWS Parameter Store.
10
+ - **Git Hooks Integration**: Integrates with Git hooks to automatically manage secrets during Git operations.
11
+ - **Logging**: Configurable logging for detailed tracking of operations and errors.
12
+
13
+ ## Installation
14
+
15
+ You can install the `git_secret_protector` module via pip:
16
+
17
+ ```sh
18
+ pip install git_secret_protector
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ ### 1. Initial Setup
24
+
25
+ Before using the tool, ensure you have the necessary AWS permissions to manage AWS MKS & SSM. Then, initialize your repository for secret protection by installing Git clean and smudge filter and setting up the module.
26
+
27
+ ```sh
28
+ git_secret_protector install
29
+ ```
30
+
31
+ ### 2. Pull AES Key
32
+
33
+ Before encrypting or decrypting files, you need to pull the relevant AES keys from AWS Parameter Store for a specific filter:
34
+
35
+ ```sh
36
+ git_secret_protector pull-aes-key <filter_name>
37
+ ```
38
+
39
+ This command will pull the latest AES data key from AWS Parameter Store for the specified filter and cache it locally.
40
+
41
+ This command will decrypt the files in your working directory for the specified filter, making them available for editing.
42
+
43
+ ### 3. Key Rotation
44
+
45
+ #### Command to Rotate Keys
46
+
47
+ ```sh
48
+ git_secret_protector rotate-key <filter_name>
49
+ ```
50
+
51
+ This command will generate a new AES data key in AWS Parameter Store, re-encrypt your files associated with the specified filter with the new key, and update the local cache.
52
+
53
+ #### Post-Rotation Code Reset
54
+ After rotating the keys, it is necessary to clear the Git cache and re-checkout all files. This step ensures that the smudge filters are triggered, allowing the files to be decrypted with the new key.
55
+
56
+ ```
57
+ # Remove all files from the index to clear the Git cache
58
+ git rm --cached -r .
59
+
60
+ # Force Git to re-checkout all files, triggering smudge filters
61
+ git reset --hard
62
+ ```
63
+
64
+ ### Configuration
65
+
66
+ All configurations are managed through a `config.ini` file located in the `.git_secret_protector` directory. You can customize the following settings:
67
+
68
+ - **AWS Configuration**: Set your AWS region, profile, and other credentials.
69
+ - **Logging**: Configure the log file path and rotation settings.
70
+ - **Module Name**: Specify a custom module name for organizing keys in AWS Parameter Store.
71
+
72
+ ### Example `.gitattributes` File
73
+
74
+ Define which files to encrypt in your `.gitattributes` file:
75
+
76
+ ```
77
+ secrets/*.tfstate filter=git-crypt-app diff=git-crypt-app
78
+ config/**/credentials/* filter=git-crypt-shared diff=git-crypt-shared
79
+ ```
80
+
81
+ ### Logging
82
+
83
+ Logs are stored in the `logs/` directory by default, and you can configure the log level and file rotation in the `config.ini` file.
84
+
85
+ ## Development
86
+
87
+ ### Running Tests
88
+
89
+ - **Unit Tests**: Located in the `tests/unit` directory, run them using `pytest`.
90
+ - **Integration Tests**: Located in the `tests/integration` directory, these tests interact with AWS Parameter Store and should be run manually.
91
+
92
+ ```sh
93
+ poetry run pytest tests/unit
94
+ ```
95
+
96
+ ### Contributing
97
+
98
+ We welcome contributions! Please read our [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines on how to contribute.
99
+
100
+ ## License
101
+
102
+ `git_secret_protector` is licensed under the MIT License. See the [LICENSE](LICENSE) file for more details.
103
+
104
+ ## Changelog
105
+
106
+ See [CHANGELOG.md](CHANGELOG.md) for a history of changes and updates.
107
+
108
+ ## Support
109
+
110
+ If you encounter any issues or have any questions, please open an issue on the GitHub repository or reach out to our support team.
@@ -0,0 +1,29 @@
1
+ [tool.poetry]
2
+ name = "git-secret-protector"
3
+ version = "0.1.0"
4
+ description = "A tool for managing secrets in Git with AWS KMS integration."
5
+ authors = ["Duc Duong <duc.duong@c0x12c.com>"]
6
+ readme = "README.md"
7
+ packages = [{ include = "git_secret_protector", from = "src" }]
8
+
9
+ [tool.poetry.dependencies]
10
+ python = ">=3.10,<3.14"
11
+ boto3 = "^1.35.10"
12
+ pycryptodome = "^3.20.0"
13
+
14
+ [tool.poetry.group.dev.dependencies]
15
+ pytest = "^8.3.2"
16
+ mock = "^5.1.0"
17
+ pyinstaller = "^6.10.0"
18
+
19
+ [tool.poetry.scripts]
20
+ git-secret-protector = "git_secret_protector.main:main"
21
+
22
+ [build-system]
23
+ requires = ["poetry-core>=1.0.0"]
24
+ build-backend = "poetry.core.masonry.api"
25
+
26
+ [tool.pytest.ini_options]
27
+ testpaths = [
28
+ "tests",
29
+ ]
@@ -0,0 +1 @@
1
+ # This file is typically empty but required for Python to treat the directory as a package.
@@ -0,0 +1,82 @@
1
+ import os
2
+ import base64
3
+ import boto3
4
+ import json
5
+ from git_secret_protector.settings import get_settings
6
+ import logging
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ class AesKeyManager:
12
+ def __init__(self):
13
+ self._ssm_client = None
14
+ self.cache_dir = get_settings().cache_dir
15
+ os.makedirs(self.cache_dir, exist_ok=True)
16
+
17
+ @property
18
+ def ssm_client(self):
19
+ if self._ssm_client is None:
20
+ self._ssm_client = boto3.client('ssm')
21
+ return self._ssm_client
22
+
23
+ def setup_aes_key_and_iv(self, filter_name):
24
+ aes_key = os.urandom(32) # 256 bits for AES-256
25
+ iv = os.urandom(16) # 128 bits (AES block size)
26
+
27
+ # Encode key and IV as base64 to serialize as JSON
28
+ data = {
29
+ 'aes_key': base64.b64encode(aes_key).decode('utf-8'),
30
+ 'iv': base64.b64encode(iv).decode('utf-8')
31
+ }
32
+ json_data = json.dumps(data)
33
+
34
+ # Store the serialized key and IV in AWS SSM Parameter Store
35
+ self.ssm_client.put_parameter(
36
+ Name=f"/encryption/{filter_name}/key_iv",
37
+ Value=json_data,
38
+ Type='SecureString',
39
+ Overwrite=True
40
+ )
41
+
42
+ logger.info(f"AES key and IV setup and stored in SSM for filter: {filter_name}")
43
+ self.cache_key_iv_locally(filter_name, json_data)
44
+
45
+ def retrieve_key_and_iv(self, filter_name):
46
+ local_data = self.load_key_iv_from_cache(filter_name)
47
+ if local_data:
48
+ return base64.b64decode(local_data['aes_key']), base64.b64decode(local_data['iv'])
49
+
50
+ response = self.ssm_client.get_parameter(
51
+ Name=f"/encryption/{filter_name}/key_iv",
52
+ WithDecryption=True
53
+ )
54
+ data = json.loads(response['Parameter']['Value'])
55
+ self.cache_key_iv_locally(filter_name, response['Parameter']['Value'])
56
+ return base64.b64decode(data['aes_key']), base64.b64decode(data['iv'])
57
+
58
+ def cache_key_iv_locally(self, filter_name, json_data):
59
+ cache_path = os.path.join(self.cache_dir, f"{filter_name}_key_iv.json")
60
+ with open(cache_path, 'w') as cache_file:
61
+ cache_file.write(json_data)
62
+ logger.debug("Cached AES key and IV locally for filter: %s", filter_name)
63
+
64
+ def load_key_iv_from_cache(self, filter_name):
65
+ cache_path = os.path.join(self.cache_dir, f"{filter_name}_key_iv.json")
66
+ if os.path.exists(cache_path):
67
+ with open(cache_path, 'r') as cache_file:
68
+ json_data = cache_file.read()
69
+ data = json.loads(json_data)
70
+ return data
71
+ logger.debug("No local cache found for filter: %s", filter_name)
72
+ return None
73
+
74
+ def destroy_aes_key_and_iv(self, filter_name):
75
+ """Destroy the AES key and IV for a specific filter name in AWS SSM."""
76
+ try:
77
+ parameter_name = f"/encryption/{filter_name}/key_iv"
78
+ self.ssm_client.delete_parameter(Name=parameter_name)
79
+ logging.info(f"Successfully destroyed AES key and IV in SSM for filter: {filter_name}")
80
+ except Exception as e:
81
+ logging.error(f"Failed to destroy AES key and IV for filter {filter_name}: {e}")
82
+ raise
@@ -0,0 +1,96 @@
1
+ import base64
2
+ import logging
3
+ import os
4
+ from Crypto.Cipher import AES
5
+ from Crypto.Util.Padding import pad, unpad
6
+ from git_secret_protector.git_attributes_parser import GitAttributesParser
7
+ from git_secret_protector.aes_key_manager import AesKeyManager
8
+
9
+ logger = logging.getLogger(__name__)
10
+ MAGIC_HEADER = b'ENCRYPTED' # Magic header to identify encrypted files
11
+
12
+
13
+ class EncryptionManager:
14
+ def __init__(self, aes_key, iv, git_attributes_parser):
15
+ if aes_key is None or iv is None:
16
+ raise ValueError("AES key and IV must not be None")
17
+ self.aes_key = aes_key
18
+ self.iv = iv
19
+ self.git_attributes_parser = git_attributes_parser
20
+
21
+ def encrypt_data(self, data):
22
+ if data.startswith(MAGIC_HEADER):
23
+ logger.warning("Data already contains MAGIC_HEADER. Skipping encryption.")
24
+ return data
25
+
26
+ ciphertext = self._perform_encryption(data)
27
+ return MAGIC_HEADER + ciphertext
28
+
29
+ def decrypt_data(self, data):
30
+ if not data.startswith(MAGIC_HEADER):
31
+ logger.warning("Data does not start with MAGIC HEADER. Skipping decryption.")
32
+ return data
33
+
34
+ encrypted_data = data[len(MAGIC_HEADER):]
35
+ plaintext = self._perform_decryption(encrypted_data)
36
+ return plaintext
37
+
38
+ def encrypt(self, filter_name):
39
+ files_to_encrypt = self.git_attributes_parser.get_files_for_filter(filter_name=filter_name)
40
+
41
+ for file_path in files_to_encrypt:
42
+ encrypted_data = self.encrypt_file(file_path)
43
+ with open(file_path, 'wb') as f:
44
+ f.write(encrypted_data)
45
+ logger.info("File encrypted: %s", file_path)
46
+
47
+ def decrypt(self, filter_name):
48
+ files_to_decrypt = self.git_attributes_parser.get_files_for_filter(filter_name=filter_name)
49
+
50
+ for file_path in files_to_decrypt:
51
+ decrypted_data = self.decrypt_file(file_path)
52
+ with open(file_path, 'wb') as f:
53
+ f.write(decrypted_data)
54
+ logger.info("File decrypted: %s", file_path)
55
+
56
+ def encrypt_file(self, file_path):
57
+ logger.info("Encrypting file: %s", file_path)
58
+ with open(file_path, 'rb') as f:
59
+ data = f.read()
60
+
61
+ if data.startswith(MAGIC_HEADER):
62
+ logger.info("File already contains MAGIC_HEADER. Skipping encryption.")
63
+ return data
64
+
65
+ ciphertext = self._perform_encryption(data)
66
+ return MAGIC_HEADER + ciphertext
67
+
68
+ def decrypt_file(self, file_path):
69
+ logger.info("Decrypting file: %s", file_path)
70
+ with open(os.path.abspath(file_path), 'rb') as f:
71
+ data = f.read()
72
+
73
+ if not data.startswith(MAGIC_HEADER):
74
+ logger.warning("File does not start with MAGIC HEADER. Skipping decryption.")
75
+ return data
76
+
77
+ encrypted_data = data[len(MAGIC_HEADER):]
78
+ plaintext = self._perform_decryption(encrypted_data)
79
+ return plaintext
80
+
81
+ def _perform_encryption(self, plaintext: bytes) -> bytes:
82
+ cipher = AES.new(self.aes_key, AES.MODE_CBC, self.iv)
83
+ ciphertext = cipher.encrypt(pad(plaintext, AES.block_size))
84
+ return base64.b64encode(ciphertext) # Base64 encode the result
85
+
86
+ def _perform_decryption(self, encrypted_text: bytes) -> bytes:
87
+ ciphertext = base64.b64decode(encrypted_text)
88
+ cipher = AES.new(self.aes_key, AES.MODE_CBC, self.iv)
89
+ plaintext = unpad(cipher.decrypt(ciphertext), AES.block_size)
90
+ return plaintext
91
+
92
+ @classmethod
93
+ def from_filter_name(cls, filter_name: str, git_attributes_parser: GitAttributesParser):
94
+ key_manager = AesKeyManager()
95
+ aes_key, iv = key_manager.retrieve_key_and_iv(filter_name)
96
+ return cls(aes_key, iv, git_attributes_parser)
@@ -0,0 +1,56 @@
1
+ import fnmatch
2
+ import glob
3
+ import os
4
+ import re
5
+
6
+
7
+ class GitAttributesParser:
8
+ def __init__(self, git_attributes_file='.gitattributes'):
9
+ self.git_attributes_file = git_attributes_file
10
+ self.patterns = self._parse_patterns()
11
+
12
+ def _parse_patterns(self):
13
+ """Parse the .gitattributes file to extract patterns associated with filters."""
14
+ patterns = {}
15
+ with open(self.git_attributes_file, 'r') as file:
16
+ for line in file:
17
+ match = re.search(r'(.+)\s+filter=(\S+)', line)
18
+ if match:
19
+ pattern = match.group(1).strip()
20
+ filter_name = match.group(2).strip()
21
+ if filter_name not in patterns:
22
+ patterns[filter_name] = []
23
+ patterns[filter_name].append(pattern)
24
+ return patterns
25
+
26
+ def _find_files_matching_patterns(self, patterns, repo_root='.'):
27
+ """Helper method to find files matching given patterns using glob."""
28
+ matched_files = set() # Using a set to avoid duplicates
29
+ for pattern in patterns:
30
+ files = glob.glob(os.path.join(repo_root, pattern), recursive=True)
31
+ matched_files.update(files)
32
+ return list(matched_files)
33
+
34
+ def get_secret_files(self, repo_root='.'):
35
+ """Return all files matching any of the filter patterns."""
36
+ secret_files = set()
37
+ for patterns in self.patterns.values():
38
+ secret_files.update(self._find_files_matching_patterns(patterns, repo_root))
39
+ return list(secret_files)
40
+
41
+ def get_files_for_filter(self, filter_name, repo_root='.'):
42
+ """Return all files matching the patterns for a specific filter name."""
43
+ patterns = self.patterns.get(filter_name, [])
44
+ return self._find_files_matching_patterns(patterns, repo_root)
45
+
46
+ def get_filter_names(self):
47
+ """Return a list of unique filter names from the .gitattributes file."""
48
+ return list(self.patterns.keys())
49
+
50
+ def get_filter_name_for_file(self, file_name, repo_root='.'):
51
+ """Return the filter name that matches the given file name based on .gitattributes patterns."""
52
+ for filter_name, patterns in self.patterns.items():
53
+ for pattern in patterns:
54
+ if fnmatch.fnmatch(os.path.relpath(file_name, repo_root), pattern):
55
+ return filter_name
56
+ return None
@@ -0,0 +1,38 @@
1
+ import logging
2
+ import os
3
+
4
+ logger = logging.getLogger(__name__)
5
+
6
+
7
+ class GitHooksInstaller:
8
+ def __init__(self, repo_path='.'):
9
+ self.repo_path = repo_path
10
+ self.hooks_path = os.path.join(self.repo_path, '.git', 'hooks')
11
+
12
+ def setup_hooks(self):
13
+ self._create_pre_commit_hook()
14
+ self._create_post_checkout_hook()
15
+ logger.info("Git hooks have been installed successfully.")
16
+
17
+ def _create_pre_commit_hook(self):
18
+ hook_script = """#!/bin/sh
19
+ # Pre-commit hook to encrypt files before commit
20
+ git_secret_protector encrypt
21
+ """
22
+
23
+ self._write_hook_script('pre-commit', hook_script)
24
+
25
+ def _create_post_checkout_hook(self):
26
+ hook_script = """#!/bin/sh
27
+ # Post-checkout hook to decrypt files after checkout
28
+ git_secret_protector decrypt
29
+ """
30
+
31
+ self._write_hook_script('post-checkout', hook_script)
32
+
33
+ def _write_hook_script(self, hook_name, script_content):
34
+ hook_file = os.path.join(self.hooks_path, hook_name)
35
+ with open(hook_file, 'w') as f:
36
+ f.write(script_content)
37
+ os.chmod(hook_file, 0o755) # Make the script executable
38
+ logger.info(f"{hook_name} hook installed.")
@@ -0,0 +1,38 @@
1
+ import logging
2
+ from git_secret_protector.encryption_manager import EncryptionManager
3
+ from git_secret_protector.git_attributes_parser import GitAttributesParser
4
+ from git_secret_protector.aes_key_manager import AesKeyManager
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+
9
+ class KeyRotator:
10
+ def __init__(self, key_manager: AesKeyManager, git_attributes_parser: GitAttributesParser):
11
+ self.aes_key_manager = key_manager
12
+ self.git_attributes_parser = git_attributes_parser
13
+
14
+ def rotate_key(self, filter_name: str):
15
+ try:
16
+ logger.info("Starting key and IV rotation for filter: %s", filter_name)
17
+
18
+ # Step 1: Retrieve the current AES key and IV
19
+ current_aes_key, current_iv = self.aes_key_manager.retrieve_key_and_iv(filter_name)
20
+
21
+ # Step 2: Decrypt all files using the current AES key and IV
22
+ decryption_manager = EncryptionManager(current_aes_key, current_iv, self.git_attributes_parser)
23
+ decryption_manager.decrypt(filter_name=filter_name)
24
+
25
+ # Step 3: Generate and store a new AES key and IV
26
+ self.aes_key_manager.setup_aes_key_and_iv(filter_name)
27
+
28
+ # Step 4: Retrieve the new AES key and IV
29
+ new_aes_key, new_iv = self.aes_key_manager.retrieve_key_and_iv(filter_name)
30
+
31
+ # Step 5: Encrypt all files using the new AES key and IV
32
+ encryption_manager = EncryptionManager(new_aes_key, new_iv, self.git_attributes_parser)
33
+ encryption_manager.encrypt(filter_name=filter_name)
34
+
35
+ logger.info("Key and IV rotation and re-encryption complete for filter: %s", filter_name)
36
+ except Exception as e:
37
+ logger.error("Failed to rotate key and IV for filter %s: %s", filter_name, e)
38
+ raise
@@ -0,0 +1,24 @@
1
+ import logging
2
+ import logging.handlers
3
+ import os
4
+
5
+ from git_secret_protector.settings import get_settings
6
+
7
+
8
+ def configure_logging():
9
+ settings = get_settings()
10
+ log_file = settings.log_file
11
+ log_level = settings.log_level
12
+ log_max_size = settings.log_max_size
13
+ log_backup_count = settings.log_backup_count
14
+
15
+ os.makedirs(os.path.dirname(log_file), exist_ok=True)
16
+
17
+ handler = logging.handlers.RotatingFileHandler(
18
+ log_file, maxBytes=log_max_size, backupCount=log_backup_count
19
+ )
20
+
21
+ formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
22
+ handler.setFormatter(formatter)
23
+
24
+ logging.basicConfig(level=log_level, handlers=[handler])
@@ -0,0 +1,164 @@
1
+ import argparse
2
+ import logging
3
+ import os
4
+ import sys
5
+ import time
6
+
7
+ from git_secret_protector.encryption_manager import EncryptionManager
8
+ from git_secret_protector.git_attributes_parser import GitAttributesParser
9
+ from git_secret_protector.git_hooks_installer import GitHooksInstaller
10
+ from git_secret_protector.key_rotator import KeyRotator
11
+ from git_secret_protector.aes_key_manager import AesKeyManager
12
+ from git_secret_protector.logging import configure_logging
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ def setup_aes_key(args):
18
+ logger.info("AES key setup complete for args: %s", args)
19
+ key_manager = AesKeyManager()
20
+ key_manager.setup_aes_key_and_iv(args.filter_name)
21
+ logger.info("AES key setup complete for filter: %s", args.filter_name)
22
+
23
+
24
+ def pull_aes_key(args):
25
+ key_manager = AesKeyManager()
26
+ key_manager.retrieve_key_and_iv(args.filter_name)
27
+ logger.info("KMS key pulled and cached for filter: %s", args.filter_name)
28
+
29
+
30
+ def rotate_key(args):
31
+ key_manager = AesKeyManager()
32
+ git_attributes_parser = GitAttributesParser()
33
+ rotator = KeyRotator(key_manager, git_attributes_parser)
34
+ rotator.rotate_key(args.filter_name)
35
+ logger.info("Key rotation complete for filter: %s", args.filter_name)
36
+
37
+
38
+ def install(args):
39
+ installer = GitHooksInstaller()
40
+ installer.setup_hooks()
41
+ logger.info("Git hooks installed successfully.")
42
+
43
+
44
+ def encrypt_files(args):
45
+ git_attributes_parser = GitAttributesParser()
46
+ filter_names = git_attributes_parser.get_filter_names()
47
+
48
+ for filter_name in filter_names:
49
+ encryption_manager = EncryptionManager.from_filter_name(filter_name)
50
+ encryption_manager.encrypt(filter_name)
51
+ logger.info("Files encrypted for filter: %s", filter_name)
52
+
53
+
54
+ def decrypt_files(args):
55
+ git_attributes_parser = GitAttributesParser()
56
+ filter_names = git_attributes_parser.get_filter_names()
57
+
58
+ for filter_name in filter_names:
59
+ encryption_manager = EncryptionManager.from_filter_name(filter_name)
60
+ encryption_manager.decrypt(filter_name)
61
+ logger.info("Files decrypted for filter: %s", filter_name)
62
+
63
+
64
+ def decrypt_stdin(args):
65
+ file_name = args.file_name # Local variable to reduce duplication
66
+ logger.info("Decrypting data from stdin with file_name: %s", file_name)
67
+
68
+ # Read all data from stdin
69
+ encrypted_data = sys.stdin.buffer.read()
70
+
71
+ if not encrypted_data:
72
+ logger.error("No data provided on stdin")
73
+ return
74
+
75
+ git_attributes_parser = GitAttributesParser()
76
+ filter_name = git_attributes_parser.get_filter_name_for_file(file_name)
77
+ logger.debug("Found filter_name to decrypt: %s", filter_name)
78
+
79
+ if filter_name is None:
80
+ logger.error("No filter found for file: %s", args.file_name)
81
+ return
82
+
83
+ # Assuming you have a modified version of the EncryptionManager to handle data instead of file names
84
+ encryption_manager = EncryptionManager.from_filter_name(filter_name)
85
+ decrypted_data = encryption_manager.decrypt_data(
86
+ encrypted_data) # This needs to be implemented in EncryptionManager
87
+
88
+ # Print the encrypted data to stdout
89
+ sys.stdout.buffer.write(decrypted_data)
90
+ sys.stdout.buffer.flush()
91
+
92
+
93
+ def encrypt_stdin(args):
94
+ file_name = args.file_name # Local variable to reduce duplication
95
+ logger.info("Encrypting data from stdin with file_name: %s", file_name)
96
+
97
+ # Read all data from stdin
98
+ input_data = sys.stdin.buffer.read()
99
+
100
+ if not input_data:
101
+ logger.error("No data provided on stdin")
102
+ return
103
+
104
+ git_attributes_parser = GitAttributesParser()
105
+ filter_name = git_attributes_parser.get_filter_name_for_file(file_name)
106
+ logger.debug("Found filter_name to decrypt: %s", filter_name)
107
+
108
+ if filter_name is None:
109
+ logger.error("No filter found for file: %s", args.file_name)
110
+ return
111
+
112
+ encryption_manager = EncryptionManager.from_filter_name(filter_name)
113
+ encrypted_data = encryption_manager.encrypt_data(input_data)
114
+
115
+ sys.stdout.buffer.write(encrypted_data)
116
+ sys.stdout.buffer.flush()
117
+
118
+
119
+ def main():
120
+ configure_logging()
121
+
122
+ parser = argparse.ArgumentParser(description="Git Secret Protector CLI")
123
+
124
+ subparsers = parser.add_subparsers(help="Available commands")
125
+
126
+ # Command to setup AES key in KMS
127
+ parser_setup_aes_key = subparsers.add_parser('setup-aes-key', help="Setup AES key in KMS")
128
+ parser_setup_aes_key.add_argument('filter_name', type=str, help="The filter name for the AES key")
129
+ parser_setup_aes_key.set_defaults(func=setup_aes_key)
130
+
131
+ # Command to pull KMS keys
132
+ parser_pull_aes_key = subparsers.add_parser('pull-aes-key', help="Pull KMS key for a filter")
133
+ parser_pull_aes_key.add_argument('filter_name', type=str, help="The filter name for the KMS key")
134
+ parser_pull_aes_key.set_defaults(func=pull_aes_key)
135
+
136
+ # Command to rotate KMS keys
137
+ parser_rotate_key = subparsers.add_parser('rotate-key', help="Rotate KMS key and re-encrypt secrets")
138
+ parser_rotate_key.add_argument('filter_name', type=str, help="The filter name for the KMS key")
139
+ parser_rotate_key.set_defaults(func=rotate_key)
140
+
141
+ # Command to install Git hooks
142
+ parser_install = subparsers.add_parser('install', help="Install Git hooks and initialize the module")
143
+ parser_install.set_defaults(func=install)
144
+
145
+ # Command to decrypt data from stdin
146
+ parser_decrypt_stdin = subparsers.add_parser('decrypt', help="Decrypt data from stdin")
147
+ parser_decrypt_stdin.add_argument('file_name', type=str, help="Filename for logging/reference")
148
+ parser_decrypt_stdin.set_defaults(func=decrypt_stdin)
149
+
150
+ # Command to encrypt data from stdin
151
+ parser_encrypt_stdin = subparsers.add_parser('encrypt', help="Encrypt data from stdin")
152
+ parser_encrypt_stdin.add_argument('file_name', type=str, help="Filename for logging/reference")
153
+ parser_encrypt_stdin.set_defaults(func=encrypt_stdin)
154
+
155
+ args = parser.parse_args()
156
+
157
+ if hasattr(args, 'func'):
158
+ args.func(args)
159
+ else:
160
+ parser.print_help()
161
+
162
+
163
+ if __name__ == '__main__':
164
+ main()
@@ -0,0 +1,43 @@
1
+ import configparser
2
+ import os
3
+ from dataclasses import dataclass, field
4
+
5
+ BASE_DIR = '.git_secret_protector'
6
+
7
+ @dataclass
8
+ class Settings:
9
+ # Singleton instance variable
10
+ _instance: 'Settings' = field(default=None, init=False, repr=False)
11
+
12
+ config_file: str = os.path.join(BASE_DIR, 'config.ini')
13
+ cache_dir: str = os.path.join(BASE_DIR, 'cache')
14
+ log_dir: str = os.path.join(BASE_DIR, 'logs')
15
+ module_name: str = 'git-secret-protector'
16
+ log_file: str = field(init=False)
17
+ log_level: str = 'INFO'
18
+ log_max_size: int = 10485760 # 10MB
19
+ log_backup_count: int = 3
20
+ config: configparser.ConfigParser = field(default_factory=configparser.ConfigParser, init=False)
21
+
22
+ def __post_init__(self):
23
+ self.log_file = os.path.join(self.log_dir, 'git_secret_protector.log')
24
+ self._load_config()
25
+
26
+ def _load_config(self):
27
+ if os.path.exists(self.config_file):
28
+ self.config.read(self.config_file)
29
+ self.module_name = self.config.get('DEFAULT', 'module_name', fallback=self.module_name)
30
+ self.log_file = self.config.get('DEFAULT', 'log_file', fallback=self.log_file)
31
+ self.log_level = self.config.get('DEFAULT', 'log_level', fallback=self.log_level)
32
+ self.log_max_size = self.config.getint('DEFAULT', 'log_max_size', fallback=self.log_max_size)
33
+ self.log_backup_count = self.config.getint('DEFAULT', 'log_backup_count', fallback=self.log_backup_count)
34
+
35
+ @classmethod
36
+ def get_instance(cls):
37
+ if cls._instance is None:
38
+ cls._instance = cls()
39
+ return cls._instance
40
+
41
+
42
+ def get_settings():
43
+ return Settings.get_instance()