check_dependencies
Analyze package.json to detect typosquatted packages, suspicious install scripts, and other dependency risks.
Instructions
Check package.json for typosquatting, suspicious install scripts, and dependency risks
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| file_path | Yes | Path to package.json file |
Implementation Reference
- src/index.ts:327-364 (registration)Tool registration for 'check_dependencies' using server.tool(). Defines the schema (file_path string) and invokes DependencyScanner, scanFileContent, buildSummary, and formatSummary.
// Tool 4: check_dependencies server.tool( "check_dependencies", "Check package.json for typosquatting, suspicious install scripts, and dependency risks", { file_path: z .string() .describe("Path to package.json file"), }, async ({ file_path }) => { try { const resolvedPath = path.resolve(file_path); const content = fs.readFileSync(resolvedPath, "utf-8"); const scanner = new DependencyScanner(); const result = scanFileContent(content, resolvedPath, [scanner]); const summary = buildSummary([result]); let output = formatSummary(summary); if (result.findings.length === 0) { output += "\n\nNo suspicious dependencies or install scripts detected."; } return { content: [{ type: "text" as const, text: output }], }; } catch (err: any) { return { content: [ { type: "text" as const, text: `Error checking dependencies: ${err.message}`, }, ], isError: true, }; } } ); - src/scanner/dependency.ts:97-219 (handler)DependencyScanner class implementing the Scanner interface. Its scan() method parses package.json, checks for typosquatting (against a lookup map of popular package variants), suspicious install scripts (preinstall/install/postinstall/preuninstall matching regex patterns), and insecure dependency URLs (git://, IP addresses).
export class DependencyScanner implements Scanner { name = "suspicious-dependency"; scan(content: string, filePath: string): Finding[] { const findings: Finding[] = []; const basename = path.basename(filePath); if (basename !== "package.json") { return findings; } let pkg: any; try { pkg = JSON.parse(content); } catch { return findings; } const allDeps: Record<string, string> = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}), ...(pkg.optionalDependencies || {}), }; // Check for typosquatting for (const depName of Object.keys(allDeps)) { const target = typosquatLookup.get(depName); if (target) { findings.push({ category: "suspicious-dependency", severity: "high", file: filePath, line: this.findDepLine(content, depName), column: 1, message: `Possible typosquatting: "${depName}" looks like popular package "${target}"`, snippet: `"${depName}": "${allDeps[depName]}"`, recommendation: `Verify this is the intended package. "${depName}" closely resembles the popular package "${target}" and may be a typosquatting attack.`, }); } } // Check for suspicious install scripts const scripts = pkg.scripts || {}; for (const [scriptName, scriptCmd] of Object.entries(scripts)) { if ( scriptName === "preinstall" || scriptName === "install" || scriptName === "postinstall" || scriptName === "preuninstall" ) { const cmd = String(scriptCmd); for (const { pattern, message } of SUSPICIOUS_SCRIPTS) { if (pattern.test(cmd)) { findings.push({ category: "suspicious-dependency", severity: "high", file: filePath, line: this.findDepLine(content, scriptName), column: 1, message: `Suspicious install script (${scriptName}): ${message}`, snippet: `"${scriptName}": "${cmd}"`, recommendation: `Review the "${scriptName}" script carefully. Install hooks are a common vector for supply chain attacks.`, }); } } // Flag any postinstall that runs a js file (common malware pattern) if (/node\s+\S+\.js/i.test(cmd)) { findings.push({ category: "suspicious-dependency", severity: "medium", file: filePath, line: this.findDepLine(content, scriptName), column: 1, message: `Install script runs a JS file: "${cmd}" — review the target file`, snippet: `"${scriptName}": "${cmd}"`, recommendation: `Install scripts that execute JS files should be reviewed. Check the target file for malicious behavior.`, }); } } } // Check for git:// URLs (can be hijacked) for (const [depName, depVersion] of Object.entries(allDeps)) { const ver = String(depVersion); if (ver.startsWith("git://")) { findings.push({ category: "suspicious-dependency", severity: "medium", file: filePath, line: this.findDepLine(content, depName), column: 1, message: `Dependency "${depName}" uses insecure git:// protocol`, snippet: `"${depName}": "${ver}"`, recommendation: `Use https:// instead of git:// for dependency URLs. git:// is unencrypted and susceptible to MITM attacks.`, }); } // Check for URL-based deps pointing to IPs if (/https?:\/\/\d+\.\d+\.\d+\.\d+/.test(ver)) { findings.push({ category: "suspicious-dependency", severity: "high", file: filePath, line: this.findDepLine(content, depName), column: 1, message: `Dependency "${depName}" references an IP address`, snippet: `"${depName}": "${ver}"`, recommendation: `Dependencies pointing to raw IP addresses are suspicious. Verify the source.`, }); } } return findings; } private findDepLine(content: string, key: string): number { const lines = content.split("\n"); for (let i = 0; i < lines.length; i++) { if (lines[i].includes(`"${key}"`)) return i + 1; } return 1; } } - src/scanner/dependency.ts:1-95 (helper)Supporting data structures and helpers: POPULAR_PACKAGES list, generateTyposquatVariants() generating swapped/missing/doubled/substituted/hyphen variants, SUSPICIOUS_SCRIPTS regex patterns (pipe-to-shell, encoded PowerShell, eval, child_process, exfiltration), and build of typosquatLookup map.
import { Finding, Scanner } from "../types"; import * as path from "path"; // Known popular packages — typosquatting targets const POPULAR_PACKAGES: string[] = [ "express", "react", "lodash", "axios", "moment", "chalk", "commander", "debug", "dotenv", "inquirer", "webpack", "babel", "eslint", "prettier", "typescript", "jest", "mocha", "chai", "underscore", "async", "request", "bluebird", "uuid", "glob", "minimist", "yargs", "colors", "semver", "mkdirp", "rimraf", "cross-env", "nodemon", "concurrently", "husky", "lint-staged", "tailwindcss", "postcss", "autoprefixer", "vite", "next", "nuxt", "vue", "angular", "svelte", "mongoose", "sequelize", "prisma", "typeorm", "socket.io", "cors", "helmet", "morgan", "jsonwebtoken", "bcrypt", "passport", "aws-sdk", "firebase", "stripe", ]; // Common typosquatting transformations function generateTyposquatVariants(name: string): string[] { const variants: string[] = []; // Character swap: adjacent chars transposed for (let i = 0; i < name.length - 1; i++) { const swapped = name.slice(0, i) + name[i + 1] + name[i] + name.slice(i + 2); if (swapped !== name) variants.push(swapped); } // Missing character for (let i = 0; i < name.length; i++) { variants.push(name.slice(0, i) + name.slice(i + 1)); } // Double character for (let i = 0; i < name.length; i++) { variants.push(name.slice(0, i) + name[i] + name[i] + name.slice(i + 1)); } // Common substitutions const subs: Record<string, string[]> = { a: ["@", "4"], e: ["3"], i: ["1", "l"], l: ["1", "i"], o: ["0"], s: ["5", "z"], t: ["7"], g: ["9", "q"], }; for (let i = 0; i < name.length; i++) { const ch = name[i].toLowerCase(); if (subs[ch]) { for (const sub of subs[ch]) { variants.push(name.slice(0, i) + sub + name.slice(i + 1)); } } } // Hyphen/underscore confusion if (name.includes("-")) { variants.push(name.replace(/-/g, "_")); variants.push(name.replace(/-/g, "")); } if (name.includes("_")) { variants.push(name.replace(/_/g, "-")); variants.push(name.replace(/_/g, "")); } if (!name.includes("-") && !name.includes("_")) { // Try adding hyphens at common spots for (let i = 1; i < name.length; i++) { variants.push(name.slice(0, i) + "-" + name.slice(i)); } } return variants; } // Suspicious install script patterns const SUSPICIOUS_SCRIPTS: Array<{ pattern: RegExp; message: string; }> = [ { pattern: /curl\s+.*?\|.*?(?:sh|bash)/i, message: "Pipe-to-shell in install script" }, { pattern: /wget\s+.*?\|.*?(?:sh|bash)/i, message: "Pipe-to-shell in install script" }, { pattern: /powershell.*?-(?:enc|EncodedCommand)/i, message: "Encoded PowerShell in install script" }, { pattern: /eval\s*\(/, message: "eval() in install script" }, { pattern: /child_process/, message: "child_process usage in install script" }, { pattern: /process\.env.*(?:fetch|http|request)/i, message: "Env var exfiltration pattern in install script" }, ]; // Build lookup of typosquat targets const typosquatLookup = new Map<string, string>(); for (const pkg of POPULAR_PACKAGES) { for (const variant of generateTyposquatVariants(pkg)) { if (!POPULAR_PACKAGES.includes(variant)) { typosquatLookup.set(variant, pkg); } } } - src/index.ts:331-335 (schema)Input schema for the tool: a single 'file_path' parameter of type string, described as 'Path to package.json file'. Uses zod for validation.
{ file_path: z .string() .describe("Path to package.json file"), }, - src/utils.ts:81-153 (helper)buildSummary() and formatSummary() helper functions that aggregate scan results and produce a human-readable text report for the tool output.
export function buildSummary(results: ScanResult[]): ScanSummary { const bySeverity: Record<Severity, number> = { critical: 0, high: 0, medium: 0, low: 0, info: 0, }; const byCategory: Record<string, number> = {}; let totalFindings = 0; for (const result of results) { for (const f of result.findings) { totalFindings++; bySeverity[f.severity]++; byCategory[f.category] = (byCategory[f.category] || 0) + 1; } } return { totalFiles: results.length, totalFindings, bySeverity, byCategory, files: results.filter((r) => r.findings.length > 0), }; } export function formatSummary(summary: ScanSummary): string { const lines: string[] = []; lines.push(`## Scan Summary`); lines.push(`- Files scanned: ${summary.totalFiles}`); lines.push(`- Total findings: ${summary.totalFindings}`); lines.push(""); if (summary.totalFindings === 0) { lines.push("No threats detected."); return lines.join("\n"); } lines.push("### By Severity"); for (const [sev, count] of Object.entries(summary.bySeverity)) { if (count > 0) { const icon = sev === "critical" ? "🔴" : sev === "high" ? "🟠" : sev === "medium" ? "🟡" : sev === "low" ? "🔵" : "⚪"; lines.push(` ${icon} ${sev}: ${count}`); } } lines.push(""); lines.push("### By Category"); for (const [cat, count] of Object.entries(summary.byCategory)) { lines.push(` - ${cat}: ${count}`); } lines.push(""); lines.push("### Details"); for (const result of summary.files) { lines.push(`\n**${result.file}** (${result.findings.length} findings)`); for (const f of result.findings) { lines.push(` - [${f.severity.toUpperCase()}] Line ${f.line}: ${f.message}`); if (f.codePoint) { lines.push(` Code point: ${f.codePoint}`); } lines.push(` → ${f.recommendation}`); } } return lines.join("\n"); }