Commit2415bdf
fix(health-score): kill false-positive "critical security issue" noise
fix(health-score): kill false-positive "critical security issue" noise
analyzeSecurityScore() was flagging committed .env.example templates as
leaked secrets, hardcoded-secret regexes matching variable interpolation
(${VAR}) and plain identifiers (a SecretStorage key name like
"gluecron.pat"), and test/fixture literals (e2e TEST_PASSWORD) as
critical findings regardless of file context. On gluecron's own repo
this produced 4 "critical" issues -- all false positives -- dragging
the self-hosted repo's health grade down to a D for no real reason.
Adds an .env.*.{example,sample,template,dist,test} allowlist, a
looksLikeRealSecret() entropy/interpolation gate, broader test/fixture
path detection applied at every severity, and respect for the existing
inline `secrets-ok` comment convention already used in scripts/install.sh.1 file changed+47−122415bdfc296c3f9ad89a16834e6cceffa5b0b179
1 changed file+47−12
Modifiedsrc/lib/intelligence.ts+47−12View fileUnifiedSplit
@@ -155,6 +155,28 @@ const SECURITY_PATTERNS: Array<{
155155 { pattern: /(?:disable|ignore).*(?:eslint|tslint|security)/i, severity: "low", message: "Security linter rule disabled", rule: "no-security-disable" },
156156];
157157
158// Committed env *templates* are the documented, expected way to ship an
159// example config — flagging them as leaked secrets is a false positive.
160const ENV_FILE_ALLOWLIST = /\.env\.(example|sample|template|dist|test)$/i;
161
162function isTestOrFixtureFile(filePath: string): boolean {
163 return (
164 /(^|\/)(tests?|__tests__|e2e|fixtures?|mocks?|specs?)(\/|$)/i.test(filePath) ||
165 /\.(test|spec)\.[jt]sx?$/i.test(filePath)
166 );
167}
168
169// Rejects matches where the "secret" is actually a variable reference
170// (`${VAR}`, template literals) or a plain identifier/slug (e.g. a
171// SecretStorage lookup KEY like "gluecron.pat") rather than a literal value.
172function looksLikeRealSecret(value: string): boolean {
173 if (!value) return false;
174 if (/[$`{]/.test(value)) return false;
175 if (value.length < 8) return false;
176 if (/^[a-z][a-z0-9._-]*$/.test(value)) return false;
177 return true;
178}
179
158180async function analyzeSecurityScore(
159181 repoDir: string,
160182 ref: string
@@ -169,9 +191,10 @@ async function analyzeSecurityScore(
169191
170192 const filePaths = files.trim().split("\n").filter(Boolean);
171193
172 // Check for .env files committed
194 // Check for .env files committed (excluding committed *templates*)
173195 for (const f of filePaths) {
174 if (/^\.env(?:\.|$)/.test(f.split("/").pop() || "")) {
196 const base = f.split("/").pop() || "";
197 if (/^\.env(?:\.|$)/.test(base) && !ENV_FILE_ALLOWLIST.test(base)) {
175198 issues.push({
176199 severity: "critical",
177200 file: f,
@@ -193,20 +216,32 @@ async function analyzeSecurityScore(
193216 );
194217 if (exitCode !== 0) continue;
195218
219 const isFixture = isTestOrFixtureFile(filePath);
196220 const lines = content.split("\n");
197221 for (let i = 0; i < lines.length; i++) {
222 const line = lines[i];
223 if (line.includes("secrets-ok")) continue;
224 // Test/fixture data (e.g. a hardcoded TEST_PASSWORD for e2e login)
225 // isn't a real leak at any severity — this is a health heuristic,
226 // not the push-time secret gate.
227 if (isFixture) continue;
228
198229 for (const rule of SECURITY_PATTERNS) {
199 if (rule.pattern.test(lines[i])) {
200 // Don't flag test files for most rules
201 if (filePath.includes("test") && rule.severity !== "critical") continue;
202 issues.push({
203 severity: rule.severity,
204 file: filePath,
205 line: i + 1,
206 message: rule.message,
207 rule: rule.rule,
208 });
230 const match = line.match(rule.pattern);
231 if (!match) continue;
232
233 if (rule.rule === "no-hardcoded-secrets") {
234 const valueMatch = line.match(/["']([^"']+)["']/);
235 if (!valueMatch || !looksLikeRealSecret(valueMatch[1])) continue;
209236 }
237
238 issues.push({
239 severity: rule.severity,
240 file: filePath,
241 line: i + 1,
242 message: rule.message,
243 rule: rule.rule,
244 });
210245 }
211246 }
212247 }
213248