kryptorious-envguard 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.
- envguard/__init__.py +2 -0
- envguard/cli.py +231 -0
- kryptorious_envguard-1.0.0.dist-info/METADATA +14 -0
- kryptorious_envguard-1.0.0.dist-info/RECORD +7 -0
- kryptorious_envguard-1.0.0.dist-info/WHEEL +5 -0
- kryptorious_envguard-1.0.0.dist-info/entry_points.txt +2 -0
- kryptorious_envguard-1.0.0.dist-info/top_level.txt +1 -0
envguard/__init__.py
ADDED
envguard/cli.py
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""EnvGuard CLI — Protect your environment variables."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Dict, Set
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.panel import Panel
|
|
11
|
+
from rich.table import Table
|
|
12
|
+
|
|
13
|
+
console = Console()
|
|
14
|
+
|
|
15
|
+
# Common patterns for secrets
|
|
16
|
+
SECRET_PATTERNS = [
|
|
17
|
+
(r'(?i)(api[_-]?key|apikey|secret|password|token|auth|credential|private[_-]?key)',
|
|
18
|
+
"Potential secret"),
|
|
19
|
+
(r'[A-Za-z0-9+/]{32,}={0,2}', "Base64-encoded value"),
|
|
20
|
+
(r'sk-[A-Za-z0-9]{32,}', "Stripe/OpenAI-style API key"),
|
|
21
|
+
(r'ghp_[A-Za-z0-9]{36}', "GitHub personal access token"),
|
|
22
|
+
(r'AKIA[0-9A-Z]{16}', "AWS access key"),
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
COMMON_REQUIRED = [
|
|
26
|
+
"DATABASE_URL", "SECRET_KEY", "API_KEY", "DEBUG",
|
|
27
|
+
"ALLOWED_HOSTS", "CORS_ORIGINS", "LOG_LEVEL",
|
|
28
|
+
"REDIS_URL", "CELERY_BROKER_URL", "SENTRY_DSN",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _load_env(path: str = ".env") -> Dict[str, str]:
|
|
33
|
+
"""Load .env file, return dict of variables."""
|
|
34
|
+
env_path = Path(path)
|
|
35
|
+
if not env_path.exists():
|
|
36
|
+
return {}
|
|
37
|
+
|
|
38
|
+
vars_dict = {}
|
|
39
|
+
with open(env_path, encoding="utf-8") as f:
|
|
40
|
+
for line in f:
|
|
41
|
+
line = line.strip()
|
|
42
|
+
if not line or line.startswith("#"):
|
|
43
|
+
continue
|
|
44
|
+
if "=" in line:
|
|
45
|
+
key, _, value = line.partition("=")
|
|
46
|
+
key = key.strip()
|
|
47
|
+
# Remove quotes
|
|
48
|
+
value = value.strip().strip('"').strip("'")
|
|
49
|
+
vars_dict[key] = value
|
|
50
|
+
return vars_dict
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _find_env_files(start: str = ".") -> list:
|
|
54
|
+
"""Find all .env files in project."""
|
|
55
|
+
env_files = []
|
|
56
|
+
for root, dirs, files in os.walk(start):
|
|
57
|
+
# Skip node_modules, .git, venv
|
|
58
|
+
dirs[:] = [d for d in dirs if d not in (".git", "node_modules", "venv", ".venv", "__pycache__")]
|
|
59
|
+
for f in files:
|
|
60
|
+
if f == ".env" or f.endswith(".env") or f == ".env.example" or f == ".env.local":
|
|
61
|
+
env_files.append(os.path.join(root, f))
|
|
62
|
+
return env_files
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@click.group()
|
|
66
|
+
@click.version_option(version="1.0.0", prog_name="envguard")
|
|
67
|
+
def main():
|
|
68
|
+
"""EnvGuard — Validate, generate, and secure your .env files.
|
|
69
|
+
|
|
70
|
+
Stop shipping broken configs. One command to check everything.
|
|
71
|
+
"""
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@main.command()
|
|
76
|
+
@click.option("--path", "-p", default=".", help="Project root to scan")
|
|
77
|
+
@click.option("--strict/--lenient", default=False, help="Fail on warnings (premium)")
|
|
78
|
+
def check(path, strict):
|
|
79
|
+
"""Check .env files for issues.
|
|
80
|
+
|
|
81
|
+
Validates syntax, finds missing values, detects hardcoded secrets.
|
|
82
|
+
"""
|
|
83
|
+
console.print()
|
|
84
|
+
console.print(Panel("[bold]EnvGuard Check[/bold]", border_style="blue"))
|
|
85
|
+
|
|
86
|
+
env_files = _find_env_files(path)
|
|
87
|
+
if not env_files:
|
|
88
|
+
console.print("[yellow]No .env files found.[/yellow]")
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
console.print(f"Found [bold]{len(env_files)}[/bold] .env file(s)\n")
|
|
92
|
+
|
|
93
|
+
total_issues = 0
|
|
94
|
+
|
|
95
|
+
for env_path in env_files:
|
|
96
|
+
env_vars = _load_env(env_path)
|
|
97
|
+
issues = []
|
|
98
|
+
|
|
99
|
+
# Check for empty values
|
|
100
|
+
empty_vars = [k for k, v in env_vars.items() if not v]
|
|
101
|
+
if empty_vars:
|
|
102
|
+
for v in empty_vars:
|
|
103
|
+
issues.append(("warning", f"Empty value: {v}"))
|
|
104
|
+
|
|
105
|
+
# Check for placeholder values
|
|
106
|
+
placeholders = ["changeme", "todo", "xxx", "your-", "example", "placeholder"]
|
|
107
|
+
for k, v in env_vars.items():
|
|
108
|
+
if v and any(p in v.lower() for p in placeholders):
|
|
109
|
+
issues.append(("warning", f"Placeholder value: {k}={v[:40]}..."))
|
|
110
|
+
|
|
111
|
+
# Check for secrets in plaintext
|
|
112
|
+
for k, v in env_vars.items():
|
|
113
|
+
if not v:
|
|
114
|
+
continue
|
|
115
|
+
for pattern, desc in SECRET_PATTERNS:
|
|
116
|
+
if re.match(pattern, v):
|
|
117
|
+
issues.append(("error", f"Hardcoded secret: {k} ({desc})"))
|
|
118
|
+
break
|
|
119
|
+
|
|
120
|
+
# Check for common missing variables
|
|
121
|
+
for var in COMMON_REQUIRED:
|
|
122
|
+
if var not in env_vars:
|
|
123
|
+
issues.append(("info", f"Missing common variable: {var}"))
|
|
124
|
+
|
|
125
|
+
# Display
|
|
126
|
+
short_path = os.path.relpath(env_path, path) if path != "." else env_path
|
|
127
|
+
if not issues:
|
|
128
|
+
console.print(f" [green]✓[/green] {short_path} — {len(env_vars)} variables, clean")
|
|
129
|
+
else:
|
|
130
|
+
total_issues += len(issues)
|
|
131
|
+
console.print(f" [yellow]{short_path}[/yellow] — {len(env_vars)} variables, {len(issues)} issues:")
|
|
132
|
+
for severity, msg in issues:
|
|
133
|
+
icon = {"error": "[red]✗[/red]", "warning": "[yellow]![/yellow]", "info": "[blue]ℹ[/blue]"}.get(severity, "?")
|
|
134
|
+
console.print(f" {icon} {msg}")
|
|
135
|
+
|
|
136
|
+
console.print()
|
|
137
|
+
if total_issues == 0:
|
|
138
|
+
console.print("[green]All .env files clean.[/green]")
|
|
139
|
+
else:
|
|
140
|
+
console.print(f"[yellow]{total_issues} issue(s) found.[/yellow]")
|
|
141
|
+
|
|
142
|
+
if strict:
|
|
143
|
+
console.print("[yellow]Strict mode requires premium. Upgrade at https://kryptorious.gumroad.com/l/jbvet[/yellow]")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@main.command()
|
|
147
|
+
@click.option("--path", "-p", default=".env", help="Source .env file")
|
|
148
|
+
@click.option("--output", "-o", default=".env.example", help="Output file")
|
|
149
|
+
def generate(path, output):
|
|
150
|
+
"""Generate .env.example from .env (values removed, keys kept).
|
|
151
|
+
|
|
152
|
+
Creates a safe-to-commit template from your .env file.
|
|
153
|
+
"""
|
|
154
|
+
console.print()
|
|
155
|
+
console.print(Panel("[bold]EnvGuard Generate[/bold]", border_style="blue"))
|
|
156
|
+
|
|
157
|
+
env_vars = _load_env(path)
|
|
158
|
+
if not env_vars:
|
|
159
|
+
console.print(f"[red]No variables found in {path}[/red]")
|
|
160
|
+
return
|
|
161
|
+
|
|
162
|
+
lines = []
|
|
163
|
+
with open(path, encoding="utf-8") as f:
|
|
164
|
+
for line in f:
|
|
165
|
+
stripped = line.strip()
|
|
166
|
+
if not stripped or stripped.startswith("#"):
|
|
167
|
+
lines.append(line.rstrip())
|
|
168
|
+
continue
|
|
169
|
+
if "=" in stripped:
|
|
170
|
+
key = stripped.split("=", 1)[0].strip()
|
|
171
|
+
lines.append(f"{key}=")
|
|
172
|
+
else:
|
|
173
|
+
lines.append(line.rstrip())
|
|
174
|
+
|
|
175
|
+
content = "\n".join(lines) + "\n"
|
|
176
|
+
Path(output).write_text(content, encoding="utf-8")
|
|
177
|
+
console.print(f"[green]✓[/green] Generated {output} with {len(env_vars)} keys (values removed)")
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@main.command()
|
|
181
|
+
@click.option("--path", "-p", default=".", help="Project root")
|
|
182
|
+
def audit(path, strict=False):
|
|
183
|
+
"""Full security audit of environment configuration (premium).
|
|
184
|
+
|
|
185
|
+
Checks for: secrets in git history, inconsistent .env files across
|
|
186
|
+
environments, missing encryption, and CI/CD exposure risks.
|
|
187
|
+
"""
|
|
188
|
+
console.print()
|
|
189
|
+
console.print("[yellow]Full security audit is a premium feature.[/yellow]")
|
|
190
|
+
console.print("Upgrade at https://kryptorious.gumroad.com/l/jbvet")
|
|
191
|
+
console.print()
|
|
192
|
+
|
|
193
|
+
# Run basic check as free preview
|
|
194
|
+
ctx = click.Context(check)
|
|
195
|
+
check.invoke(ctx)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@main.command()
|
|
199
|
+
@click.argument("file1")
|
|
200
|
+
@click.argument("file2")
|
|
201
|
+
def diff(file1, file2):
|
|
202
|
+
"""Compare two .env files and show differences (premium).
|
|
203
|
+
|
|
204
|
+
\b
|
|
205
|
+
Example:
|
|
206
|
+
envguard diff .env .env.production
|
|
207
|
+
"""
|
|
208
|
+
console.print()
|
|
209
|
+
console.print("[yellow]env diff is a premium feature.[/yellow]")
|
|
210
|
+
console.print("Upgrade at https://kryptorious.gumroad.com/l/jbvet")
|
|
211
|
+
|
|
212
|
+
v1 = _load_env(file1)
|
|
213
|
+
v2 = _load_env(file2)
|
|
214
|
+
|
|
215
|
+
all_keys = set(v1.keys()) | set(v2.keys())
|
|
216
|
+
only_in_1 = set(v1.keys()) - set(v2.keys())
|
|
217
|
+
only_in_2 = set(v2.keys()) - set(v1.keys())
|
|
218
|
+
different = {k for k in all_keys if k in v1 and k in v2 and v1[k] != v2[k]}
|
|
219
|
+
|
|
220
|
+
console.print(f"\n[bold]Preview:[/bold] {len(all_keys)} total keys")
|
|
221
|
+
if only_in_1:
|
|
222
|
+
console.print(f" Only in {file1}: {', '.join(sorted(only_in_1)[:5])}")
|
|
223
|
+
if only_in_2:
|
|
224
|
+
console.print(f" Only in {file2}: {', '.join(sorted(only_in_2)[:5])}")
|
|
225
|
+
if different:
|
|
226
|
+
console.print(f" Different values: {len(different)} keys")
|
|
227
|
+
console.print("\n[dim]Full diff requires premium upgrade.[/dim]")
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
if __name__ == "__main__":
|
|
231
|
+
main()
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kryptorious-envguard
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Environment variable management CLI — validate, generate, audit, and encrypt .env files.
|
|
5
|
+
Author: Kryptorious Quantum Biosciences, Inc.
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://devflow.sh
|
|
8
|
+
Requires-Python: >=3.9
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
Requires-Dist: click>=8.0
|
|
11
|
+
Requires-Dist: rich>=13.0
|
|
12
|
+
Requires-Dist: python-dotenv>=1.0
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
envguard/__init__.py,sha256=543ushF0dGjMf7cZE3YJbEX2mQd06DD9VkhYeD5z9hA,78
|
|
2
|
+
envguard/cli.py,sha256=YllqeIw_lCNfQWgp5ckxN9m_apEHcSLU_yylDJKoPyY,7881
|
|
3
|
+
kryptorious_envguard-1.0.0.dist-info/METADATA,sha256=r6Y9U8XgvpZ3KBotioUNrpe7M_IZL2Stm6nb9Il7G8w,491
|
|
4
|
+
kryptorious_envguard-1.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
5
|
+
kryptorious_envguard-1.0.0.dist-info/entry_points.txt,sha256=u3t7YuHqdCbAkAbK2_uy0bM0ERne6xgpvh-K26aplAI,47
|
|
6
|
+
kryptorious_envguard-1.0.0.dist-info/top_level.txt,sha256=yu47J3wd6FhM7bR522MayR5X1amyGhCGDH9fX4879M4,9
|
|
7
|
+
kryptorious_envguard-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
envguard
|