infinity-envguard 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- envguard/__init__.py +3 -0
- envguard/analysis.py +196 -0
- envguard/cli.py +601 -0
- envguard/discovery.py +166 -0
- envguard/envfiles.py +130 -0
- envguard/gitcheck.py +110 -0
- envguard/report.py +134 -0
- envguard/sources.py +101 -0
- infinity_envguard-0.1.0.dist-info/METADATA +482 -0
- infinity_envguard-0.1.0.dist-info/RECORD +14 -0
- infinity_envguard-0.1.0.dist-info/WHEEL +5 -0
- infinity_envguard-0.1.0.dist-info/entry_points.txt +2 -0
- infinity_envguard-0.1.0.dist-info/licenses/LICENSE +21 -0
- infinity_envguard-0.1.0.dist-info/top_level.txt +1 -0
envguard/__init__.py
ADDED
envguard/analysis.py
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""Comparison of referenced variables against environment files.
|
|
2
|
+
|
|
3
|
+
Pure data-in / data-out logic: no terminal output, no filesystem access, so it
|
|
4
|
+
can be unit-tested independently of the CLI.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import Literal
|
|
11
|
+
|
|
12
|
+
from envguard.envfiles import EnvFile
|
|
13
|
+
from envguard.sources import PUBLIC_PREFIX, SourceScanResult
|
|
14
|
+
|
|
15
|
+
Severity = Literal["error", "warning", "info"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class Finding:
|
|
20
|
+
"""One actionable result. Messages never contain environment values."""
|
|
21
|
+
|
|
22
|
+
severity: Severity
|
|
23
|
+
code: str
|
|
24
|
+
message: str
|
|
25
|
+
hint: str | None = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class AnalysisReport:
|
|
30
|
+
findings: list[Finding] = field(default_factory=list)
|
|
31
|
+
referenced: dict[str, list[str]] = field(default_factory=dict) # var -> files
|
|
32
|
+
template: str | None = None # relative path of the template used
|
|
33
|
+
real_env: str | None = None # relative path of the real env file used
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def errors(self) -> list[Finding]:
|
|
37
|
+
return [f for f in self.findings if f.severity == "error"]
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def warnings(self) -> list[Finding]:
|
|
41
|
+
return [f for f in self.findings if f.severity == "warning"]
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def infos(self) -> list[Finding]:
|
|
45
|
+
return [f for f in self.findings if f.severity == "info"]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def analyze(
|
|
49
|
+
scan: SourceScanResult,
|
|
50
|
+
template: EnvFile | None,
|
|
51
|
+
real_env: EnvFile | None,
|
|
52
|
+
) -> AnalysisReport:
|
|
53
|
+
"""Build the findings for ``envguard check``."""
|
|
54
|
+
report = AnalysisReport(
|
|
55
|
+
referenced=scan.variables,
|
|
56
|
+
template=template.relative_path if template else None,
|
|
57
|
+
real_env=real_env.relative_path if real_env else None,
|
|
58
|
+
)
|
|
59
|
+
referenced = set(scan.variables)
|
|
60
|
+
template_names = template.name_set if template else set()
|
|
61
|
+
|
|
62
|
+
if template is None:
|
|
63
|
+
report.findings.append(
|
|
64
|
+
Finding(
|
|
65
|
+
severity="warning",
|
|
66
|
+
code="no-template",
|
|
67
|
+
message="No environment template (.env.example, .env.template or "
|
|
68
|
+
".env.sample) was found.",
|
|
69
|
+
hint="Run `envguard init` to create one from the variables "
|
|
70
|
+
"referenced in your source code.",
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# 1. Referenced in code but missing from the template.
|
|
75
|
+
if template is not None:
|
|
76
|
+
missing = sorted(referenced - template_names)
|
|
77
|
+
if missing:
|
|
78
|
+
report.findings.append(
|
|
79
|
+
Finding(
|
|
80
|
+
severity="error",
|
|
81
|
+
code="missing-from-template",
|
|
82
|
+
message=f"{len(missing)} variable(s) referenced in code but missing "
|
|
83
|
+
f"from {template.relative_path}: {', '.join(missing)}",
|
|
84
|
+
hint="Document each variable in the template, or remove the "
|
|
85
|
+
"unused code reference.",
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# 2. Referenced in code but absent from the real environment file.
|
|
90
|
+
if real_env is not None:
|
|
91
|
+
missing_env = sorted(referenced - real_env.name_set)
|
|
92
|
+
if missing_env:
|
|
93
|
+
report.findings.append(
|
|
94
|
+
Finding(
|
|
95
|
+
severity="error",
|
|
96
|
+
code="missing-from-env",
|
|
97
|
+
message=f"{len(missing_env)} variable(s) referenced in code but not "
|
|
98
|
+
f"defined in {real_env.relative_path}: {', '.join(missing_env)}",
|
|
99
|
+
hint="The application may fail at runtime. Add them locally "
|
|
100
|
+
"(never commit the real file) or provide them via your CI/host.",
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
elif referenced:
|
|
104
|
+
report.findings.append(
|
|
105
|
+
Finding(
|
|
106
|
+
severity="info",
|
|
107
|
+
code="no-env-file",
|
|
108
|
+
message="No real environment file (.env) found; referenced variables "
|
|
109
|
+
"must come from the shell or the deployment platform.",
|
|
110
|
+
)
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# 3. Declared in the template but not detected in source (possible staleness).
|
|
114
|
+
if template is not None:
|
|
115
|
+
unused = sorted(template_names - referenced)
|
|
116
|
+
if unused:
|
|
117
|
+
report.findings.append(
|
|
118
|
+
Finding(
|
|
119
|
+
severity="warning",
|
|
120
|
+
code="unused-in-template",
|
|
121
|
+
message=f"{len(unused)} variable(s) in {template.relative_path} were "
|
|
122
|
+
f"not detected in source: {', '.join(unused)}",
|
|
123
|
+
hint="Static analysis is best-effort: dynamic access (e.g. "
|
|
124
|
+
"os.environ[name]) is not detected, so this can be a false "
|
|
125
|
+
"positive. Verify manually before removing anything.",
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# 4. Browser-exposed configuration.
|
|
130
|
+
public = sorted(
|
|
131
|
+
{v for v in referenced if v.startswith(PUBLIC_PREFIX)}
|
|
132
|
+
| (
|
|
133
|
+
{n for n in template_names if n.startswith(PUBLIC_PREFIX)}
|
|
134
|
+
if template is not None
|
|
135
|
+
else set()
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
if public:
|
|
139
|
+
report.findings.append(
|
|
140
|
+
Finding(
|
|
141
|
+
severity="info",
|
|
142
|
+
code="public-vars",
|
|
143
|
+
message=f"{len(public)} NEXT_PUBLIC_* variable(s) are exposed to the "
|
|
144
|
+
f"browser bundle: {', '.join(public)}",
|
|
145
|
+
hint="Anything prefixed NEXT_PUBLIC_ is embedded at build time and "
|
|
146
|
+
"readable by end users. Never put secrets there.",
|
|
147
|
+
)
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# 5. Malformed environment files.
|
|
151
|
+
for env_file in (template, real_env):
|
|
152
|
+
if env_file is None:
|
|
153
|
+
continue
|
|
154
|
+
if env_file.error:
|
|
155
|
+
report.findings.append(
|
|
156
|
+
Finding(
|
|
157
|
+
severity="warning",
|
|
158
|
+
code="unreadable-env-file",
|
|
159
|
+
message=f"{env_file.relative_path} could not be parsed: {env_file.error}",
|
|
160
|
+
)
|
|
161
|
+
)
|
|
162
|
+
elif env_file.issues:
|
|
163
|
+
lines = ", ".join(str(issue.line) for issue in env_file.issues[:5])
|
|
164
|
+
report.findings.append(
|
|
165
|
+
Finding(
|
|
166
|
+
severity="warning",
|
|
167
|
+
code="malformed-env-file",
|
|
168
|
+
message=f"{env_file.relative_path} has "
|
|
169
|
+
f"{len(env_file.issues)} malformed line(s) (line {lines}).",
|
|
170
|
+
)
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# 6. Report skipped/unreadable source files
|
|
174
|
+
if scan.skipped:
|
|
175
|
+
report.findings.append(
|
|
176
|
+
Finding(
|
|
177
|
+
severity="warning",
|
|
178
|
+
code="unreadable-sources",
|
|
179
|
+
message=f"{len(scan.skipped)} source file(s) could not be read and were skipped.",
|
|
180
|
+
hint="Check file permissions and encoding. Unreadable files were not scanned, "
|
|
181
|
+
"so their variable references may be missing from this report.",
|
|
182
|
+
)
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
if not referenced and not scan.skipped:
|
|
186
|
+
report.findings.append(
|
|
187
|
+
Finding(
|
|
188
|
+
severity="warning",
|
|
189
|
+
code="no-references",
|
|
190
|
+
message="No environment-variable references were detected in source files.",
|
|
191
|
+
hint="If your project does use environment variables, the supported "
|
|
192
|
+
"patterns may not cover your access style yet.",
|
|
193
|
+
)
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
return report
|