Skip to main content
Glama
goklab

guardvibe

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

TableJSON Schema
NameRequiredDescriptionDefault
manifest_pathYesPath to manifest file (e.g. 'package.json', 'requirements.txt', 'go.mod')
formatNoOutput format: markdown (human) or json (machine-readable for agents)markdown

Implementation Reference

  • 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");
    }
  • 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;
    }
  • 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()];
    }
  • 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");
    }
  • 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: [
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description fully bears the burden of transparency. It discloses that the tool 'Reads the file directly' and checks against the OSV database, making its operation clear. There is no contradiction with annotations (none exist).

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is concise with three sentences: the first states the core functionality, the second adds a key behavioral detail, and the third provides usage guidance. Every sentence earns its place with no redundancy or fluff.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Despite having no output schema, the description does not fully explain the return value structure beyond output format options. For example, it doesn't specify whether the tool returns a list of CVEs, severity levels, per-dependency results, or a summary. This gap makes the description less complete for an agent to understand what to expect.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema has 100% coverage for both parameters, meaning the schema already documents them. The description adds value by explaining that the tool reads the file directly (context for manifest_path) and by elaborating on the output format options (human vs machine-readable). This exceeds the baseline of 3.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's purpose: parse a lockfile/manifest and check dependencies for known CVEs via the OSV database. The verb 'Parse' and resources 'lockfile or manifest' specify the action and target, and the description distinguishes it from sibling tools like 'check_dependencies' by mentioning direct file reading and the OSV database.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description explicitly provides usage context: 'Use this after installing dependencies, during CI, or when auditing existing projects for vulnerable packages.' This covers when to use the tool, though it does not mention when not to use it or suggest alternatives, which would improve the score.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

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/goklab/guardvibe'

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