scan_dependencies
Parse your lockfile or manifest to check all dependencies against the OSV database for known CVEs. Use after installing dependencies, during CI, or when auditing existing projects.
Instructions
Parse a lockfile or manifest (package.json, package-lock.json, requirements.txt, go.mod) and check all dependencies for known CVEs via the OSV database. Reads the file directly. Use this after installing dependencies, during CI, or when auditing existing projects for vulnerable packages.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| manifest_path | Yes | Path to manifest file (e.g. 'package.json', 'requirements.txt', 'go.mod') | |
| format | No | Output format: markdown (human) or json (machine-readable for agents) | markdown |
Implementation Reference
- src/tools/scan-dependencies.ts:6-105 (handler)The main handler function for the scan_dependencies tool. Reads a manifest file (package.json, package-lock.json, etc.), parses it, queries the OSV API for known vulnerabilities, and returns results in markdown or JSON format.
export async function scanDependencies(manifestPath: string, format: "markdown" | "json" = "markdown"): Promise<string> { let content: string; try { content = readFileSync(manifestPath, "utf-8"); } catch { return `# GuardVibe Dependency Report\n\nError: Could not read file: ${manifestPath}`; } const filename = basename(manifestPath); let packages; try { packages = parseManifest(content, filename); } catch (e) { const msg = e instanceof Error ? e.message : "Unknown error"; return `# GuardVibe Dependency Report\n\nError: ${msg}`; } if (packages.length === 0) { return `# GuardVibe Dependency Report\n\nFile: ${manifestPath}\nPackages found: 0\n\nNo packages to check.`; } const lines: string[] = [ `# GuardVibe Dependency Report`, ``, `File: ${manifestPath}`, `Packages checked: ${packages.length}`, `Database: OSV (Google Open Source Vulnerabilities)`, ``, `---`, ``, ]; let vulnResults: Map<string, any[]>; try { vulnResults = await queryOsvBatch(packages); } catch { lines.push(`Error: Could not reach OSV API. Check your network connection.`); return lines.join("\n"); } let totalVulns = 0; const criticalPackages: string[] = []; // Build per-package vulnerability data const pkgResults: Array<{ name: string; version: string; ecosystem: string; vulnerabilities: any[] }> = []; for (const pkg of packages) { const key = `${pkg.name}@${pkg.version}`; const vulns = vulnResults.get(key) || []; if (vulns.length === 0) continue; totalVulns += vulns.length; criticalPackages.push(key); pkgResults.push({ name: pkg.name, version: pkg.version, ecosystem: pkg.ecosystem, vulnerabilities: vulns.map(v => ({ id: v.id, severity: normalizeSeverity(v), summary: v.summary, fixedIn: (v.affected ?? []).flatMap((a: any) => (a.ranges ?? []).flatMap((r: any) => r.events.filter((e: any) => e.fixed).map((e: any) => e.fixed))).join(", ") || undefined, url: v.references?.[0]?.url, })), }); lines.push(`## ${key} (${pkg.ecosystem}) — ${vulns.length} vulnerabilities`, ``); for (const vuln of vulns) { lines.push(formatVulnerability(vuln), ``); } } if (format === "json") { const sevCounts = { critical: 0, high: 0, medium: 0, low: 0 }; for (const pr of pkgResults) { for (const v of pr.vulnerabilities) { if (v.severity in sevCounts) sevCounts[v.severity as keyof typeof sevCounts]++; } } return JSON.stringify({ summary: { total: packages.length, vulnerable: criticalPackages.length, vulnerablePackages: criticalPackages.length, totalAdvisories: totalVulns, ...sevCounts, }, packages: pkgResults, }); } lines.push(`---`, ``, `## Summary`, ``); if (totalVulns === 0) { lines.push(`All ${packages.length} packages are clean. No known vulnerabilities found.`); } else { lines.push(`**${totalVulns} vulnerabilities** found in ${criticalPackages.length} packages:`, ``); for (const pkg of criticalPackages) lines.push(`- ${pkg}`); lines.push(``, `**Action:** Update affected packages to their fixed versions.`); } return lines.join("\n"); } - src/utils/manifest-parser.ts:1-5 (schema)The ParsedPackage interface defining the shape of a parsed dependency (name, version, ecosystem), used as input/output for manifest parsing.
export interface ParsedPackage { name: string; version: string; ecosystem: string; } - src/utils/manifest-parser.ts:9-264 (helper)Parses various manifest file formats (package.json, package-lock.json, yarn.lock, pnpm-lock.yaml, requirements.txt, go.mod) into an array of ParsedPackage objects.
export function parseManifest(content: string, filename: string): ParsedPackage[] { const lower = filename.toLowerCase(); if (lower === "package-lock.json") return parsePackageLock(content); if (lower === "package.json") return parsePackageJson(content); if (lower === "yarn.lock") return parseYarnLock(content); if (lower === "pnpm-lock.yaml") return parsePnpmLock(content); if (lower === "requirements.txt") return parseRequirementsTxt(content); if (lower === "go.mod") return parseGoMod(content); throw new Error(`Unsupported manifest format: ${filename}`); } function addPackage(packages: PackageAccumulator, pkg: ParsedPackage): void { const key = `${pkg.ecosystem}:${pkg.name}@${pkg.version}`; packages.set(key, pkg); } function sanitizeVersion(rawVersion: string): string | null { const trimmed = rawVersion.trim(); if (!trimmed) return null; if ( trimmed.startsWith("file:") || trimmed.startsWith("link:") || trimmed.startsWith("workspace:") || trimmed.startsWith("git+") || trimmed.startsWith("github:") || trimmed.startsWith("http://") || trimmed.startsWith("https://") ) { return null; } const normalized = trimmed.replace(/^[\^~<>=\sv]*/g, ""); return normalized || null; } function parsePackageJson(content: string): ParsedPackage[] { const pkg = JSON.parse(content); const packages: PackageAccumulator = new Map(); for (const section of ["dependencies", "devDependencies", "optionalDependencies"]) { for (const [name, ver] of Object.entries(pkg[section] || {})) { const version = sanitizeVersion(String(ver)); if (!version) continue; addPackage(packages, { name, version, ecosystem: "npm" }); } } return [...packages.values()]; } function parsePackageLock(content: string): ParsedPackage[] { const lock = JSON.parse(content); const packages: PackageAccumulator = new Map(); if (lock.packages && typeof lock.packages === "object") { for (const [pkgPath, info] of Object.entries(lock.packages)) { if (pkgPath === "") continue; const pkg = info as { version?: string }; if (!pkg.version) continue; const name = pkgPath.split("node_modules/").filter(Boolean).at(-1); if (!name) continue; addPackage(packages, { name, version: pkg.version, ecosystem: "npm" }); } } if (packages.size === 0 && lock.dependencies && typeof lock.dependencies === "object") { walkPackageLockDependencies(lock.dependencies as Record<string, unknown>, packages); } return [...packages.values()]; } function walkPackageLockDependencies( dependencies: Record<string, unknown>, packages: PackageAccumulator ): void { for (const [name, info] of Object.entries(dependencies)) { if (!info || typeof info !== "object") continue; const pkg = info as { version?: string; dependencies?: Record<string, unknown> }; if (pkg.version) { addPackage(packages, { name, version: pkg.version, ecosystem: "npm" }); } if (pkg.dependencies) { walkPackageLockDependencies(pkg.dependencies, packages); } } } function parseYarnLock(content: string): ParsedPackage[] { const packages: PackageAccumulator = new Map(); // yarn.lock v1 format: // "package@^version": // version "1.2.3" // resolved "https://registry.yarnpkg.com/..." // integrity sha512-... // // yarn berry (v2+) format: // "package@npm:^version": // version: 1.2.3 // resolution: "package@npm:1.2.3" const lines = content.split("\n"); let currentName: string | null = null; for (const rawLine of lines) { const line = rawLine.trimEnd(); // Skip comments and empty lines if (!line || line.startsWith("#")) { currentName = null; continue; } // Package header line (not indented) if (!line.startsWith(" ") && !line.startsWith("\t")) { // Extract package name from patterns like: // "axios@^1.7.0, axios@^1.7.9": // axios@^1.7.0: const headerMatch = line.match(/^"?(@?[^@\s,"]+)@/); if (headerMatch) { currentName = headerMatch[1]; } else { currentName = null; } continue; } // Version line (indented) if (currentName) { // v1: ' version "1.7.9"' const v1Match = line.match(/^\s+version\s+"([^"]+)"/); if (v1Match) { const version = sanitizeVersion(v1Match[1]); if (version) { addPackage(packages, { name: currentName, version, ecosystem: "npm" }); } currentName = null; continue; } // berry: ' version: 1.7.9' const berryMatch = line.match(/^\s+version:\s+(.+)/); if (berryMatch) { const version = sanitizeVersion(berryMatch[1].trim().replace(/^"|"$/g, "")); if (version) { addPackage(packages, { name: currentName, version, ecosystem: "npm" }); } currentName = null; continue; } } } return [...packages.values()]; } function parsePnpmLock(content: string): ParsedPackage[] { const packages: PackageAccumulator = new Map(); // pnpm-lock.yaml format (v6+): // /@scope/package@1.2.3: // resolution: {integrity: sha512-...} // // pnpm-lock.yaml format (v9+): // '@scope/package@1.2.3': // resolution: {integrity: sha512-...} // // Also handles packages section: // /package@1.2.3: const lines = content.split("\n"); for (const rawLine of lines) { const line = rawLine.trimEnd(); // Match package entries like: // '/@scope/package@1.2.3:' or ' /@scope/package@1.2.3:' // '/package@1.2.3:' // '@scope/package@1.2.3': (v9+ quoted format) const pnpmMatch = line.match( /^\s+['"]?\/?(@?[a-zA-Z0-9][\w./-]*?)@(\d[^:'"\s]*)['"]?\s*:/ ); if (pnpmMatch) { const name = pnpmMatch[1]; const version = sanitizeVersion(pnpmMatch[2]); if (version && name) { addPackage(packages, { name, version, ecosystem: "npm" }); } } } return [...packages.values()]; } function parseRequirementsTxt(content: string): ParsedPackage[] { const packages: PackageAccumulator = new Map(); for (const line of content.split("\n")) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("-")) continue; const match = trimmed.match(/^([a-zA-Z0-9_.-]+)==([a-zA-Z0-9_.+-]+)/); if (!match) continue; addPackage(packages, { name: match[1], version: match[2], ecosystem: "PyPI", }); } return [...packages.values()]; } function parseGoMod(content: string): ParsedPackage[] { const packages: PackageAccumulator = new Map(); const lines = content.split("\n"); let inRequireBlock = false; for (const rawLine of lines) { const line = rawLine.trim(); if (!line || line.startsWith("//")) continue; if (line.startsWith("require (")) { inRequireBlock = true; continue; } if (inRequireBlock && line === ")") { inRequireBlock = false; continue; } const candidate = inRequireBlock ? line : line.startsWith("require ") ? line.slice("require ".length).trim() : ""; if (!candidate) continue; const match = candidate.match(/^(\S+)\s+v?([^\s]+)(?:\s+\/\/.*)?$/); if (!match) continue; addPackage(packages, { name: match[1], version: match[2].replace(/^v/, ""), ecosystem: "Go", }); } return [...packages.values()]; } - src/utils/osv-client.ts:50-190 (helper)Queries the OSV batch API to get vulnerability data for a batch of packages. Chunks into 500-pkg batches and fetches full vulnerability details for each.
export async function queryOsvBatch( packages: BatchQuery[] ): Promise<Map<string, OsvVulnerability[]>> { const results = new Map<string, OsvVulnerability[]>(); // OSV batch can time out on large monorepo lockfiles (1000+ packages), // and very-large requests can be rate-limited. Chunk into 500-pkg batches // and process sequentially so the per-request timeout is generous. const CHUNK_SIZE = 500; for (let start = 0; start < packages.length; start += CHUNK_SIZE) { const chunk = packages.slice(start, start + CHUNK_SIZE); const queries = chunk.map(pkg => ({ package: { name: pkg.name, ecosystem: pkg.ecosystem }, version: pkg.version, })); const response = await fetch("https://api.osv.dev/v1/querybatch", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ queries }), signal: AbortSignal.timeout(60000), }); if (!response.ok) { throw new Error(`OSV batch API error: ${response.status} ${response.statusText}`); } const data = await response.json() as { results: Array<{ vulns?: Array<{ id: string }> }> }; for (let i = 0; i < chunk.length; i++) { const key = `${chunk[i].name}@${chunk[i].version}`; const batchVulns = data.results[i]?.vulns || []; if (batchVulns.length === 0) { results.set(key, []); continue; } const fullVulns: OsvVulnerability[] = []; for (const bv of batchVulns) { try { const vulnResponse = await fetch(`https://api.osv.dev/v1/vulns/${bv.id}`, { signal: AbortSignal.timeout(5000), }); if (vulnResponse.ok) { const vulnData = await vulnResponse.json() as OsvVulnerability; fullVulns.push(vulnData); } } catch { fullVulns.push({ id: bv.id, summary: "Details unavailable" } as OsvVulnerability); } } results.set(key, fullVulns); } } return results; } export function normalizeSeverity(vuln: OsvVulnerability | any): string { if (!vuln.severity || vuln.severity.length === 0) { // Fallback: check database_specific for severity if (vuln.database_specific?.severity) { const s = vuln.database_specific.severity.toLowerCase(); if (s === "critical") return "critical"; if (s === "high") return "high"; if (s === "moderate" || s === "medium") return "medium"; if (s === "low") return "low"; } return "unknown"; } const cvss = vuln.severity.find((s: any) => s.type === "CVSS_V3" || s.type === "CVSS_V4"); if (!cvss) { // No CVSS entry — try database_specific fallback if (vuln.database_specific?.severity) { const s = vuln.database_specific.severity.toLowerCase(); if (s === "critical") return "critical"; if (s === "high") return "high"; if (s === "moderate" || s === "medium") return "medium"; if (s === "low") return "low"; } return "unknown"; } // CVSS score can be: a number, a numeric string, or a CVSS vector string let score: number | null = null; if (typeof cvss.score === "number") { score = cvss.score; } else if (typeof cvss.score === "string") { // Try parsing as number first const parsed = parseFloat(cvss.score); if (!isNaN(parsed) && !cvss.score.startsWith("CVSS:")) { score = parsed; } else { // It's a CVSS vector string like "CVSS:3.1/AV:N/AC:L/..." // Fall back to database_specific severity if (vuln.database_specific?.severity) { const s = vuln.database_specific.severity.toLowerCase(); if (s === "critical") return "critical"; if (s === "high") return "high"; if (s === "moderate" || s === "medium") return "medium"; if (s === "low") return "low"; } return "unknown"; } } if (score === null) return "unknown"; if (score >= 9.0) return "critical"; if (score >= 7.0) return "high"; if (score >= 4.0) return "medium"; return "low"; } export function formatVulnerability(vuln: OsvVulnerability): string { const severity = normalizeSeverity(vuln); const fixedVersions: string[] = []; for (const affected of vuln.affected ?? []) { for (const range of affected.ranges ?? []) { for (const event of range.events) { if (event.fixed) fixedVersions.push(event.fixed); } } } const fixInfo = fixedVersions.length > 0 ? `Fixed in: ${fixedVersions.join(", ")}` : "No fix available yet"; const refUrl = vuln.references?.[0]?.url ?? ""; return [ `### ${vuln.id}`, `**Severity:** ${severity}`, `**Summary:** ${vuln.summary}`, `**${fixInfo}**`, refUrl ? `**Reference:** ${refUrl}` : "", ] .filter(Boolean) .join("\n"); } - src/tools/full-audit.ts:526-529 (registration)Registration reference: the scan_dependencies tool is invoked within the full-audit orchestration tool as part of the 'dependencies' section.
dependencies: { priority: 3, tool: "scan_dependencies", actions: [