envshield 4.7.3__tar.gz → 4.7.4__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.
- {envshield-4.7.3 → envshield-4.7.4}/PKG-INFO +82 -6
- {envshield-4.7.3 → envshield-4.7.4}/README.md +81 -5
- envshield-4.7.4/envshield/__init__.py +1 -0
- {envshield-4.7.3 → envshield-4.7.4}/envshield/cli.py +6 -0
- {envshield-4.7.3 → envshield-4.7.4}/envshield/config/manager.py +16 -0
- {envshield-4.7.3 → envshield-4.7.4}/envshield/core/hooks_manager.py +56 -10
- {envshield-4.7.3 → envshield-4.7.4}/envshield/core/scanner.py +10 -1
- {envshield-4.7.3 → envshield-4.7.4}/envshield/core/service_discovery.py +19 -2
- {envshield-4.7.3 → envshield-4.7.4}/envshield.egg-info/PKG-INFO +82 -6
- {envshield-4.7.3 → envshield-4.7.4}/pyproject.toml +1 -1
- envshield-4.7.3/envshield/__init__.py +0 -1
- {envshield-4.7.3 → envshield-4.7.4}/LICENSE.md +0 -0
- {envshield-4.7.3 → envshield-4.7.4}/envshield/__main__.py +0 -0
- {envshield-4.7.3 → envshield-4.7.4}/envshield/classifier/__init__.py +0 -0
- {envshield-4.7.3 → envshield-4.7.4}/envshield/classifier/context_classifier.py +0 -0
- {envshield-4.7.3 → envshield-4.7.4}/envshield/classifier/tokenizer.py +0 -0
- {envshield-4.7.3 → envshield-4.7.4}/envshield/config/__init__.py +0 -0
- {envshield-4.7.3 → envshield-4.7.4}/envshield/core/__init__.py +0 -0
- {envshield-4.7.3 → envshield-4.7.4}/envshield/core/contract_diff.py +0 -0
- {envshield-4.7.3 → envshield-4.7.4}/envshield/core/dependency_diff.py +0 -0
- {envshield-4.7.3 → envshield-4.7.4}/envshield/core/dependency_snapshot.py +0 -0
- {envshield-4.7.3 → envshield-4.7.4}/envshield/core/discovery.py +0 -0
- {envshield-4.7.3 → envshield-4.7.4}/envshield/core/doctor.py +0 -0
- {envshield-4.7.3 → envshield-4.7.4}/envshield/core/enforcement.py +0 -0
- {envshield-4.7.3 → envshield-4.7.4}/envshield/core/exceptions.py +0 -0
- {envshield-4.7.3 → envshield-4.7.4}/envshield/core/explain.py +0 -0
- {envshield-4.7.3 → envshield-4.7.4}/envshield/core/file_updater.py +0 -0
- {envshield-4.7.3 → envshield-4.7.4}/envshield/core/generator.py +0 -0
- {envshield-4.7.3 → envshield-4.7.4}/envshield/core/importer.py +0 -0
- {envshield-4.7.3 → envshield-4.7.4}/envshield/core/inspector.py +0 -0
- {envshield-4.7.3 → envshield-4.7.4}/envshield/core/schema_manager.py +0 -0
- {envshield-4.7.3 → envshield-4.7.4}/envshield/core/schema_snapshot.py +0 -0
- {envshield-4.7.3 → envshield-4.7.4}/envshield/core/schema_types.py +0 -0
- {envshield-4.7.3 → envshield-4.7.4}/envshield/core/service_manager.py +0 -0
- {envshield-4.7.3 → envshield-4.7.4}/envshield/core/setup_manager.py +0 -0
- {envshield-4.7.3 → envshield-4.7.4}/envshield/parsers/__init__.py +0 -0
- {envshield-4.7.3 → envshield-4.7.4}/envshield/parsers/_base.py +0 -0
- {envshield-4.7.3 → envshield-4.7.4}/envshield/parsers/_deployment.py +0 -0
- {envshield-4.7.3 → envshield-4.7.4}/envshield/parsers/_docker_compose.py +0 -0
- {envshield-4.7.3 → envshield-4.7.4}/envshield/parsers/_dotenv.py +0 -0
- {envshield-4.7.3 → envshield-4.7.4}/envshield/parsers/_kubernetes.py +0 -0
- {envshield-4.7.3 → envshield-4.7.4}/envshield/parsers/_python.py +0 -0
- {envshield-4.7.3 → envshield-4.7.4}/envshield/parsers/factory.py +0 -0
- {envshield-4.7.3 → envshield-4.7.4}/envshield/state.py +0 -0
- {envshield-4.7.3 → envshield-4.7.4}/envshield/utils/__init__.py +0 -0
- {envshield-4.7.3 → envshield-4.7.4}/envshield/utils/git_utils.py +0 -0
- {envshield-4.7.3 → envshield-4.7.4}/envshield/utils/paths.py +0 -0
- {envshield-4.7.3 → envshield-4.7.4}/envshield.egg-info/SOURCES.txt +0 -0
- {envshield-4.7.3 → envshield-4.7.4}/envshield.egg-info/dependency_links.txt +0 -0
- {envshield-4.7.3 → envshield-4.7.4}/envshield.egg-info/entry_points.txt +0 -0
- {envshield-4.7.3 → envshield-4.7.4}/envshield.egg-info/requires.txt +0 -0
- {envshield-4.7.3 → envshield-4.7.4}/envshield.egg-info/top_level.txt +0 -0
- {envshield-4.7.3 → envshield-4.7.4}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: envshield
|
|
3
|
-
Version: 4.7.
|
|
3
|
+
Version: 4.7.4
|
|
4
4
|
Summary: EnvShield: environment variables as a version-controlled configuration contract.
|
|
5
5
|
Author-email: Rabbil Yasar Sajal <rabbilyasar@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -270,7 +270,7 @@ Every command above documents its own options with `--help`; the full reference
|
|
|
270
270
|
|
|
271
271
|
## Git / CI workflow
|
|
272
272
|
|
|
273
|
-
This is where the schema stops being documentation and starts being enforcement. Three concrete things EnvShield catches
|
|
273
|
+
This is where the schema stops being documentation and starts being enforcement. Three concrete things EnvShield catches before a change reaches production:
|
|
274
274
|
|
|
275
275
|
**A new environment variable used in code but missing from the schema:**
|
|
276
276
|
|
|
@@ -284,12 +284,14 @@ envshield undeclared origin/main HEAD --service api
|
|
|
284
284
|
envshield schema diff origin/main HEAD --service api
|
|
285
285
|
```
|
|
286
286
|
|
|
287
|
-
**A secret accidentally introduced into a commit
|
|
287
|
+
**A secret accidentally introduced into a commit:**
|
|
288
288
|
|
|
289
289
|
```bash
|
|
290
290
|
envshield hook install
|
|
291
291
|
```
|
|
292
292
|
|
|
293
|
+
The pre-commit hook runs `scan --staged --enforce`, applying classification-based enforcement with interactive override for high-confidence findings on local commits, and strict blocking in CI. See [Secret safety](#secret-safety) for enforcement behavior details.
|
|
294
|
+
|
|
293
295
|
In CI, diff against the merge-base, not the base branch's current tip (which can move while the PR is open):
|
|
294
296
|
|
|
295
297
|
```yaml
|
|
@@ -466,10 +468,84 @@ Source-code discovery (what powers `undeclared` and `explain`'s "used in source"
|
|
|
466
468
|
|
|
467
469
|
## Secret safety
|
|
468
470
|
|
|
469
|
-
A supporting check alongside the contract, not the product itself. `secret = true` on a field tells EnvShield (and `setup`, and code generation) that it's sensitive — the schema records that fact, never the actual value.
|
|
471
|
+
A supporting check alongside the contract, not the product itself. `secret = true` on a field tells EnvShield (and `setup`, and code generation) that it's sensitive — the schema records that fact, never the actual value.
|
|
472
|
+
|
|
473
|
+
### Secret scanning
|
|
474
|
+
|
|
475
|
+
**`envshield scan`** looks for hardcoded secrets by pattern (Stripe, AWS, GitHub tokens, and more), and for env-var reads the schema doesn't declare, in one pass. Values are always redacted in output — a Stripe *publishable* key (`pk_...`) is never flagged, since detection matches the secret-key pattern (`sk_...`) by shape, not the variable's name.
|
|
476
|
+
|
|
477
|
+
### Context-aware classification
|
|
478
|
+
|
|
479
|
+
Python candidates are analyzed for syntactic context to distinguish code patterns from secret values:
|
|
480
|
+
|
|
481
|
+
```python
|
|
482
|
+
# Classified as code (suppressed):
|
|
483
|
+
client(key=CONFIG_CONSTANT)
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def handler(api_key: str): ...
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
# Classified as likely secret (detected):
|
|
490
|
+
client(key="sk_live_abc123...")
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
This reduces false positives on ordinary code patterns without broadly suppressing secret-shaped values. Classification is lightweight and syntactic — it recognizes type annotations, function arguments, and identifier references, but doesn't perform semantic analysis beyond what the tokenizer can see.
|
|
494
|
+
|
|
495
|
+
### Git hook enforcement
|
|
496
|
+
|
|
497
|
+
**`envshield hook install`** wires secret scanning into your Git workflow with classification-based enforcement:
|
|
498
|
+
|
|
499
|
+
```bash
|
|
500
|
+
envshield hook install
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
The pre-commit hook runs `scan --staged --enforce`, which scans only staged content and applies an enforcement policy based on classification confidence.
|
|
504
|
+
|
|
505
|
+
#### Enforcement behavior
|
|
506
|
+
|
|
507
|
+
| Finding type | Interactive (local commit) | Non-interactive (CI) |
|
|
508
|
+
|---|---|---|
|
|
509
|
+
| No findings | Allow | Allow |
|
|
510
|
+
| Clearly code-shaped | Suppressed | Suppressed |
|
|
511
|
+
| Ambiguous | Block | Block |
|
|
512
|
+
| High-confidence secret | Block + explicit override | Block |
|
|
513
|
+
|
|
514
|
+
**Clearly code-shaped** candidates (type annotations, function keyword arguments, etc.) are suppressed by the classifier and don't produce findings.
|
|
515
|
+
|
|
516
|
+
**Ambiguous** findings — insufficient context to confidently classify — block without override in both modes.
|
|
517
|
+
|
|
518
|
+
**High-confidence secrets** (string literals matching secret patterns) block by default but offer an interactive override for local commits:
|
|
519
|
+
|
|
520
|
+
```
|
|
521
|
+
⚠ EnvShield found potential secrets
|
|
522
|
+
|
|
523
|
+
HIGH CONFIDENCE
|
|
524
|
+
src/config.py:42 Generic API Key [REDACTED (30 chars)]
|
|
525
|
+
|
|
526
|
+
[1] Abort commit
|
|
527
|
+
[2] Commit anyway
|
|
528
|
+
|
|
529
|
+
Choice: 2
|
|
530
|
+
|
|
531
|
+
Type COMMIT ANYWAY to confirm: COMMIT ANYWAY
|
|
532
|
+
|
|
533
|
+
Proceeding with commit. The finding remains in the repository history.
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
The override requires exact confirmation text — `yes` or `Y` won't work. EOF (Ctrl+D) or Ctrl+C aborts. Raw secret values are never displayed in enforcement output.
|
|
537
|
+
|
|
538
|
+
In CI or other non-interactive contexts, high-confidence findings block with no prompt — the commit is refused, and the scan exits non-zero.
|
|
539
|
+
|
|
540
|
+
#### Scanning modes
|
|
541
|
+
|
|
542
|
+
```bash
|
|
543
|
+
envshield scan # Audit entire codebase
|
|
544
|
+
envshield scan --staged # Scan only staged Git content
|
|
545
|
+
envshield scan --staged --enforce # Staged scan + enforcement policy
|
|
546
|
+
```
|
|
470
547
|
|
|
471
|
-
-
|
|
472
|
-
- **`envshield hook install`** wires `scan --staged` into a pre-commit hook, so a real secret is caught before it's committed, not after.
|
|
548
|
+
The pre-commit hook uses the third form. `scan` without flags is for repository-wide audits, not commit-time enforcement.
|
|
473
549
|
|
|
474
550
|
---
|
|
475
551
|
|
|
@@ -211,7 +211,7 @@ Every command above documents its own options with `--help`; the full reference
|
|
|
211
211
|
|
|
212
212
|
## Git / CI workflow
|
|
213
213
|
|
|
214
|
-
This is where the schema stops being documentation and starts being enforcement. Three concrete things EnvShield catches
|
|
214
|
+
This is where the schema stops being documentation and starts being enforcement. Three concrete things EnvShield catches before a change reaches production:
|
|
215
215
|
|
|
216
216
|
**A new environment variable used in code but missing from the schema:**
|
|
217
217
|
|
|
@@ -225,12 +225,14 @@ envshield undeclared origin/main HEAD --service api
|
|
|
225
225
|
envshield schema diff origin/main HEAD --service api
|
|
226
226
|
```
|
|
227
227
|
|
|
228
|
-
**A secret accidentally introduced into a commit
|
|
228
|
+
**A secret accidentally introduced into a commit:**
|
|
229
229
|
|
|
230
230
|
```bash
|
|
231
231
|
envshield hook install
|
|
232
232
|
```
|
|
233
233
|
|
|
234
|
+
The pre-commit hook runs `scan --staged --enforce`, applying classification-based enforcement with interactive override for high-confidence findings on local commits, and strict blocking in CI. See [Secret safety](#secret-safety) for enforcement behavior details.
|
|
235
|
+
|
|
234
236
|
In CI, diff against the merge-base, not the base branch's current tip (which can move while the PR is open):
|
|
235
237
|
|
|
236
238
|
```yaml
|
|
@@ -407,10 +409,84 @@ Source-code discovery (what powers `undeclared` and `explain`'s "used in source"
|
|
|
407
409
|
|
|
408
410
|
## Secret safety
|
|
409
411
|
|
|
410
|
-
A supporting check alongside the contract, not the product itself. `secret = true` on a field tells EnvShield (and `setup`, and code generation) that it's sensitive — the schema records that fact, never the actual value.
|
|
412
|
+
A supporting check alongside the contract, not the product itself. `secret = true` on a field tells EnvShield (and `setup`, and code generation) that it's sensitive — the schema records that fact, never the actual value.
|
|
413
|
+
|
|
414
|
+
### Secret scanning
|
|
415
|
+
|
|
416
|
+
**`envshield scan`** looks for hardcoded secrets by pattern (Stripe, AWS, GitHub tokens, and more), and for env-var reads the schema doesn't declare, in one pass. Values are always redacted in output — a Stripe *publishable* key (`pk_...`) is never flagged, since detection matches the secret-key pattern (`sk_...`) by shape, not the variable's name.
|
|
417
|
+
|
|
418
|
+
### Context-aware classification
|
|
419
|
+
|
|
420
|
+
Python candidates are analyzed for syntactic context to distinguish code patterns from secret values:
|
|
421
|
+
|
|
422
|
+
```python
|
|
423
|
+
# Classified as code (suppressed):
|
|
424
|
+
client(key=CONFIG_CONSTANT)
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def handler(api_key: str): ...
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
# Classified as likely secret (detected):
|
|
431
|
+
client(key="sk_live_abc123...")
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
This reduces false positives on ordinary code patterns without broadly suppressing secret-shaped values. Classification is lightweight and syntactic — it recognizes type annotations, function arguments, and identifier references, but doesn't perform semantic analysis beyond what the tokenizer can see.
|
|
435
|
+
|
|
436
|
+
### Git hook enforcement
|
|
437
|
+
|
|
438
|
+
**`envshield hook install`** wires secret scanning into your Git workflow with classification-based enforcement:
|
|
439
|
+
|
|
440
|
+
```bash
|
|
441
|
+
envshield hook install
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
The pre-commit hook runs `scan --staged --enforce`, which scans only staged content and applies an enforcement policy based on classification confidence.
|
|
445
|
+
|
|
446
|
+
#### Enforcement behavior
|
|
447
|
+
|
|
448
|
+
| Finding type | Interactive (local commit) | Non-interactive (CI) |
|
|
449
|
+
|---|---|---|
|
|
450
|
+
| No findings | Allow | Allow |
|
|
451
|
+
| Clearly code-shaped | Suppressed | Suppressed |
|
|
452
|
+
| Ambiguous | Block | Block |
|
|
453
|
+
| High-confidence secret | Block + explicit override | Block |
|
|
454
|
+
|
|
455
|
+
**Clearly code-shaped** candidates (type annotations, function keyword arguments, etc.) are suppressed by the classifier and don't produce findings.
|
|
456
|
+
|
|
457
|
+
**Ambiguous** findings — insufficient context to confidently classify — block without override in both modes.
|
|
458
|
+
|
|
459
|
+
**High-confidence secrets** (string literals matching secret patterns) block by default but offer an interactive override for local commits:
|
|
460
|
+
|
|
461
|
+
```
|
|
462
|
+
⚠ EnvShield found potential secrets
|
|
463
|
+
|
|
464
|
+
HIGH CONFIDENCE
|
|
465
|
+
src/config.py:42 Generic API Key [REDACTED (30 chars)]
|
|
466
|
+
|
|
467
|
+
[1] Abort commit
|
|
468
|
+
[2] Commit anyway
|
|
469
|
+
|
|
470
|
+
Choice: 2
|
|
471
|
+
|
|
472
|
+
Type COMMIT ANYWAY to confirm: COMMIT ANYWAY
|
|
473
|
+
|
|
474
|
+
Proceeding with commit. The finding remains in the repository history.
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
The override requires exact confirmation text — `yes` or `Y` won't work. EOF (Ctrl+D) or Ctrl+C aborts. Raw secret values are never displayed in enforcement output.
|
|
478
|
+
|
|
479
|
+
In CI or other non-interactive contexts, high-confidence findings block with no prompt — the commit is refused, and the scan exits non-zero.
|
|
480
|
+
|
|
481
|
+
#### Scanning modes
|
|
482
|
+
|
|
483
|
+
```bash
|
|
484
|
+
envshield scan # Audit entire codebase
|
|
485
|
+
envshield scan --staged # Scan only staged Git content
|
|
486
|
+
envshield scan --staged --enforce # Staged scan + enforcement policy
|
|
487
|
+
```
|
|
411
488
|
|
|
412
|
-
-
|
|
413
|
-
- **`envshield hook install`** wires `scan --staged` into a pre-commit hook, so a real secret is caught before it's committed, not after.
|
|
489
|
+
The pre-commit hook uses the third form. `scan` without flags is for repository-wide audits, not commit-time enforcement.
|
|
414
490
|
|
|
415
491
|
---
|
|
416
492
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "4.7.4"
|
|
@@ -1128,6 +1128,12 @@ def _render_dependency_change_table(
|
|
|
1128
1128
|
console.print(
|
|
1129
1129
|
"\n[bold red]New source dependencies are missing from the contract.[/bold red]"
|
|
1130
1130
|
)
|
|
1131
|
+
console.print(
|
|
1132
|
+
"[bold]Suggestion:[/bold] Review these -- not every read belongs "
|
|
1133
|
+
"in the contract. For the ones that do, add them to "
|
|
1134
|
+
"'env.schema.toml' (hand-edited, not generated). Re-run "
|
|
1135
|
+
"'envshield undeclared' to confirm."
|
|
1136
|
+
)
|
|
1131
1137
|
|
|
1132
1138
|
|
|
1133
1139
|
def _print_dependency_error(
|
|
@@ -437,6 +437,22 @@ def add_service(
|
|
|
437
437
|
entry["config_source"] = config_source
|
|
438
438
|
services[name] = entry
|
|
439
439
|
|
|
440
|
+
if (
|
|
441
|
+
entry.get("local_file")
|
|
442
|
+
and not entry.get("example_file")
|
|
443
|
+
and not entry["local_file"].endswith(".py")
|
|
444
|
+
and os.path.basename(entry["local_file"]) != ".env"
|
|
445
|
+
):
|
|
446
|
+
console.print(
|
|
447
|
+
f"[yellow]Note:[/yellow] service '{name}' overrides local_file to "
|
|
448
|
+
f"'{entry['local_file']}' but not example_file -- the template "
|
|
449
|
+
"will still default to '.env.example' in this service's "
|
|
450
|
+
"directory. If this local_file follows a different naming "
|
|
451
|
+
"convention (e.g. '.env.local' pairs with '.env.local.example'), "
|
|
452
|
+
"set example_file explicitly too, or Template Sync will look at "
|
|
453
|
+
"the wrong file."
|
|
454
|
+
)
|
|
455
|
+
|
|
440
456
|
with open(CONFIG_FILE_NAME, "w") as f:
|
|
441
457
|
yaml.dump(config, f, sort_keys=False, indent=2)
|
|
442
458
|
|
|
@@ -145,24 +145,70 @@ class HooksManager:
|
|
|
145
145
|
except EnvShieldException as e:
|
|
146
146
|
console.print(f"[bold yellow]⚠️ Warning:[/] {e}")
|
|
147
147
|
|
|
148
|
+
def _hook_status_line(
|
|
149
|
+
self, hooks_dir: str, filename: str, label: str, generator
|
|
150
|
+
) -> str:
|
|
151
|
+
"""
|
|
152
|
+
Describes one installed hook: missing, up to date, or stale --
|
|
153
|
+
present, and still carrying EnvShield's marker/shape, but no longer
|
|
154
|
+
byte-for-byte what EnvShield would generate right now (e.g. a
|
|
155
|
+
service was added to envshield.yml after this hook was installed,
|
|
156
|
+
so it's missing that service's block; or the file was hand-edited).
|
|
157
|
+
|
|
158
|
+
This is purely informational: unlike `are_hooks_installed` (used by
|
|
159
|
+
`hook install`/`uninstall`'s exact-match ownership check -- see
|
|
160
|
+
BL-119/BL-120), staleness here never gates or changes what those
|
|
161
|
+
commands do. A stale hook is not "not installed" -- it still runs
|
|
162
|
+
and still protects whatever it was generated to protect; this only
|
|
163
|
+
surfaces that it may no longer match the project's current config,
|
|
164
|
+
so nothing here needed to touch that exact-match safety invariant.
|
|
165
|
+
"""
|
|
166
|
+
path = os.path.join(hooks_dir, filename)
|
|
167
|
+
if not os.path.exists(path):
|
|
168
|
+
return f"[dim]✗ {label}[/dim]"
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
with open(path, "r") as f:
|
|
172
|
+
installed_content = f.read()
|
|
173
|
+
except OSError:
|
|
174
|
+
return f"[green]✓ {label}[/green]"
|
|
175
|
+
|
|
176
|
+
if installed_content == generator():
|
|
177
|
+
return f"[green]✓ {label}[/green]"
|
|
178
|
+
|
|
179
|
+
return (
|
|
180
|
+
f"[yellow]✓ {label} (stale -- doesn't match what EnvShield would "
|
|
181
|
+
"generate now; run 'envshield hook install --yes' to refresh)[/yellow]"
|
|
182
|
+
)
|
|
183
|
+
|
|
148
184
|
def print_hook_status(self) -> None:
|
|
149
185
|
"""Print the current status of installed hooks."""
|
|
150
186
|
if not self.git_root:
|
|
151
187
|
console.print("[dim]Not in a git repository.[/dim]")
|
|
152
188
|
return
|
|
153
189
|
|
|
154
|
-
|
|
190
|
+
# Import here to avoid circular imports (same pattern as
|
|
191
|
+
# _do_install_hooks above).
|
|
192
|
+
from . import scanner
|
|
155
193
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
else:
|
|
160
|
-
status.append("[dim]✗ pre-commit hook[/dim]")
|
|
194
|
+
hooks_dir = git_utils.get_hooks_dir() or os.path.join(
|
|
195
|
+
self.git_root, ".git", "hooks"
|
|
196
|
+
)
|
|
161
197
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
198
|
+
status = [
|
|
199
|
+
self._hook_status_line(
|
|
200
|
+
hooks_dir,
|
|
201
|
+
"pre-commit",
|
|
202
|
+
"pre-commit hook (secret scanning)",
|
|
203
|
+
scanner._generate_pre_commit_hook_content,
|
|
204
|
+
),
|
|
205
|
+
self._hook_status_line(
|
|
206
|
+
hooks_dir,
|
|
207
|
+
"post-merge",
|
|
208
|
+
"post-merge hook (config change detection)",
|
|
209
|
+
scanner._generate_post_merge_hook_content,
|
|
210
|
+
),
|
|
211
|
+
]
|
|
166
212
|
|
|
167
213
|
console.print("\n[bold]Git Hooks:[/bold]")
|
|
168
214
|
for s in status:
|
|
@@ -1387,6 +1387,12 @@ def run_scan(
|
|
|
1387
1387
|
finding["variable_name"],
|
|
1388
1388
|
)
|
|
1389
1389
|
console.print(undeclared_table)
|
|
1390
|
+
console.print(
|
|
1391
|
+
"\n[bold]Suggestion:[/bold] Review these -- not every read "
|
|
1392
|
+
"belongs in the contract. For the ones that do, add them to "
|
|
1393
|
+
"'env.schema.toml' (hand-edited, not generated), then re-stage "
|
|
1394
|
+
"and commit again."
|
|
1395
|
+
)
|
|
1390
1396
|
|
|
1391
1397
|
console.print(
|
|
1392
1398
|
"\n[bold red]Commit aborted. Please fix the issues above before committing.[/bold red]"
|
|
@@ -1438,7 +1444,10 @@ def run_scan(
|
|
|
1438
1444
|
)
|
|
1439
1445
|
console.print(undeclared_table)
|
|
1440
1446
|
console.print(
|
|
1441
|
-
"\n[bold]Suggestion:[/bold]
|
|
1447
|
+
"\n[bold]Suggestion:[/bold] Review these -- not every read belongs "
|
|
1448
|
+
"in the contract. For the ones that do, add them to "
|
|
1449
|
+
"'env.schema.toml' (hand-edited, not generated). Re-run "
|
|
1450
|
+
"'envshield scan' to confirm."
|
|
1442
1451
|
)
|
|
1443
1452
|
|
|
1444
1453
|
# A skip means eligible content was never actually inspected -- this
|
|
@@ -291,13 +291,30 @@ def detect_env_style(service_dir: str) -> Dict[str, Optional[str]]:
|
|
|
291
291
|
('<service_dir>/.env' / '<service_dir>/.env.example') -- i.e. for the
|
|
292
292
|
python format, when the real dotenv file found isn't literally named
|
|
293
293
|
'.env', or when only a template was found under a non-standard name.
|
|
294
|
+
|
|
295
|
+
When the real file isn't '.env' (e.g. Next.js's '.env.local'), its
|
|
296
|
+
paired template conventionally isn't '.env.example' either -- it's
|
|
297
|
+
'<real_file>.example' ('.env.local' -> '.env.local.example'). Without
|
|
298
|
+
checking for that file specifically, every downstream command
|
|
299
|
+
(doctor's Template Sync, 'schema sync', the pre-commit hook's
|
|
300
|
+
post-schema-change check) would keep resolving the untouched default
|
|
301
|
+
'.env.example' instead, reporting a real, in-sync template as missing.
|
|
302
|
+
This only sets `example_file` when that paired file is actually found
|
|
303
|
+
on disk -- evidence-gated, like the rest of this module's detection,
|
|
304
|
+
not guessed from the name alone.
|
|
294
305
|
"""
|
|
295
306
|
real_file = _find_real_dotenv_file(service_dir)
|
|
296
307
|
if real_file:
|
|
308
|
+
local_file = None if os.path.basename(real_file) == ".env" else real_file
|
|
309
|
+
example_file = None
|
|
310
|
+
if local_file:
|
|
311
|
+
paired_template = real_file + ".example"
|
|
312
|
+
if os.path.isfile(paired_template):
|
|
313
|
+
example_file = paired_template
|
|
297
314
|
return {
|
|
298
315
|
"format": "dotenv",
|
|
299
|
-
"local_file":
|
|
300
|
-
"example_file":
|
|
316
|
+
"local_file": local_file,
|
|
317
|
+
"example_file": example_file,
|
|
301
318
|
}
|
|
302
319
|
|
|
303
320
|
template_file = _find_dotenv_template(service_dir)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: envshield
|
|
3
|
-
Version: 4.7.
|
|
3
|
+
Version: 4.7.4
|
|
4
4
|
Summary: EnvShield: environment variables as a version-controlled configuration contract.
|
|
5
5
|
Author-email: Rabbil Yasar Sajal <rabbilyasar@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -270,7 +270,7 @@ Every command above documents its own options with `--help`; the full reference
|
|
|
270
270
|
|
|
271
271
|
## Git / CI workflow
|
|
272
272
|
|
|
273
|
-
This is where the schema stops being documentation and starts being enforcement. Three concrete things EnvShield catches
|
|
273
|
+
This is where the schema stops being documentation and starts being enforcement. Three concrete things EnvShield catches before a change reaches production:
|
|
274
274
|
|
|
275
275
|
**A new environment variable used in code but missing from the schema:**
|
|
276
276
|
|
|
@@ -284,12 +284,14 @@ envshield undeclared origin/main HEAD --service api
|
|
|
284
284
|
envshield schema diff origin/main HEAD --service api
|
|
285
285
|
```
|
|
286
286
|
|
|
287
|
-
**A secret accidentally introduced into a commit
|
|
287
|
+
**A secret accidentally introduced into a commit:**
|
|
288
288
|
|
|
289
289
|
```bash
|
|
290
290
|
envshield hook install
|
|
291
291
|
```
|
|
292
292
|
|
|
293
|
+
The pre-commit hook runs `scan --staged --enforce`, applying classification-based enforcement with interactive override for high-confidence findings on local commits, and strict blocking in CI. See [Secret safety](#secret-safety) for enforcement behavior details.
|
|
294
|
+
|
|
293
295
|
In CI, diff against the merge-base, not the base branch's current tip (which can move while the PR is open):
|
|
294
296
|
|
|
295
297
|
```yaml
|
|
@@ -466,10 +468,84 @@ Source-code discovery (what powers `undeclared` and `explain`'s "used in source"
|
|
|
466
468
|
|
|
467
469
|
## Secret safety
|
|
468
470
|
|
|
469
|
-
A supporting check alongside the contract, not the product itself. `secret = true` on a field tells EnvShield (and `setup`, and code generation) that it's sensitive — the schema records that fact, never the actual value.
|
|
471
|
+
A supporting check alongside the contract, not the product itself. `secret = true` on a field tells EnvShield (and `setup`, and code generation) that it's sensitive — the schema records that fact, never the actual value.
|
|
472
|
+
|
|
473
|
+
### Secret scanning
|
|
474
|
+
|
|
475
|
+
**`envshield scan`** looks for hardcoded secrets by pattern (Stripe, AWS, GitHub tokens, and more), and for env-var reads the schema doesn't declare, in one pass. Values are always redacted in output — a Stripe *publishable* key (`pk_...`) is never flagged, since detection matches the secret-key pattern (`sk_...`) by shape, not the variable's name.
|
|
476
|
+
|
|
477
|
+
### Context-aware classification
|
|
478
|
+
|
|
479
|
+
Python candidates are analyzed for syntactic context to distinguish code patterns from secret values:
|
|
480
|
+
|
|
481
|
+
```python
|
|
482
|
+
# Classified as code (suppressed):
|
|
483
|
+
client(key=CONFIG_CONSTANT)
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def handler(api_key: str): ...
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
# Classified as likely secret (detected):
|
|
490
|
+
client(key="sk_live_abc123...")
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
This reduces false positives on ordinary code patterns without broadly suppressing secret-shaped values. Classification is lightweight and syntactic — it recognizes type annotations, function arguments, and identifier references, but doesn't perform semantic analysis beyond what the tokenizer can see.
|
|
494
|
+
|
|
495
|
+
### Git hook enforcement
|
|
496
|
+
|
|
497
|
+
**`envshield hook install`** wires secret scanning into your Git workflow with classification-based enforcement:
|
|
498
|
+
|
|
499
|
+
```bash
|
|
500
|
+
envshield hook install
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
The pre-commit hook runs `scan --staged --enforce`, which scans only staged content and applies an enforcement policy based on classification confidence.
|
|
504
|
+
|
|
505
|
+
#### Enforcement behavior
|
|
506
|
+
|
|
507
|
+
| Finding type | Interactive (local commit) | Non-interactive (CI) |
|
|
508
|
+
|---|---|---|
|
|
509
|
+
| No findings | Allow | Allow |
|
|
510
|
+
| Clearly code-shaped | Suppressed | Suppressed |
|
|
511
|
+
| Ambiguous | Block | Block |
|
|
512
|
+
| High-confidence secret | Block + explicit override | Block |
|
|
513
|
+
|
|
514
|
+
**Clearly code-shaped** candidates (type annotations, function keyword arguments, etc.) are suppressed by the classifier and don't produce findings.
|
|
515
|
+
|
|
516
|
+
**Ambiguous** findings — insufficient context to confidently classify — block without override in both modes.
|
|
517
|
+
|
|
518
|
+
**High-confidence secrets** (string literals matching secret patterns) block by default but offer an interactive override for local commits:
|
|
519
|
+
|
|
520
|
+
```
|
|
521
|
+
⚠ EnvShield found potential secrets
|
|
522
|
+
|
|
523
|
+
HIGH CONFIDENCE
|
|
524
|
+
src/config.py:42 Generic API Key [REDACTED (30 chars)]
|
|
525
|
+
|
|
526
|
+
[1] Abort commit
|
|
527
|
+
[2] Commit anyway
|
|
528
|
+
|
|
529
|
+
Choice: 2
|
|
530
|
+
|
|
531
|
+
Type COMMIT ANYWAY to confirm: COMMIT ANYWAY
|
|
532
|
+
|
|
533
|
+
Proceeding with commit. The finding remains in the repository history.
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
The override requires exact confirmation text — `yes` or `Y` won't work. EOF (Ctrl+D) or Ctrl+C aborts. Raw secret values are never displayed in enforcement output.
|
|
537
|
+
|
|
538
|
+
In CI or other non-interactive contexts, high-confidence findings block with no prompt — the commit is refused, and the scan exits non-zero.
|
|
539
|
+
|
|
540
|
+
#### Scanning modes
|
|
541
|
+
|
|
542
|
+
```bash
|
|
543
|
+
envshield scan # Audit entire codebase
|
|
544
|
+
envshield scan --staged # Scan only staged Git content
|
|
545
|
+
envshield scan --staged --enforce # Staged scan + enforcement policy
|
|
546
|
+
```
|
|
470
547
|
|
|
471
|
-
-
|
|
472
|
-
- **`envshield hook install`** wires `scan --staged` into a pre-commit hook, so a real secret is caught before it's committed, not after.
|
|
548
|
+
The pre-commit hook uses the third form. `scan` without flags is for repository-wide audits, not commit-time enforcement.
|
|
473
549
|
|
|
474
550
|
---
|
|
475
551
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "envshield"
|
|
7
|
-
version = "4.7.
|
|
7
|
+
version = "4.7.4" # must be a string for PEP 621 compliance
|
|
8
8
|
description = "EnvShield: environment variables as a version-controlled configuration contract."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "4.7.3"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|