Skip to main content
Glama
env-hygiene.tsβ€’6.56 kB
import type { AnalysisPlugin, AnalysisResult } from "@snapback/core"; /** * .env hygiene plugin * Detects potential security issues in .env files: * - Key-like entries that might be secrets * - Insecure configurations * - Missing required security settings */ export class EnvHygienePlugin implements AnalysisPlugin { name = "env-hygiene"; /** * Analyze .env file content for hygiene issues * @param content The content of the .env file * @param filePath The path of the file being analyzed * @param metadata Additional metadata (including changedLines for diff-aware analysis) * @returns Analysis result with score, factors, and recommendations */ async analyze(content: string, filePath?: string, metadata?: any): Promise<AnalysisResult> { const factors: string[] = []; const recommendations: string[] = []; let maxSeverity: "low" | "medium" | "high" = "low"; // Severity levels for comparison const severityLevels: ("low" | "medium" | "high")[] = ["low", "medium", "high"]; // Check if this is a .env file (not .env.example or .env.sample) const isEnvFile = filePath && (filePath.endsWith(".env") || filePath.endsWith(".env.local") || filePath.endsWith(".env.development") || filePath.endsWith(".env.production")); const isEnvExample = filePath && (filePath.endsWith(".env.example") || filePath.endsWith(".env.sample")); // Skip analysis for .env.example files as they're meant to be templates if (isEnvExample) { return { score: 0, factors: [], recommendations: [], }; } // Only analyze actual .env files if (!isEnvFile) { return { score: 0, factors: [], recommendations: [], }; } // Get the lines that should be analyzed based on changedLines metadata const linesToAnalyze = this.getLinesToAnalyze(content, metadata); // Check for key-like entries that might be secrets const keyLikeEntries = this.detectKeyLikeEntries(linesToAnalyze); if (keyLikeEntries.length > 0) { factors.push(...keyLikeEntries.map((entry) => `Key-like entry detected: ${entry.key}`)); const keySeverity = "high"; if (severityLevels.indexOf(keySeverity) > severityLevels.indexOf(maxSeverity)) { maxSeverity = keySeverity; } } // Check for common insecure configurations const insecureConfigs = this.detectInsecureConfigurations(linesToAnalyze); if (insecureConfigs.length > 0) { factors.push(...insecureConfigs.map((config) => `Insecure configuration: ${config}`)); const configSeverity = "medium"; if (severityLevels.indexOf(configSeverity) > severityLevels.indexOf(maxSeverity)) { maxSeverity = configSeverity; } } // Calculate score based on findings let score = 0; if (factors.length > 0) { // Higher score for high/medium findings if (maxSeverity === "high") { score = 0.8; } else if (maxSeverity === "medium") { score = 0.5; } else { score = 0.3; } } if (factors.length > 0) { recommendations.push("Move secrets to secure secret management systems"); recommendations.push("Use .env.example as a template without actual values"); recommendations.push("Review all key-like entries for sensitive data"); } return { score, factors, recommendations, severity: maxSeverity, }; } /** * Get lines to analyze based on changedLines metadata for diff-aware analysis * @param content File content * @param metadata Metadata containing changedLines information * @returns Array of lines to analyze */ private getLinesToAnalyze(content: string, metadata?: any): string[] { const lines = content.split("\n"); // If no metadata or changedLines, analyze all lines if (!metadata || !metadata.changedLines) { return lines; } // Filter lines based on changedLines metadata return lines.filter((_, index) => metadata.changedLines.includes(index + 1)); } /** * Detect key-like entries that might be secrets * @param lines Lines to analyze * @returns Array of detected key-like entries */ private detectKeyLikeEntries(lines: string[]): { key: string; value: string }[] { const findings: { key: string; value: string }[] = []; const keyLikePatterns = [ /(?:^|[^a-zA-Z0-9_])(?:api[_-]?key|secret|token|password|auth|access[_-]?key)[_=-]?\s*=\s*(.*)/i, /^([A-Z][A-Z0-9_]*(?:[-_][A-Z0-9]+)*)\s*=\s*(.+)$/, ]; for (const line of lines) { // Skip comments and empty lines if (line.trim().startsWith("#") || line.trim() === "") { continue; } // Check for key-like patterns for (const pattern of keyLikePatterns) { const match = line.match(pattern); if (match) { const key = match[1].trim(); const value = match[2] ? match[2].trim() : ""; // Skip if value is empty or looks like a placeholder if ( !value || value === "" || value.includes("your") || value.includes("example") || value.includes("placeholder") ) { continue; } // Skip if it looks like a variable reference if (value.startsWith("$") || value.includes("${")) { continue; } // Skip common non-sensitive keys const nonSensitiveKeys = ["node_env", "port", "host", "debug", "log_level"]; if (nonSensitiveKeys.some((nsKey) => key.toLowerCase().includes(nsKey))) { continue; } findings.push({ key, value }); } } } return findings; } /** * Detect insecure configurations * @param lines Lines to analyze * @returns Array of detected insecure configurations */ private detectInsecureConfigurations(lines: string[]): string[] { const findings: string[] = []; const insecurePatterns = [ { pattern: /debug\s*=\s*(true|1|on)/i, message: "Debug mode enabled", severity: "medium" as const }, { pattern: /ssl\s*=\s*(false|0|off)/i, message: "SSL disabled", severity: "medium" as const }, { pattern: /node_env\s*=\s*development/i, message: "Development environment in production", severity: "medium" as const, }, { pattern: /log_level\s*=\s*(debug|silly)/i, message: "Verbose logging level", severity: "low" as const }, ]; for (const line of lines) { // Skip comments and empty lines if (line.trim().startsWith("#") || line.trim() === "") { continue; } // Check for insecure patterns for (const { pattern, message, severity } of insecurePatterns) { if (pattern.test(line)) { findings.push(message); // For this plugin, we'll keep the highest severity as 'medium' for insecure configs } } } return findings; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/snapback-dev/mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server