env-sentry 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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Your Name
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,125 @@
1
+ Metadata-Version: 2.4
2
+ Name: env-sentry
3
+ Version: 0.1.0
4
+ Summary: A tiny, zero-dependency CLI that keeps your .env setup safe and in sync.
5
+ Author: nour yahyaoui
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/nour-yahyaoui/env-sentry
8
+ Project-URL: Issues, https://github.com/nour-yahyaoui/env-sentry/issues
9
+ Keywords: env,dotenv,security,secrets,cli,devtools
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Software Development :: Build Tools
15
+ Classifier: Topic :: Security
16
+ Requires-Python: >=3.8
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Dynamic: license-file
20
+
21
+ # 🛡️ env-sentry
22
+
23
+ **Stop leaking secrets in `.env` files. Zero dependencies, one command.**
24
+
25
+ [![PyPI](https://img.shields.io/badge/pip%20install-env--sentry-blue)](https://pypi.org/project/env-sentry/)
26
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
27
+ [![Python 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org)
28
+ [![Zero Dependencies](https://img.shields.io/badge/dependencies-0-brightgreen)](pyproject.toml)
29
+
30
+ Every project with a `.env` file eventually has one of these moments:
31
+
32
+ - Someone's `.env` isn't in `.gitignore` and real secrets get pushed to GitHub.
33
+ - `.env.example` drifts out of sync, and a new teammate spends 20 minutes figuring out which env vars they're missing.
34
+ - A Stripe key or AWS credential gets hardcoded "just for testing" and never gets cleaned up.
35
+
36
+ `env-sentry` is a tiny CLI that catches all three, in one command, with **no dependencies to install**.
37
+
38
+ ---
39
+
40
+ ## Install
41
+
42
+ ```bash
43
+ pip install env-sentry
44
+ ```
45
+
46
+ ## Quick start
47
+
48
+ ```bash
49
+ # Scaffold .env.example and make sure .env is git-ignored
50
+ env-sentry init
51
+
52
+ # Compare .env against .env.example, flag drift and leaked secrets
53
+ env-sentry check
54
+
55
+ # Regenerate .env.example from .env (values are redacted, never copied)
56
+ env-sentry sync
57
+
58
+ # Scan the whole repo for hardcoded API keys, tokens, and private keys
59
+ env-sentry scan
60
+ ```
61
+
62
+ ### Example output
63
+
64
+ ```
65
+ $ env-sentry check
66
+
67
+ ❌ Keys in .env but missing from .env.example:
68
+ - STRIPE_SECRET_KEY
69
+ - DATABASE_URL
70
+
71
+ 🚨 .env.example may contain REAL secret values, not placeholders:
72
+ - OLD_API_KEY
73
+
74
+ 🚨 '.env' does not appear to be listed in .gitignore!
75
+ Anyone who commits will leak real secrets. Run `env-sentry init` to fix this.
76
+ ```
77
+
78
+ ```
79
+ $ env-sentry scan
80
+
81
+ config.py:14 AWS Access Key
82
+ config.py:14 Suspicious value for 'AWS_SECRET'
83
+ notes.txt:3 GitHub Token
84
+
85
+ 🚨 3 potential secret(s) found. Review before committing/pushing.
86
+ ```
87
+
88
+ ---
89
+
90
+ ## What it checks for
91
+
92
+ | Command | What it does |
93
+ |---|---|
94
+ | `init` | Creates `.env.example` if missing, adds `.env` to `.gitignore` |
95
+ | `check` | Diffs `.env` vs `.env.example`, flags real secrets accidentally left in the example file, confirms `.env` is git-ignored |
96
+ | `sync` | Rewrites `.env.example` from `.env`'s keys with placeholder values — your real secrets never touch the example file |
97
+ | `scan` | Walks the repo looking for AWS keys, GitHub/Slack tokens, Stripe live keys, private key blocks, JWTs, and suspicious `KEY=value` pairs |
98
+
99
+ Exit codes are non-zero on any issue found, so it's a one-line addition to CI or a pre-commit hook:
100
+
101
+ ```yaml
102
+ # .github/workflows/tests.yml
103
+ - run: pip install env-sentry && env-sentry check && env-sentry scan
104
+ ```
105
+
106
+ ```bash
107
+ # pre-commit hook
108
+ env-sentry scan || exit 1
109
+ ```
110
+
111
+ ---
112
+
113
+ ## Why not just use `git-secrets` / `truffleHog` / `detect-secrets`?
114
+
115
+ Those are great, heavier tools for deep git-history scanning. `env-sentry` is intentionally small: it's the **5-second daily check**, not a security audit platform. No config file, no dependencies, no setup — just an honest answer to "did I mess up my `.env` today?"
116
+
117
+ ---
118
+
119
+ ## Contributing
120
+
121
+ Issues and PRs welcome. The whole tool is a single ~250 line file (`env_sentry/cli.py`) on purpose — keep it that way.
122
+
123
+ ## License
124
+
125
+ MIT
@@ -0,0 +1,105 @@
1
+ # 🛡️ env-sentry
2
+
3
+ **Stop leaking secrets in `.env` files. Zero dependencies, one command.**
4
+
5
+ [![PyPI](https://img.shields.io/badge/pip%20install-env--sentry-blue)](https://pypi.org/project/env-sentry/)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
7
+ [![Python 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org)
8
+ [![Zero Dependencies](https://img.shields.io/badge/dependencies-0-brightgreen)](pyproject.toml)
9
+
10
+ Every project with a `.env` file eventually has one of these moments:
11
+
12
+ - Someone's `.env` isn't in `.gitignore` and real secrets get pushed to GitHub.
13
+ - `.env.example` drifts out of sync, and a new teammate spends 20 minutes figuring out which env vars they're missing.
14
+ - A Stripe key or AWS credential gets hardcoded "just for testing" and never gets cleaned up.
15
+
16
+ `env-sentry` is a tiny CLI that catches all three, in one command, with **no dependencies to install**.
17
+
18
+ ---
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ pip install env-sentry
24
+ ```
25
+
26
+ ## Quick start
27
+
28
+ ```bash
29
+ # Scaffold .env.example and make sure .env is git-ignored
30
+ env-sentry init
31
+
32
+ # Compare .env against .env.example, flag drift and leaked secrets
33
+ env-sentry check
34
+
35
+ # Regenerate .env.example from .env (values are redacted, never copied)
36
+ env-sentry sync
37
+
38
+ # Scan the whole repo for hardcoded API keys, tokens, and private keys
39
+ env-sentry scan
40
+ ```
41
+
42
+ ### Example output
43
+
44
+ ```
45
+ $ env-sentry check
46
+
47
+ ❌ Keys in .env but missing from .env.example:
48
+ - STRIPE_SECRET_KEY
49
+ - DATABASE_URL
50
+
51
+ 🚨 .env.example may contain REAL secret values, not placeholders:
52
+ - OLD_API_KEY
53
+
54
+ 🚨 '.env' does not appear to be listed in .gitignore!
55
+ Anyone who commits will leak real secrets. Run `env-sentry init` to fix this.
56
+ ```
57
+
58
+ ```
59
+ $ env-sentry scan
60
+
61
+ config.py:14 AWS Access Key
62
+ config.py:14 Suspicious value for 'AWS_SECRET'
63
+ notes.txt:3 GitHub Token
64
+
65
+ 🚨 3 potential secret(s) found. Review before committing/pushing.
66
+ ```
67
+
68
+ ---
69
+
70
+ ## What it checks for
71
+
72
+ | Command | What it does |
73
+ |---|---|
74
+ | `init` | Creates `.env.example` if missing, adds `.env` to `.gitignore` |
75
+ | `check` | Diffs `.env` vs `.env.example`, flags real secrets accidentally left in the example file, confirms `.env` is git-ignored |
76
+ | `sync` | Rewrites `.env.example` from `.env`'s keys with placeholder values — your real secrets never touch the example file |
77
+ | `scan` | Walks the repo looking for AWS keys, GitHub/Slack tokens, Stripe live keys, private key blocks, JWTs, and suspicious `KEY=value` pairs |
78
+
79
+ Exit codes are non-zero on any issue found, so it's a one-line addition to CI or a pre-commit hook:
80
+
81
+ ```yaml
82
+ # .github/workflows/tests.yml
83
+ - run: pip install env-sentry && env-sentry check && env-sentry scan
84
+ ```
85
+
86
+ ```bash
87
+ # pre-commit hook
88
+ env-sentry scan || exit 1
89
+ ```
90
+
91
+ ---
92
+
93
+ ## Why not just use `git-secrets` / `truffleHog` / `detect-secrets`?
94
+
95
+ Those are great, heavier tools for deep git-history scanning. `env-sentry` is intentionally small: it's the **5-second daily check**, not a security audit platform. No config file, no dependencies, no setup — just an honest answer to "did I mess up my `.env` today?"
96
+
97
+ ---
98
+
99
+ ## Contributing
100
+
101
+ Issues and PRs welcome. The whole tool is a single ~250 line file (`env_sentry/cli.py`) on purpose — keep it that way.
102
+
103
+ ## License
104
+
105
+ MIT
@@ -0,0 +1,3 @@
1
+ from .cli import main # noqa: F401
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,316 @@
1
+ """
2
+ env-sentry
3
+ A tiny, zero-dependency CLI that keeps your .env setup safe and sane.
4
+
5
+ Commands:
6
+ env-sentry init Scaffold .env.example and make sure .env is git-ignored
7
+ env-sentry check Compare .env against .env.example and flag drift
8
+ env-sentry sync Regenerate .env.example from .env (values redacted)
9
+ env-sentry scan Walk the repo looking for hardcoded secrets
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import argparse
15
+ import os
16
+ import re
17
+ import sys
18
+ from pathlib import Path
19
+
20
+ VERSION = "0.1.0"
21
+
22
+ # ----------------------------------------------------------------------
23
+ # .env parsing helpers
24
+ # ----------------------------------------------------------------------
25
+
26
+ _KV_RE = re.compile(r"""^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)\s*$""")
27
+
28
+
29
+ def parse_env_file(path: Path) -> dict[str, str]:
30
+ """Parse a .env-style file into a dict of key -> raw value string."""
31
+ values: dict[str, str] = {}
32
+ if not path.exists():
33
+ return values
34
+ for line in path.read_text(errors="ignore").splitlines():
35
+ stripped = line.strip()
36
+ if not stripped or stripped.startswith("#"):
37
+ continue
38
+ match = _KV_RE.match(line)
39
+ if not match:
40
+ continue
41
+ key, val = match.group(1), match.group(2).strip()
42
+ if len(val) >= 2 and val[0] == val[-1] and val[0] in ("'", '"'):
43
+ val = val[1:-1]
44
+ values[key] = val
45
+ return values
46
+
47
+
48
+ # ----------------------------------------------------------------------
49
+ # check / sync / init
50
+ # ----------------------------------------------------------------------
51
+
52
+ def cmd_check(args: argparse.Namespace) -> int:
53
+ env_path = Path(args.env)
54
+ example_path = Path(args.example)
55
+
56
+ env_vars = parse_env_file(env_path)
57
+ example_vars = parse_env_file(example_path)
58
+
59
+ exit_code = 0
60
+
61
+ if not env_path.exists():
62
+ print(f"⚠️ {env_path} not found (nothing to compare against).")
63
+ if not example_path.exists():
64
+ print(f"⚠️ {example_path} not found. Run `env-sentry sync` to create it.")
65
+ exit_code = 1
66
+
67
+ missing_in_example = sorted(set(env_vars) - set(example_vars))
68
+ missing_in_env = sorted(set(example_vars) - set(env_vars))
69
+
70
+ if missing_in_example:
71
+ exit_code = 1
72
+ print(f"\n❌ Keys in {env_path} but missing from {example_path}:")
73
+ for key in missing_in_example:
74
+ print(f" - {key}")
75
+
76
+ if missing_in_env:
77
+ print(f"\nℹ️ Keys in {example_path} but not set in {env_path}:")
78
+ for key in missing_in_env:
79
+ print(f" - {key}")
80
+
81
+ # Warn if .env.example contains what looks like a real secret
82
+ suspicious = [k for k, v in example_vars.items() if looks_like_secret(k, v)]
83
+ if suspicious:
84
+ exit_code = 1
85
+ print(f"\n🚨 {example_path} may contain REAL secret values, not placeholders:")
86
+ for key in suspicious:
87
+ print(f" - {key}")
88
+ print(" Replace these with placeholders like `your_api_key_here` before committing.")
89
+
90
+ # Warn if .env is not gitignored
91
+ if not is_gitignored(".env"):
92
+ exit_code = 1
93
+ print(f"\n🚨 '.env' does not appear to be listed in .gitignore!")
94
+ print(" Anyone who commits will leak real secrets. Run `env-sentry init` to fix this.")
95
+
96
+ if exit_code == 0:
97
+ print("✅ .env and .env.example are in sync. .env is git-ignored. Looking good.")
98
+
99
+ return exit_code
100
+
101
+
102
+ def cmd_sync(args: argparse.Namespace) -> int:
103
+ env_path = Path(args.env)
104
+ example_path = Path(args.example)
105
+
106
+ if not env_path.exists():
107
+ print(f"❌ {env_path} not found, nothing to sync from.")
108
+ return 1
109
+
110
+ env_vars = parse_env_file(env_path)
111
+ lines = ["# Auto-generated by env-sentry — fill in real values in your own .env\n"]
112
+ for key in env_vars:
113
+ lines.append(f"{key}=your_{key.lower()}_here\n")
114
+
115
+ example_path.write_text("".join(lines))
116
+ print(f"✅ Wrote {len(env_vars)} placeholder keys to {example_path}")
117
+ return 0
118
+
119
+
120
+ def cmd_init(args: argparse.Namespace) -> int:
121
+ exit_code = 0
122
+ example_path = Path(args.example)
123
+ if not example_path.exists():
124
+ example_path.write_text("# Copy this file to .env and fill in real values\n")
125
+ print(f"✅ Created {example_path}")
126
+ else:
127
+ print(f"ℹ️ {example_path} already exists, leaving it alone.")
128
+
129
+ gitignore_path = Path(".gitignore")
130
+ if not is_gitignored(".env"):
131
+ with gitignore_path.open("a") as f:
132
+ f.write("\n.env\n")
133
+ print("✅ Added '.env' to .gitignore")
134
+ else:
135
+ print("ℹ️ '.env' is already git-ignored.")
136
+
137
+ return exit_code
138
+
139
+
140
+ def is_gitignored(name: str) -> bool:
141
+ gitignore_path = Path(".gitignore")
142
+ if not gitignore_path.exists():
143
+ return False
144
+ patterns = [p.strip() for p in gitignore_path.read_text().splitlines()]
145
+ return name in patterns or f"/{name}" in patterns or "*.env" in patterns
146
+
147
+
148
+ # ----------------------------------------------------------------------
149
+ # secret detection
150
+ # ----------------------------------------------------------------------
151
+
152
+ SECRET_KEY_HINTS = re.compile(
153
+ r"(secret|token|api[_-]?key|password|passwd|access[_-]?key|private[_-]?key|"
154
+ r"auth|credential|client[_-]?secret)",
155
+ re.IGNORECASE,
156
+ )
157
+
158
+ PLACEHOLDER_RE = re.compile(
159
+ r"^(your|my|example|placeholder|xxx+|changeme|todo|<.*>|\$\{.*\}|test|fake|dummy)",
160
+ re.IGNORECASE,
161
+ )
162
+
163
+ # Well-known secret formats worth flagging outright.
164
+ KNOWN_PATTERNS = {
165
+ "AWS Access Key": re.compile(r"AKIA[0-9A-Z]{16}"),
166
+ "GitHub Token": re.compile(r"gh[pousr]_[A-Za-z0-9]{36,}"),
167
+ "Slack Token": re.compile(r"xox[baprs]-[A-Za-z0-9-]{10,}"),
168
+ "Stripe Key": re.compile(r"sk_live_[A-Za-z0-9]{20,}"),
169
+ "Private Key Block": re.compile(r"-----BEGIN (RSA|EC|OPENSSH|DSA|PGP)? ?PRIVATE KEY-----"),
170
+ "Generic JWT": re.compile(r"eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}"),
171
+ }
172
+
173
+ DEFAULT_IGNORE_DIRS = {
174
+ ".git", "node_modules", "venv", ".venv", "__pycache__", "dist", "build",
175
+ ".mypy_cache", ".pytest_cache", "target", ".next", ".tox", "vendor",
176
+ }
177
+
178
+ SCANNABLE_EXTENSIONS = {
179
+ ".py", ".js", ".ts", ".jsx", ".tsx", ".env", ".yml", ".yaml", ".json",
180
+ ".toml", ".ini", ".cfg", ".sh", ".rb", ".go", ".java", ".php", ".txt",
181
+ ".properties", ".conf",
182
+ }
183
+
184
+
185
+ def looks_like_secret(key: str, value: str) -> bool:
186
+ if not value or PLACEHOLDER_RE.match(value):
187
+ return False
188
+ if len(value) < 12:
189
+ return False
190
+ if not SECRET_KEY_HINTS.search(key):
191
+ return False
192
+ return True
193
+
194
+
195
+ def scan_line(line: str) -> list[str]:
196
+ findings = []
197
+ for name, pattern in KNOWN_PATTERNS.items():
198
+ if pattern.search(line):
199
+ findings.append(name)
200
+ match = _KV_RE.match(line)
201
+ if match:
202
+ key, val = match.group(1), match.group(2).strip().strip("'\"")
203
+ if looks_like_secret(key, val):
204
+ findings.append(f"Suspicious value for '{key}'")
205
+ return findings
206
+
207
+
208
+ def cmd_scan(args: argparse.Namespace) -> int:
209
+ root = Path(args.path)
210
+ total_findings = 0
211
+
212
+ for dirpath, dirnames, filenames in os.walk(root):
213
+ dirnames[:] = [d for d in dirnames if d not in DEFAULT_IGNORE_DIRS and not d.startswith(".git")]
214
+ for filename in filenames:
215
+ file_path = Path(dirpath) / filename
216
+ if file_path.suffix not in SCANNABLE_EXTENSIONS and filename != ".env":
217
+ continue
218
+ try:
219
+ lines = file_path.read_text(errors="ignore").splitlines()
220
+ except (UnicodeDecodeError, OSError):
221
+ continue
222
+ for lineno, line in enumerate(lines, start=1):
223
+ findings = scan_line(line)
224
+ for finding in findings:
225
+ total_findings += 1
226
+ print(f"{file_path}:{lineno} {finding}")
227
+
228
+ if total_findings == 0:
229
+ print("✅ No obvious hardcoded secrets found.")
230
+ return 0
231
+ else:
232
+ print(f"\n🚨 {total_findings} potential secret(s) found. Review before committing/pushing.")
233
+ return 1
234
+
235
+
236
+ # ----------------------------------------------------------------------
237
+ # entrypoint
238
+ # ----------------------------------------------------------------------
239
+
240
+ def build_parser() -> argparse.ArgumentParser:
241
+ parser = argparse.ArgumentParser(
242
+ prog="env-sentry",
243
+ description="Keep your .env setup safe and in sync. Zero dependencies.",
244
+ )
245
+ parser.add_argument("--version", action="version", version=f"env-sentry {VERSION}")
246
+ subparsers = parser.add_subparsers(dest="command", required=True)
247
+
248
+ p_init = subparsers.add_parser(
249
+ "init",
250
+ help="Scaffold .env.example and git-ignore .env",
251
+ description="Create .env.example if it doesn't exist yet, and make sure "
252
+ "'.env' is listed in .gitignore so real secrets never get committed.",
253
+ )
254
+ p_init.add_argument(
255
+ "--example", default=".env.example",
256
+ help="Path to the example file to create (default: .env.example)",
257
+ )
258
+ p_init.set_defaults(func=cmd_init)
259
+
260
+ p_check = subparsers.add_parser(
261
+ "check",
262
+ help="Compare .env against .env.example",
263
+ description="Diff .env against .env.example, flag any real secret values "
264
+ "left in the example file, and confirm .env is git-ignored. "
265
+ "Exits non-zero if anything looks wrong (safe to use in CI).",
266
+ )
267
+ p_check.add_argument(
268
+ "--env", default=".env",
269
+ help="Path to your real env file (default: .env)",
270
+ )
271
+ p_check.add_argument(
272
+ "--example", default=".env.example",
273
+ help="Path to the example/template file to compare against (default: .env.example)",
274
+ )
275
+ p_check.set_defaults(func=cmd_check)
276
+
277
+ p_sync = subparsers.add_parser(
278
+ "sync",
279
+ help="Regenerate .env.example from .env (values redacted)",
280
+ description="Rewrite .env.example using the keys from .env, but with every "
281
+ "value replaced by a placeholder — your real secrets are never copied.",
282
+ )
283
+ p_sync.add_argument(
284
+ "--env", default=".env",
285
+ help="Path to your real env file to read keys from (default: .env)",
286
+ )
287
+ p_sync.add_argument(
288
+ "--example", default=".env.example",
289
+ help="Path to the example file to write (default: .env.example)",
290
+ )
291
+ p_sync.set_defaults(func=cmd_sync)
292
+
293
+ p_scan = subparsers.add_parser(
294
+ "scan",
295
+ help="Scan the repo for hardcoded secrets",
296
+ description="Recursively scan source files for AWS/GitHub/Slack/Stripe keys, "
297
+ "private key blocks, JWTs, and suspicious KEY=value pairs. "
298
+ "Exits non-zero if anything is found (safe to use as a pre-commit hook).",
299
+ )
300
+ p_scan.add_argument(
301
+ "path", nargs="?", default=".",
302
+ help="Directory to scan (default: current directory)",
303
+ )
304
+ p_scan.set_defaults(func=cmd_scan)
305
+
306
+ return parser
307
+
308
+
309
+ def main(argv: list[str] | None = None) -> int:
310
+ parser = build_parser()
311
+ args = parser.parse_args(argv)
312
+ return args.func(args)
313
+
314
+
315
+ if __name__ == "__main__":
316
+ sys.exit(main())
@@ -0,0 +1,125 @@
1
+ Metadata-Version: 2.4
2
+ Name: env-sentry
3
+ Version: 0.1.0
4
+ Summary: A tiny, zero-dependency CLI that keeps your .env setup safe and in sync.
5
+ Author: nour yahyaoui
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/nour-yahyaoui/env-sentry
8
+ Project-URL: Issues, https://github.com/nour-yahyaoui/env-sentry/issues
9
+ Keywords: env,dotenv,security,secrets,cli,devtools
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Software Development :: Build Tools
15
+ Classifier: Topic :: Security
16
+ Requires-Python: >=3.8
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Dynamic: license-file
20
+
21
+ # 🛡️ env-sentry
22
+
23
+ **Stop leaking secrets in `.env` files. Zero dependencies, one command.**
24
+
25
+ [![PyPI](https://img.shields.io/badge/pip%20install-env--sentry-blue)](https://pypi.org/project/env-sentry/)
26
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
27
+ [![Python 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org)
28
+ [![Zero Dependencies](https://img.shields.io/badge/dependencies-0-brightgreen)](pyproject.toml)
29
+
30
+ Every project with a `.env` file eventually has one of these moments:
31
+
32
+ - Someone's `.env` isn't in `.gitignore` and real secrets get pushed to GitHub.
33
+ - `.env.example` drifts out of sync, and a new teammate spends 20 minutes figuring out which env vars they're missing.
34
+ - A Stripe key or AWS credential gets hardcoded "just for testing" and never gets cleaned up.
35
+
36
+ `env-sentry` is a tiny CLI that catches all three, in one command, with **no dependencies to install**.
37
+
38
+ ---
39
+
40
+ ## Install
41
+
42
+ ```bash
43
+ pip install env-sentry
44
+ ```
45
+
46
+ ## Quick start
47
+
48
+ ```bash
49
+ # Scaffold .env.example and make sure .env is git-ignored
50
+ env-sentry init
51
+
52
+ # Compare .env against .env.example, flag drift and leaked secrets
53
+ env-sentry check
54
+
55
+ # Regenerate .env.example from .env (values are redacted, never copied)
56
+ env-sentry sync
57
+
58
+ # Scan the whole repo for hardcoded API keys, tokens, and private keys
59
+ env-sentry scan
60
+ ```
61
+
62
+ ### Example output
63
+
64
+ ```
65
+ $ env-sentry check
66
+
67
+ ❌ Keys in .env but missing from .env.example:
68
+ - STRIPE_SECRET_KEY
69
+ - DATABASE_URL
70
+
71
+ 🚨 .env.example may contain REAL secret values, not placeholders:
72
+ - OLD_API_KEY
73
+
74
+ 🚨 '.env' does not appear to be listed in .gitignore!
75
+ Anyone who commits will leak real secrets. Run `env-sentry init` to fix this.
76
+ ```
77
+
78
+ ```
79
+ $ env-sentry scan
80
+
81
+ config.py:14 AWS Access Key
82
+ config.py:14 Suspicious value for 'AWS_SECRET'
83
+ notes.txt:3 GitHub Token
84
+
85
+ 🚨 3 potential secret(s) found. Review before committing/pushing.
86
+ ```
87
+
88
+ ---
89
+
90
+ ## What it checks for
91
+
92
+ | Command | What it does |
93
+ |---|---|
94
+ | `init` | Creates `.env.example` if missing, adds `.env` to `.gitignore` |
95
+ | `check` | Diffs `.env` vs `.env.example`, flags real secrets accidentally left in the example file, confirms `.env` is git-ignored |
96
+ | `sync` | Rewrites `.env.example` from `.env`'s keys with placeholder values — your real secrets never touch the example file |
97
+ | `scan` | Walks the repo looking for AWS keys, GitHub/Slack tokens, Stripe live keys, private key blocks, JWTs, and suspicious `KEY=value` pairs |
98
+
99
+ Exit codes are non-zero on any issue found, so it's a one-line addition to CI or a pre-commit hook:
100
+
101
+ ```yaml
102
+ # .github/workflows/tests.yml
103
+ - run: pip install env-sentry && env-sentry check && env-sentry scan
104
+ ```
105
+
106
+ ```bash
107
+ # pre-commit hook
108
+ env-sentry scan || exit 1
109
+ ```
110
+
111
+ ---
112
+
113
+ ## Why not just use `git-secrets` / `truffleHog` / `detect-secrets`?
114
+
115
+ Those are great, heavier tools for deep git-history scanning. `env-sentry` is intentionally small: it's the **5-second daily check**, not a security audit platform. No config file, no dependencies, no setup — just an honest answer to "did I mess up my `.env` today?"
116
+
117
+ ---
118
+
119
+ ## Contributing
120
+
121
+ Issues and PRs welcome. The whole tool is a single ~250 line file (`env_sentry/cli.py`) on purpose — keep it that way.
122
+
123
+ ## License
124
+
125
+ MIT
@@ -0,0 +1,11 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ env_sentry/__init__.py
5
+ env_sentry/cli.py
6
+ env_sentry.egg-info/PKG-INFO
7
+ env_sentry.egg-info/SOURCES.txt
8
+ env_sentry.egg-info/dependency_links.txt
9
+ env_sentry.egg-info/entry_points.txt
10
+ env_sentry.egg-info/top_level.txt
11
+ tests/test_basic.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ env-sentry = env_sentry.cli:main
@@ -0,0 +1 @@
1
+ env_sentry
@@ -0,0 +1,31 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "env-sentry"
7
+ version = "0.1.0"
8
+ description = "A tiny, zero-dependency CLI that keeps your .env setup safe and in sync."
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "nour yahyaoui" }]
13
+ keywords = ["env", "dotenv", "security", "secrets", "cli", "devtools"]
14
+ classifiers = [
15
+ "Environment :: Console",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3",
19
+ "Topic :: Software Development :: Build Tools",
20
+ "Topic :: Security",
21
+ ]
22
+
23
+ [project.urls]
24
+ Homepage = "https://github.com/nour-yahyaoui/env-sentry"
25
+ Issues = "https://github.com/nour-yahyaoui/env-sentry/issues"
26
+
27
+ [project.scripts]
28
+ env-sentry = "env_sentry.cli:main"
29
+
30
+ [tool.setuptools]
31
+ packages = ["env_sentry"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,44 @@
1
+ import subprocess
2
+ import sys
3
+ from pathlib import Path
4
+
5
+
6
+ def run(args, cwd):
7
+ return subprocess.run(
8
+ [sys.executable, "-m", "env_sentry.cli", *args],
9
+ cwd=cwd,
10
+ capture_output=True,
11
+ text=True,
12
+ )
13
+
14
+
15
+ def test_init_and_check(tmp_path: Path):
16
+ (tmp_path / ".env").write_text("API_KEY=abc123\nDEBUG=true\n")
17
+
18
+ result = run(["init"], cwd=tmp_path)
19
+ assert "Created" in result.stdout or ".env.example" in result.stdout
20
+
21
+ result = run(["sync"], cwd=tmp_path)
22
+ assert result.returncode == 0
23
+
24
+ result = run(["check"], cwd=tmp_path)
25
+ assert result.returncode == 0
26
+ assert "in sync" in result.stdout
27
+
28
+
29
+ def test_check_flags_missing_key(tmp_path: Path):
30
+ (tmp_path / ".env").write_text("API_KEY=abc123\nEXTRA_KEY=zzz\n")
31
+ (tmp_path / ".env.example").write_text("API_KEY=your_api_key_here\n")
32
+ (tmp_path / ".gitignore").write_text(".env\n")
33
+
34
+ result = run(["check"], cwd=tmp_path)
35
+ assert result.returncode == 1
36
+ assert "EXTRA_KEY" in result.stdout
37
+
38
+
39
+ def test_scan_detects_known_pattern(tmp_path: Path):
40
+ (tmp_path / "config.py").write_text('AWS_KEY = "AKIAABCDEFGHIJKLMNOP"\n')
41
+
42
+ result = run(["scan", "."], cwd=tmp_path)
43
+ assert result.returncode == 1
44
+ assert "AWS Access Key" in result.stdout