git-secret-protector 0.1.0__py3-none-any.whl
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.
- git_secret_protector/__init__.py +1 -0
- git_secret_protector/aes_key_manager.py +82 -0
- git_secret_protector/encryption_manager.py +96 -0
- git_secret_protector/git_attributes_parser.py +56 -0
- git_secret_protector/git_hooks_installer.py +38 -0
- git_secret_protector/key_rotator.py +38 -0
- git_secret_protector/logging.py +24 -0
- git_secret_protector/main.py +164 -0
- git_secret_protector/settings.py +43 -0
- git_secret_protector-0.1.0.dist-info/METADATA +126 -0
- git_secret_protector-0.1.0.dist-info/RECORD +13 -0
- git_secret_protector-0.1.0.dist-info/WHEEL +4 -0
- git_secret_protector-0.1.0.dist-info/entry_points.txt +3 -0
|
@@ -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()
|
|
@@ -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,13 @@
|
|
|
1
|
+
git_secret_protector/__init__.py,sha256=dJ2wWyeYPToile4j1SXxxGJZj8-ZtP740jG9_awPNN0,92
|
|
2
|
+
git_secret_protector/aes_key_manager.py,sha256=alGq_NTscfFazHHGBs-wz-uDSTn9r7JKQcdxvC-cxMo,3177
|
|
3
|
+
git_secret_protector/encryption_manager.py,sha256=d_OeP6mnofiXsoWPjIhhogzyjyvefxN3tsx3HVmpqY0,3741
|
|
4
|
+
git_secret_protector/git_attributes_parser.py,sha256=ADmdc0oPQsw_uO1htKl0j9lneUweqi_ocBiAenzJZso,2412
|
|
5
|
+
git_secret_protector/git_hooks_installer.py,sha256=Eii-UCk7qD1jQ8EtyCb5CV2C3BeyqURwtI5VmFVpNHI,1220
|
|
6
|
+
git_secret_protector/key_rotator.py,sha256=-nsCu4YJn4nJmEOHW6my2pECCwCCB2vjlRqUV18hSzc,1771
|
|
7
|
+
git_secret_protector/logging.py,sha256=-_H4excUtSraGC7NM-kLsFQIc5BY2PVFDg-swY3N-Jc,697
|
|
8
|
+
git_secret_protector/main.py,sha256=3MSoLp927qsUVLaoeLOe2CBYklqOq0sn0dMoSoAROoo,6024
|
|
9
|
+
git_secret_protector/settings.py,sha256=ZSa70BaXc6vTd5BLdvWXMABReUyPT1xYQr8Rer_I2NY,1656
|
|
10
|
+
git_secret_protector-0.1.0.dist-info/METADATA,sha256=1tKgFoAiEjXfnzbFumV2zl8fZ3X3UpEyzaR5-YXtEAw,4576
|
|
11
|
+
git_secret_protector-0.1.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
12
|
+
git_secret_protector-0.1.0.dist-info/entry_points.txt,sha256=LixAV_KKoYKwkoOyUCS1xpjuX06ubHoha6FhaQ2toYg,71
|
|
13
|
+
git_secret_protector-0.1.0.dist-info/RECORD,,
|