credlock 1.0.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.
credlock/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ """CredLock - Prevent accidental secret commits to Git"""
2
+
3
+ __version__ = "1.0.0"
4
+ __author__ = "Your Name"
5
+ __description__ = "Automatic credential detection in your code"
6
+
7
+ # Version info
8
+ VERSION = __version__
credlock/cli.py ADDED
@@ -0,0 +1,244 @@
1
+ """
2
+ CredLock CLI
3
+ Command-line interface for CredLock
4
+ """
5
+
6
+ import typer
7
+ import time
8
+ import os
9
+ from pathlib import Path
10
+ from typing import Optional
11
+ from .scanner import scan_directory, scan_file, check_dangerous_files
12
+ from .formatter import (
13
+ print_scan_start, print_no_secrets, print_secrets_found,
14
+ print_dangerous_files, print_scan_summary, print_history,
15
+ print_config_info, print_setup_success, print_error, print_success
16
+ )
17
+ from .storage import save_scan, get_history, get_storage_path
18
+ from .config import load_config, add_custom_pattern
19
+ from . import __version__
20
+
21
+
22
+ app = typer.Typer(
23
+ name="credlock",
24
+ help="🔐 Prevent accidental credential commits to Git"
25
+ )
26
+
27
+
28
+ @app.command()
29
+ def scan(
30
+ directory: str = typer.Argument(".", help="Directory to scan"),
31
+ exit_on_secrets: bool = typer.Option(True, help="Exit with code 1 if secrets found")
32
+ ):
33
+ """
34
+ Scan directory for secrets
35
+
36
+ Usage:
37
+ credlock scan # Scan current directory
38
+ credlock scan /path/to/dir # Scan specific directory
39
+ """
40
+ print_scan_start(directory)
41
+
42
+ start_time = time.time()
43
+
44
+ # Scan directory
45
+ findings = scan_directory(directory)
46
+
47
+ # Check for dangerous files
48
+ dangerous_files = check_dangerous_files(directory)
49
+
50
+ # Count total files scanned
51
+ total_files = sum(1 for p in Path(directory).rglob("*") if p.is_file())
52
+
53
+ duration = time.time() - start_time
54
+
55
+ # Save scan to history
56
+ scan_data = {
57
+ "directory": directory,
58
+ "secrets_found": len(findings),
59
+ "dangerous_files": len(dangerous_files),
60
+ "total_files": total_files,
61
+ "duration": duration,
62
+ "findings": [
63
+ {
64
+ "file": f.file,
65
+ "line": f.line_number,
66
+ "pattern": f.pattern_name,
67
+ "content": f.line_content[:100]
68
+ } for f in findings
69
+ ]
70
+ }
71
+ scan_id = save_scan(scan_data)
72
+
73
+ # Print results
74
+ if findings:
75
+ print_secrets_found(findings, scan_id)
76
+ if dangerous_files:
77
+ print_dangerous_files(dangerous_files)
78
+ else:
79
+ print_no_secrets()
80
+
81
+ # Print dangerous files if found
82
+ if dangerous_files:
83
+ print_dangerous_files(dangerous_files)
84
+
85
+ print_scan_summary(total_files, findings, duration)
86
+
87
+ # Exit with appropriate code
88
+ if exit_on_secrets and (findings or dangerous_files):
89
+ raise typer.Exit(code=1)
90
+
91
+ raise typer.Exit(code=0)
92
+
93
+
94
+ @app.command()
95
+ def setup_git():
96
+ """
97
+ Install Git pre-commit hook
98
+
99
+ This command installs a pre-commit hook that runs CredLock
100
+ automatically before every git push.
101
+ """
102
+ git_dir = Path(".git")
103
+ if not git_dir.exists():
104
+ print_error("Not a git repository. Run 'git init' first.")
105
+ raise typer.Exit(code=1)
106
+
107
+ hooks_dir = git_dir / "hooks"
108
+ hooks_dir.mkdir(exist_ok=True)
109
+
110
+ pre_commit_path = hooks_dir / "pre-commit"
111
+
112
+ # Create pre-commit hook script
113
+ hook_script = """#!/bin/bash
114
+ # CredLock - Automatic Secret Detection
115
+ # This hook prevents accidental secret commits
116
+
117
+ credlock scan
118
+
119
+ if [ $? -ne 0 ]; then
120
+ echo ""
121
+ echo "[CredLock] Secrets detected. Push blocked."
122
+ echo "Fix the issues above before trying again."
123
+ exit 1
124
+ fi
125
+
126
+ exit 0
127
+ """
128
+
129
+ # Write hook file
130
+ with open(pre_commit_path, 'w') as f:
131
+ f.write(hook_script)
132
+
133
+ # Make executable
134
+ os.chmod(pre_commit_path, 0o755)
135
+
136
+ print_setup_success()
137
+
138
+
139
+ @app.command()
140
+ def history(
141
+ limit: int = typer.Option(10, help="Number of scans to show")
142
+ ):
143
+ """
144
+ View scan history
145
+
146
+ Usage:
147
+ credlock history # Show last 10 scans
148
+ credlock history --limit=5 # Show last 5 scans
149
+ """
150
+ scans = get_history(limit)
151
+
152
+ if not scans:
153
+ print_error("No scan history found")
154
+ raise typer.Exit(code=0)
155
+
156
+ # Format scans for display
157
+ formatted_scans = []
158
+ for scan in scans:
159
+ formatted_scans.append({
160
+ "scan_id": scan.get("scan_id", "unknown"),
161
+ "timestamp": scan.get("timestamp", "unknown")[:19],
162
+ "secrets_found": scan.get("secrets_found", 0),
163
+ "dangerous_files": scan.get("dangerous_files", 0),
164
+ "duration": f"{scan.get('duration', 0):.3f}s"
165
+ })
166
+
167
+ print_history(formatted_scans)
168
+
169
+
170
+ @app.command()
171
+ def configure():
172
+ """
173
+ Configure CredLock settings
174
+
175
+ Interactive configuration wizard
176
+ """
177
+ config = load_config()
178
+
179
+ print_config_info(config)
180
+
181
+ typer.echo("\nWhat would you like to do?")
182
+ typer.echo("1. Add custom pattern")
183
+ typer.echo("2. View all patterns")
184
+ typer.echo("3. Reset to defaults")
185
+ typer.echo("4. Exit")
186
+
187
+ choice = typer.prompt("Enter your choice (1-4)", type=int)
188
+
189
+ if choice == 1:
190
+ name = typer.prompt("Pattern name")
191
+ regex = typer.prompt("Regex pattern")
192
+ add_custom_pattern(name, regex)
193
+ print_success(f"Custom pattern '{name}' added!")
194
+
195
+ elif choice == 2:
196
+ typer.echo("\nBuilt-in patterns: 50+")
197
+ typer.echo("Custom patterns: " + str(len(config.get("custom_patterns", {}))))
198
+
199
+ elif choice == 3:
200
+ from .config import _config
201
+ _config.save(_config.get_default_config())
202
+ print_success("Configuration reset to defaults")
203
+
204
+ else:
205
+ typer.echo("Exiting...")
206
+ raise typer.Exit(code=0)
207
+
208
+
209
+ @app.command()
210
+ def version():
211
+ """
212
+ Show version information
213
+ """
214
+ typer.echo(f"CredLock v{__version__}")
215
+ typer.echo("🔐 Prevent accidental credential commits to Git")
216
+
217
+
218
+ @app.callback(invoke_without_command=True)
219
+ def main(
220
+ ctx: typer.Context,
221
+ show_version: bool = typer.Option(False, "--version", help="Show version"),
222
+ ):
223
+ """
224
+ CredLock - Automatic credential detection in your code
225
+
226
+ Prevent accidental secret commits to Git with automatic pre-commit scanning.
227
+
228
+ Usage:
229
+ credlock scan Scan current directory
230
+ credlock setup-git Install Git pre-commit hook
231
+ credlock history View scan history
232
+ credlock configure Configure settings
233
+ """
234
+ if show_version:
235
+ typer.echo(f"CredLock v{__version__}")
236
+ raise typer.Exit(code=0)
237
+
238
+ # If no command provided, show help
239
+ if ctx.invoked_subcommand is None:
240
+ typer.echo(ctx.get_help())
241
+
242
+
243
+ if __name__ == "__main__":
244
+ app()
credlock/config.py ADDED
@@ -0,0 +1,95 @@
1
+ """
2
+ CredLock Config
3
+ Configuration management
4
+ """
5
+
6
+ import yaml
7
+ from pathlib import Path
8
+ from typing import Dict
9
+
10
+
11
+ class Config:
12
+ """Manage CredLock configuration"""
13
+
14
+ def __init__(self):
15
+ """Initialize configuration"""
16
+ self.config_dir = Path.home() / ".credlock"
17
+ self.config_file = self.config_dir / "config.yaml"
18
+ self.config_dir.mkdir(exist_ok=True)
19
+
20
+ def load(self) -> Dict:
21
+ """Load configuration from file"""
22
+ if not self.config_file.exists():
23
+ return self.get_default_config()
24
+
25
+ try:
26
+ with open(self.config_file, 'r') as f:
27
+ config = yaml.safe_load(f)
28
+ return config if config else self.get_default_config()
29
+ except:
30
+ return self.get_default_config()
31
+
32
+ def save(self, config: Dict):
33
+ """Save configuration to file"""
34
+ with open(self.config_file, 'w') as f:
35
+ yaml.dump(config, f, default_flow_style=False)
36
+
37
+ def get_default_config(self) -> Dict:
38
+ """Get default configuration"""
39
+ return {
40
+ "patterns_enabled": 50,
41
+ "custom_patterns": {},
42
+ "ignored_files": [
43
+ ".git/*",
44
+ "node_modules/*",
45
+ "__pycache__/*",
46
+ "venv/*",
47
+ "env/*",
48
+ "*.pyc",
49
+ ],
50
+ "auto_scan": True,
51
+ "scan_depth": "recursive",
52
+ "report_format": "terminal",
53
+ }
54
+
55
+ def add_custom_pattern(self, name: str, regex: str):
56
+ """Add custom pattern"""
57
+ config = self.load()
58
+ config["custom_patterns"][name] = regex
59
+ self.save(config)
60
+
61
+ def remove_custom_pattern(self, name: str):
62
+ """Remove custom pattern"""
63
+ config = self.load()
64
+ if name in config["custom_patterns"]:
65
+ del config["custom_patterns"][name]
66
+ self.save(config)
67
+
68
+ def get_custom_patterns(self) -> Dict:
69
+ """Get custom patterns"""
70
+ config = self.load()
71
+ return config.get("custom_patterns", {})
72
+
73
+
74
+ # Global config instance
75
+ _config = Config()
76
+
77
+
78
+ def load_config() -> Dict:
79
+ """Load configuration"""
80
+ return _config.load()
81
+
82
+
83
+ def save_config(config: Dict):
84
+ """Save configuration"""
85
+ return _config.save(config)
86
+
87
+
88
+ def get_default_config() -> Dict:
89
+ """Get default configuration"""
90
+ return _config.get_default_config()
91
+
92
+
93
+ def add_custom_pattern(name: str, regex: str):
94
+ """Add custom pattern"""
95
+ return _config.add_custom_pattern(name, regex)
credlock/formatter.py ADDED
@@ -0,0 +1,231 @@
1
+ """
2
+ CredLock Formatter
3
+ Format and display scan results with colors and tables
4
+ """
5
+
6
+ from typing import List, Dict
7
+ from rich.console import Console
8
+ from rich.table import Table
9
+ from rich.panel import Panel
10
+ from rich.text import Text
11
+ from .scanner import Finding
12
+
13
+
14
+ console = Console()
15
+
16
+
17
+ class Formatter:
18
+ """Format scan results for terminal output"""
19
+
20
+ @staticmethod
21
+ def print_scan_start(directory: str):
22
+ """Print scan start message"""
23
+ console.print(f"\n🔍 Scanning {directory} for secrets...\n", style="blue")
24
+
25
+ @staticmethod
26
+ def print_no_secrets():
27
+ """Print when no secrets found"""
28
+ console.print("✅ No secrets found. Scan clean!\n", style="green bold")
29
+
30
+ @staticmethod
31
+ def print_secrets_found(findings: List[Finding], scan_id: str = None):
32
+ """
33
+ Print secrets found with formatted output
34
+
35
+ Args:
36
+ findings: List of Finding objects
37
+ scan_id: Scan ID for tracking
38
+ """
39
+ if not findings:
40
+ Formatter.print_no_secrets()
41
+ return False
42
+
43
+ # Group findings by file
44
+ by_file: Dict[str, List[Finding]] = {}
45
+ for finding in findings:
46
+ if finding.file not in by_file:
47
+ by_file[finding.file] = []
48
+ by_file[finding.file].append(finding)
49
+
50
+ # Print header
51
+ panel_text = f"⛔ SECRETS DETECTED! Found {len(findings)} potential secrets"
52
+ console.print(Panel(panel_text, style="red bold"), end="\n")
53
+
54
+ # Print by file
55
+ for filepath, file_findings in sorted(by_file.items()):
56
+ console.print(f"\n📄 [bold red]{filepath}[/bold red]", end="")
57
+ console.print(f" ({len(file_findings)} secret{'s' if len(file_findings) > 1 else ''})\n")
58
+
59
+ # Create table for this file
60
+ table = Table(show_header=False, box=None, padding=(0, 2))
61
+
62
+ for i, finding in enumerate(file_findings, 1):
63
+ line_str = f"Line {finding.line_number}:"
64
+ pattern_str = f"[{finding.pattern_name}]"
65
+
66
+ table.add_row(
67
+ Text(line_str, style="yellow"),
68
+ Text(pattern_str, style="magenta"),
69
+ Text(finding.line_content[:60], style="dim")
70
+ )
71
+
72
+ console.print(table)
73
+
74
+ # Print recommendations
75
+ console.print("\n" + "="*60)
76
+ console.print("[bold red]❌ PUSH BLOCKED[/bold red]")
77
+ console.print("="*60 + "\n")
78
+
79
+ console.print("[bold]Recommendations:[/bold]")
80
+ console.print("1. Review each secret above")
81
+ console.print("2. Move secrets to .env file (add to .gitignore)")
82
+ console.print("3. Use environment variables in your code:")
83
+ console.print(" const API_KEY = process.env.API_KEY;")
84
+ console.print("4. Remove secrets from Git history:")
85
+ console.print(" $ git rm --cached <file>")
86
+ console.print(" $ git commit --amend")
87
+ console.print("")
88
+
89
+ if scan_id:
90
+ console.print(f"Scan ID: {scan_id}", style="dim")
91
+
92
+ console.print("")
93
+ return True
94
+
95
+ @staticmethod
96
+ def print_dangerous_files(dangerous_files: List[str]):
97
+ """Print dangerous files that exist"""
98
+ if not dangerous_files:
99
+ return
100
+
101
+ console.print("\n[bold yellow]⚠️ DANGEROUS FILES DETECTED:[/bold yellow]\n")
102
+
103
+ for filename in dangerous_files:
104
+ console.print(f" ❌ {filename}")
105
+ console.print(f" └─ This file should NEVER be committed!")
106
+
107
+ console.print()
108
+
109
+ @staticmethod
110
+ def print_scan_summary(total_files: int, findings: List[Finding], duration: float):
111
+ """Print scan summary"""
112
+ console.print("\n" + "="*60)
113
+ console.print("[bold]SCAN SUMMARY[/bold]")
114
+ console.print("="*60)
115
+ console.print(f"Files scanned: {total_files}")
116
+ console.print(f"Secrets found: {len(findings)}")
117
+ console.print(f"Duration: {duration:.3f} seconds")
118
+ console.print("="*60 + "\n")
119
+
120
+ @staticmethod
121
+ def print_history(scans: List[Dict]):
122
+ """Print scan history"""
123
+ if not scans:
124
+ console.print("No scan history found.\n")
125
+ return
126
+
127
+ console.print("\n[bold]📋 CREDLOCK SCAN HISTORY[/bold]\n")
128
+
129
+ table = Table(title="Recent Scans")
130
+ table.add_column("Scan ID", style="cyan")
131
+ table.add_column("Timestamp", style="yellow")
132
+ table.add_column("Status", style="magenta")
133
+ table.add_column("Secrets", justify="right")
134
+
135
+ for scan in scans[:10]: # Show last 10
136
+ status = "✅ PASSED" if scan["secrets_found"] == 0 else f"❌ BLOCKED ({scan['secrets_found']})"
137
+ table.add_row(
138
+ scan["scan_id"],
139
+ scan["timestamp"],
140
+ status,
141
+ str(scan["secrets_found"])
142
+ )
143
+
144
+ console.print(table)
145
+ console.print()
146
+
147
+ @staticmethod
148
+ def print_config_info(config: Dict):
149
+ """Print configuration information"""
150
+ console.print("\n[bold]📝 CREDLOCK CONFIGURATION[/bold]\n")
151
+
152
+ table = Table(show_header=False)
153
+ table.add_column("Setting", style="cyan")
154
+ table.add_column("Value")
155
+
156
+ table.add_row("Patterns enabled", str(config.get("patterns_enabled", 50)))
157
+ table.add_row("Custom patterns", str(config.get("custom_patterns", 0)))
158
+ table.add_row("Config location", config.get("config_path", "~/.credlock/config.yaml"))
159
+
160
+ console.print(table)
161
+ console.print()
162
+
163
+ @staticmethod
164
+ def print_setup_success():
165
+ """Print successful setup message"""
166
+ console.print("\n" + "="*60)
167
+ console.print("[bold green]✅ SETUP SUCCESSFUL[/bold green]")
168
+ console.print("="*60)
169
+ console.print("\nCredLock Git pre-commit hook installed!")
170
+ console.print("\nFrom now on:")
171
+ console.print(" • Secrets will be detected before you push")
172
+ console.print(" • Dangerous files will be flagged")
173
+ console.print(" • Scan history will be saved locally")
174
+ console.print("\nTo verify:")
175
+ console.print(" $ cat .git/hooks/pre-commit")
176
+ console.print("\n" + "="*60 + "\n")
177
+
178
+ @staticmethod
179
+ def print_error(message: str):
180
+ """Print error message"""
181
+ console.print(f"\n[bold red]❌ ERROR:[/bold red] {message}\n")
182
+
183
+ @staticmethod
184
+ def print_success(message: str):
185
+ """Print success message"""
186
+ console.print(f"\n[bold green]✅ {message}[/bold green]\n")
187
+
188
+
189
+ # Create global formatter instance
190
+ _formatter = Formatter()
191
+
192
+
193
+ # Export functions
194
+ def print_scan_start(directory: str):
195
+ return _formatter.print_scan_start(directory)
196
+
197
+
198
+ def print_no_secrets():
199
+ return _formatter.print_no_secrets()
200
+
201
+
202
+ def print_secrets_found(findings: List[Finding], scan_id: str = None):
203
+ return _formatter.print_secrets_found(findings, scan_id)
204
+
205
+
206
+ def print_dangerous_files(dangerous_files: List[str]):
207
+ return _formatter.print_dangerous_files(dangerous_files)
208
+
209
+
210
+ def print_scan_summary(total_files: int, findings: List[Finding], duration: float):
211
+ return _formatter.print_scan_summary(total_files, findings, duration)
212
+
213
+
214
+ def print_history(scans: List[Dict]):
215
+ return _formatter.print_history(scans)
216
+
217
+
218
+ def print_config_info(config: Dict):
219
+ return _formatter.print_config_info(config)
220
+
221
+
222
+ def print_setup_success():
223
+ return _formatter.print_setup_success()
224
+
225
+
226
+ def print_error(message: str):
227
+ return _formatter.print_error(message)
228
+
229
+
230
+ def print_success(message: str):
231
+ return _formatter.print_success(message)
credlock/git_hook.py ADDED
@@ -0,0 +1,85 @@
1
+ """
2
+ CredLock Git Hook (FIXED)
3
+ Manage Git pre-commit hooks - removed emoji for Windows compatibility
4
+ """
5
+
6
+ import os
7
+ from pathlib import Path
8
+
9
+
10
+ class GitHookManager:
11
+ """Manage Git pre-commit hooks"""
12
+
13
+ @staticmethod
14
+ def install_hook(repo_path: str = ".") -> bool:
15
+ """
16
+ Install pre-commit hook
17
+
18
+ Args:
19
+ repo_path: Path to git repository
20
+
21
+ Returns:
22
+ True if successful
23
+ """
24
+ git_dir = Path(repo_path) / ".git"
25
+ if not git_dir.exists():
26
+ return False
27
+
28
+ hooks_dir = git_dir / "hooks"
29
+ hooks_dir.mkdir(exist_ok=True)
30
+
31
+ hook_file = hooks_dir / "pre-commit"
32
+
33
+ # FIXED: Removed emoji characters that break Windows CP1252 encoding
34
+ hook_content = """#!/bin/bash
35
+ # CredLock - Automatic Secret Detection
36
+ # Prevents accidental secret commits
37
+
38
+ credlock scan
39
+
40
+ if [ $? -ne 0 ]; then
41
+ echo ""
42
+ echo "[CredLock] Secrets detected. Commit blocked."
43
+ echo "Fix the issues above before trying again."
44
+ exit 1
45
+ fi
46
+
47
+ exit 0
48
+ """
49
+
50
+ try:
51
+ # FIXED: Explicitly use UTF-8 encoding for Windows compatibility
52
+ with open(hook_file, 'w', encoding='utf-8') as f:
53
+ f.write(hook_content)
54
+
55
+ os.chmod(hook_file, 0o755)
56
+ return True
57
+ except Exception as e:
58
+ print(f"Error installing hook: {e}")
59
+ return False
60
+
61
+ @staticmethod
62
+ def uninstall_hook(repo_path: str = ".") -> bool:
63
+ """
64
+ Remove pre-commit hook
65
+
66
+ Args:
67
+ repo_path: Path to git repository
68
+
69
+ Returns:
70
+ True if successful
71
+ """
72
+ hook_file = Path(repo_path) / ".git" / "hooks" / "pre-commit"
73
+
74
+ try:
75
+ if hook_file.exists():
76
+ hook_file.unlink()
77
+ return True
78
+ except:
79
+ return False
80
+
81
+ @staticmethod
82
+ def hook_exists(repo_path: str = ".") -> bool:
83
+ """Check if pre-commit hook exists"""
84
+ hook_file = Path(repo_path) / ".git" / "hooks" / "pre-commit"
85
+ return hook_file.exists()