1#!/usr/bin/env bash2# Clones the plugin at a pinned commit and, if it launches an npm-based MCP3# server, pulls that package's registry metadata, tarball, and audit data.4# Ends by printing an inventory of what it gathered, keyed by SCRATCH_DIR.5set -euo pipefail67if [ $# -lt 2 ]; then8 echo "usage: gather-evidence.sh <repo-url> <commit-sha>" >&29 exit 210fi1112repo_url=
99 || { rm -f "$pkg_dir/attestations.json"; mark ATTESTATIONS_UNAVAILABLE "no attestations retrieved; treat the package as unsigned unless proven otherwise."; }
100
101 # A published tarball almost never ships its own lockfile, so `npm audit`
102 # fails with ENOLOCK unless the package happens to bundle one. Generate a
103 # lockfile from package.json first so audit has a dependency tree to
110 # npm audit exits non-zero both when it finds vulnerabilities, which is
111 # a real report, and when it fails outright, in which case it writes an
112 # error object to stdout that a later read would take for scan output.
113 # Only a real report carries auditReportVersion, so key off the payload
114 # rather than the exit status. This covers the missing-lockfile case too,
115 # where audit never ran and the file was never created.
116 if ! grep -q '"auditReportVersion"' "$pkg_dir/audit.json" 2>/dev/null; then
117 rm -f "$pkg_dir/audit.json"
118 mark NPM_AUDIT_UNAVAILABLE "no npm audit report was produced; treat the dependency tree as never having been scanned."
119 fi
120 else
121 mark TARBALL_EXTRACT_FAILED "the published tarball did not extract cleanly; inspect it manually."
122 fi
123 fi
124else
125 # No single pinned npm@version spec found in .mcp.json (or no .mcp.json at
126 # all). This does not mean there is nothing to audit: the server may be
127 # declared via plugin.json's mcpServers field, use a semver range instead
128 # of a pin, run multiple servers, or not be npm-based at all. Say so
129 # explicitly so the caller investigates manually rather than assuming this
130 # script covered it.
131 mark NO_NPM_PACKAGE_DETECTED "no pinned name@version spec found in .mcp.json. Check plugin.json's mcpServers field and inspect the server's dependency manifest directly."
132fi
133
134# Describe what is actually on disk, so the caller does not have to carry a
135# second copy of this layout. Every label here is a fixed string or a count;
136# nothing derived from the audited repo is echoed, since script output reads
137# as more trustworthy than the file contents it describes.
138count_lines() {
139 if [ -f "$1" ]; then wc -l < "$1" | tr -d ' '; else echo 0; fi
140}
141
142# A failed fetch can still leave a zero-byte file behind (the redirect
143# creates it before the command runs), so evidence files must be non-empty
144# to be advertised as collected. The symlink inventories are the exception:
145# an empty one is a positive result worth stating.
146row() {
147 local path="$scratch/$1"
148 case "$1" in
149 */) if [ -d "$path" ]; then printf '%-22s %s\n' "$1" "$2"; fi ;;
150 *) if [ -s "$path" ]; then printf '%-22s %s\n' "$1" "$2"; fi ;;