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 ADDED
@@ -0,0 +1,3 @@
1
+ """EnvGuard - local-first environment-configuration inspection toolkit."""
2
+
3
+ __version__ = "0.1.0"
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