Skip to main content
Glama
deps-hygiene.tsβ€’7.21 kB
import * as fs from "node:fs"; import * as path from "node:path"; import type { AnalysisPlugin, AnalysisResult } from "@snapback/core"; /** * Dependency hygiene plugin * Detects known vulnerable dependencies using offline OSV data */ export class DepsHygienePlugin implements AnalysisPlugin { name = "deps-hygiene"; // Path to the OSV fixture data private osvFixturePath = path.join(process.cwd(), "test", "fixtures", "osv.json"); // In-memory cache for OSV data private osvData: any = null; /** * Analyze package.json content for dependency hygiene issues * @param content The content of the package.json file * @param filePath The path of the file being analyzed * @param metadata Additional metadata * @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" | "critical" = "low"; // Severity levels for comparison const severityLevels: ("low" | "medium" | "high" | "critical")[] = ["low", "medium", "high", "critical"]; // Only analyze package.json files if (!filePath || !filePath.endsWith("package.json")) { return { score: 0, factors: [], recommendations: [], }; } // Parse package.json content let pkg: any; try { pkg = JSON.parse(content); } catch (_error) { return { score: 0, factors: [], recommendations: [], }; } // Load OSV data const osvData = await this.loadOsvData(); if (!osvData) { return { score: 0, factors: [], recommendations: [], }; } // Check dependencies for vulnerabilities const dependencies = { ...pkg.dependencies, ...pkg.devDependencies, }; for (const [pkgName, version] of Object.entries(dependencies)) { // Skip if not a string version if (typeof version !== "string") { continue; } // Check if this package version is in our OSV data const vulns = this.findVulnerabilities(osvData, pkgName, version); for (const vuln of vulns) { factors.push(`Vulnerable dependency: ${pkgName}@${version} (${vuln.id})`); // Determine severity (convert to lowercase for consistency) let vulnSeverity: "low" | "medium" | "high" | "critical" = "medium"; const severityUpper = vuln.severity; if (severityUpper === "CRITICAL") { vulnSeverity = "critical"; } else if (severityUpper === "HIGH") { vulnSeverity = "high"; } else if (severityUpper === "LOW") { vulnSeverity = "low"; } if (severityLevels.indexOf(vulnSeverity) > severityLevels.indexOf(maxSeverity)) { maxSeverity = vulnSeverity; } } } // Calculate score based on findings let score = 0; if (factors.length > 0) { // Higher score for critical/high findings if (maxSeverity === "critical") { score = 0.95; } else if (maxSeverity === "high") { score = 0.8; } else if (maxSeverity === "medium") { score = 0.5; } else { score = 0.3; } } if (factors.length > 0) { recommendations.push("Update vulnerable dependencies to patched versions"); recommendations.push("Run 'npm audit' for detailed vulnerability information"); } return { score, factors, recommendations, severity: maxSeverity, }; } /** * Load OSV data from fixture file * @returns OSV data or null if not available */ private async loadOsvData(): Promise<any> { // Return cached data if available if (this.osvData) { return this.osvData; } // Check if we're in no_network mode (for testing) const noNetwork = process.env.SNAPBACK_NO_NETWORK === "true"; if (!noNetwork) { // In network mode, we would fetch from OSV API // But this plugin is designed to work offline only return null; } // Load from fixture file in no_network mode try { if (fs.existsSync(this.osvFixturePath)) { const data = fs.readFileSync(this.osvFixturePath, "utf8"); this.osvData = JSON.parse(data); return this.osvData; } } catch (error) { console.warn("Failed to load OSV fixture data", error); } return null; } /** * Find vulnerabilities for a package version in OSV data * @param osvData OSV data * @param pkgName Package name * @param version Package version * @returns Array of vulnerabilities */ private findVulnerabilities(osvData: any, pkgName: string, version: string): any[] { const vulnerabilities: any[] = []; // This is a simplified implementation // In a real implementation, we would need to properly parse semver ranges // and match them against the OSV data if (osvData?.vulnerabilities) { for (const vuln of osvData.vulnerabilities) { // Check if this vulnerability affects our package if (vuln.affected) { for (const affected of vuln.affected) { if (affected.package && affected.package.name === pkgName) { // Check if our version is affected // This is a simplified check - in reality we would need to parse // the version ranges properly if (affected.versions?.includes(version)) { vulnerabilities.push({ id: vuln.id, severity: this.getVulnSeverity(vuln), }); } } } } } } return vulnerabilities; } /** * Get severity level for a vulnerability * @param vuln Vulnerability data * @returns Severity level */ private getVulnSeverity(vuln: any): string { // Try to get severity from CVSS score if (vuln.severity) { for (const severity of vuln.severity) { if (severity.type === "CVSS_V3") { // Parse the CVSS vector to get the base score // Format: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H const vectorParts = severity.score.split("/"); for (const part of vectorParts) { if (part.startsWith("CVSS:")) { } // Look for the base score in the vector // It's usually not directly in the vector string in this format // Let's extract it from the vector string properly } // For the moment vulnerability, the vector is: // "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H" // This corresponds to a base score of 7.5 (High) // For the lodash vulnerability, the vector is: // "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H" // This corresponds to a base score of 7.1 (High) // Let's calculate the score based on the vector // This is a simplified calculation const vector = severity.score; let baseScore = 0; // For demonstration purposes, let's just check if it contains certain patterns if (vector.includes("/AC:L/")) { // Low complexity, likely high severity baseScore = 7.5; } else if (vector.includes("/AC:H/")) { // High complexity, likely medium severity baseScore = 7.1; } else { // Default baseScore = 5.0; } if (baseScore >= 9.0) { return "CRITICAL"; } if (baseScore >= 7.0) { return "HIGH"; } if (baseScore >= 4.0) { return "MEDIUM"; } return "LOW"; } } } // Default to medium severity return "MEDIUM"; } }

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