agentverify 1.4.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -1
- package/README.md +1 -1
- package/dist/cli.js +774 -28
- package/dist/cli.mjs +776 -28
- package/dist/guard.d.mts +105 -0
- package/dist/guard.d.ts +105 -0
- package/dist/guard.js +249 -0
- package/dist/guard.mjs +220 -0
- package/dist/sdk.d.mts +4 -0
- package/dist/sdk.d.ts +4 -0
- package/package.json +28 -8
package/dist/cli.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
// src/cli.ts
|
|
5
|
-
var
|
|
6
|
-
var
|
|
5
|
+
var import_fs4 = require("fs");
|
|
6
|
+
var import_path3 = require("path");
|
|
7
7
|
|
|
8
8
|
// src/sdk.ts
|
|
9
9
|
var DEFAULT_API_URL = "https://agentverify-api.agentverify.workers.dev/v1/scan";
|
|
@@ -115,8 +115,642 @@ async function scan(input, options) {
|
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
// src/mcp.ts
|
|
119
|
+
var import_readline = require("readline");
|
|
120
|
+
var import_fs2 = require("fs");
|
|
121
|
+
var import_path2 = require("path");
|
|
122
|
+
|
|
123
|
+
// src/files.ts
|
|
124
|
+
var import_fs = require("fs");
|
|
125
|
+
var import_path = require("path");
|
|
126
|
+
var import_os = require("os");
|
|
127
|
+
var SCANNABLE_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx", ".py", ".json", ".yaml", ".yml", ".toml", ".md", ".mjs", ".cjs", ".sh"]);
|
|
128
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", "vendor", "coverage", ".cache", ".venv", "venv", "__pycache__", "out", "target"]);
|
|
129
|
+
var SKIP_FILES = /* @__PURE__ */ new Set(["package-lock.json", "yarn.lock", "pnpm-lock.yaml", "poetry.lock", "Cargo.lock", "composer.lock", "tsconfig.json"]);
|
|
130
|
+
var MAX_FILES = 60;
|
|
131
|
+
var MAX_TOTAL_BYTES = 5 * 1024 * 1024;
|
|
132
|
+
var MAX_FILE_BYTES = 400 * 1024;
|
|
133
|
+
function filePriority(relPath) {
|
|
134
|
+
const p = relPath.toLowerCase().replace(/\\/g, "/");
|
|
135
|
+
const name = p.split("/").pop() ?? p;
|
|
136
|
+
let score = 0;
|
|
137
|
+
if (/(^|\/)(tests?|__tests__|spec|e2e|fixtures?|examples?)\//.test(p) || /\.(test|spec)\.[a-z]+$/.test(name)) score -= 80;
|
|
138
|
+
if (/(agent|tool|mcp|prompt|crew|graph|server|config|policy|skill)/.test(name)) score += 30;
|
|
139
|
+
if (/(^|\/)(agents?|tools?|mcp|src|app|lib)\//.test(p)) score += 10;
|
|
140
|
+
if (name.endsWith(".md")) score -= 20;
|
|
141
|
+
if (name === "skill.md" || name === "agents.md" || name === "claude.md") score += 40;
|
|
142
|
+
return score - p.split("/").length;
|
|
143
|
+
}
|
|
144
|
+
function walk(dir, out) {
|
|
145
|
+
let entries;
|
|
146
|
+
try {
|
|
147
|
+
entries = (0, import_fs.readdirSync)(dir);
|
|
148
|
+
} catch {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
for (const entry of entries) {
|
|
152
|
+
if (SKIP_DIRS.has(entry) || SKIP_FILES.has(entry)) continue;
|
|
153
|
+
const full = (0, import_path.join)(dir, entry);
|
|
154
|
+
let st;
|
|
155
|
+
try {
|
|
156
|
+
st = (0, import_fs.statSync)(full);
|
|
157
|
+
} catch {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (st.isDirectory()) walk(full, out);
|
|
161
|
+
else if (SCANNABLE_EXTENSIONS.has((0, import_path.extname)(entry).toLowerCase())) out.push(full);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function pickFiles(target) {
|
|
165
|
+
const notes = [];
|
|
166
|
+
let st;
|
|
167
|
+
try {
|
|
168
|
+
st = (0, import_fs.statSync)(target);
|
|
169
|
+
} catch {
|
|
170
|
+
throw new Error(`Path not found: ${target}`);
|
|
171
|
+
}
|
|
172
|
+
if (st.isFile()) {
|
|
173
|
+
if (st.size > MAX_TOTAL_BYTES) throw new Error(`${(0, import_path.basename)(target)} is larger than 5MB, which is the most one scan can take.`);
|
|
174
|
+
return { files: [{ path: (0, import_path.basename)(target), content: (0, import_fs.readFileSync)(target, "utf8") }], notes };
|
|
175
|
+
}
|
|
176
|
+
const all = [];
|
|
177
|
+
walk(target, all);
|
|
178
|
+
if (all.length === 0) throw new Error(`No agent files found in ${target}. Agent Verify reads ${[...SCANNABLE_EXTENSIONS].join(" ")} files.`);
|
|
179
|
+
const ranked = all.map((full) => ({ full, rel: (0, import_path.relative)(target, full).split(import_path.sep).join("/") })).sort((a, b) => filePriority(b.rel) - filePriority(a.rel) || a.rel.localeCompare(b.rel));
|
|
180
|
+
const files = [];
|
|
181
|
+
let total = 0;
|
|
182
|
+
let tooBig = 0;
|
|
183
|
+
for (const { full, rel } of ranked) {
|
|
184
|
+
if (files.length >= MAX_FILES) break;
|
|
185
|
+
let size = 0;
|
|
186
|
+
try {
|
|
187
|
+
size = (0, import_fs.statSync)(full).size;
|
|
188
|
+
} catch {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (size > MAX_FILE_BYTES) {
|
|
192
|
+
tooBig++;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (total + size > MAX_TOTAL_BYTES) break;
|
|
196
|
+
files.push({ path: rel, content: (0, import_fs.readFileSync)(full, "utf8") });
|
|
197
|
+
total += size;
|
|
198
|
+
}
|
|
199
|
+
const left = ranked.length - files.length - tooBig;
|
|
200
|
+
if (left > 0) notes.push(`${left} lower-priority file(s) were left out to stay within one scan (${MAX_FILES} files / 5MB).`);
|
|
201
|
+
if (tooBig > 0) notes.push(`${tooBig} file(s) over 400KB were skipped.`);
|
|
202
|
+
return { files, notes };
|
|
203
|
+
}
|
|
204
|
+
function combineFiles(files) {
|
|
205
|
+
if (files.length === 1) return files[0].content;
|
|
206
|
+
return files.map((f) => `// ===== FILE: ${f.path} =====
|
|
207
|
+
${f.content}`).join("\n\n");
|
|
208
|
+
}
|
|
209
|
+
function locateLine(files, combinedLine) {
|
|
210
|
+
if (files.length === 1) return { path: files[0].path, line: combinedLine };
|
|
211
|
+
let cursor = 1;
|
|
212
|
+
for (let i = 0; i < files.length; i++) {
|
|
213
|
+
const start = cursor + 1;
|
|
214
|
+
const lines = files[i].content.split("\n").length;
|
|
215
|
+
if (combinedLine >= start && combinedLine < start + lines) return { path: files[i].path, line: combinedLine - start + 1 };
|
|
216
|
+
cursor = start + lines + 1;
|
|
217
|
+
}
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
function knownMcpConfigPaths(cwd = process.cwd()) {
|
|
221
|
+
const home = (0, import_os.homedir)();
|
|
222
|
+
const appData = process.env.APPDATA ?? (0, import_path.join)(home, "AppData", "Roaming");
|
|
223
|
+
const claudeDesktop = (0, import_os.platform)() === "win32" ? (0, import_path.join)(appData, "Claude", "claude_desktop_config.json") : (0, import_os.platform)() === "darwin" ? (0, import_path.join)(home, "Library", "Application Support", "Claude", "claude_desktop_config.json") : (0, import_path.join)(home, ".config", "Claude", "claude_desktop_config.json");
|
|
224
|
+
return [
|
|
225
|
+
{ client: "Claude Code (this project)", path: (0, import_path.join)(cwd, ".mcp.json") },
|
|
226
|
+
{ client: "Claude Code (user)", path: (0, import_path.join)(home, ".claude.json") },
|
|
227
|
+
{ client: "Claude Desktop", path: claudeDesktop },
|
|
228
|
+
{ client: "Cursor (this project)", path: (0, import_path.join)(cwd, ".cursor", "mcp.json") },
|
|
229
|
+
{ client: "Cursor (user)", path: (0, import_path.join)(home, ".cursor", "mcp.json") },
|
|
230
|
+
{ client: "VS Code (this project)", path: (0, import_path.join)(cwd, ".vscode", "mcp.json") },
|
|
231
|
+
{ client: "Windsurf", path: (0, import_path.join)(home, ".codeium", "windsurf", "mcp_config.json") }
|
|
232
|
+
];
|
|
233
|
+
}
|
|
234
|
+
function extractMcpServers(raw) {
|
|
235
|
+
let data;
|
|
236
|
+
try {
|
|
237
|
+
data = JSON.parse(raw);
|
|
238
|
+
} catch {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
if (!data || typeof data !== "object") return null;
|
|
242
|
+
const servers = {};
|
|
243
|
+
const take = (obj, prefix = "") => {
|
|
244
|
+
if (!obj || typeof obj !== "object") return;
|
|
245
|
+
for (const key of ["mcpServers", "servers"]) {
|
|
246
|
+
const block = obj[key];
|
|
247
|
+
if (block && typeof block === "object" && !Array.isArray(block)) {
|
|
248
|
+
for (const [name, def] of Object.entries(block)) servers[prefix + name] = def;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
take(data);
|
|
253
|
+
const projects = data.projects;
|
|
254
|
+
if (projects && typeof projects === "object") {
|
|
255
|
+
for (const [proj, value] of Object.entries(projects)) take(value, `${(0, import_path.basename)(proj)}/`);
|
|
256
|
+
}
|
|
257
|
+
return Object.keys(servers).length ? servers : null;
|
|
258
|
+
}
|
|
259
|
+
var SECRET_NAME = /(TOKEN|KEY|SECRET|PASSWORD|PASSWD|PAT|AUTH|CREDENTIAL|COOKIE|SESSION)/i;
|
|
260
|
+
var SECRET_VALUE = /^(sk-|sk_|ghp_|gho_|github_pat_|xox[abp]-|AKIA|AIza|hf_|npm_|glpat-|eyJ)/;
|
|
261
|
+
function mask(value) {
|
|
262
|
+
return value.length <= 4 ? "****redacted" : `${value.slice(0, 4)}****redacted`;
|
|
263
|
+
}
|
|
264
|
+
function redactMcpSecrets(servers) {
|
|
265
|
+
const clean = (node, parentKey = "") => {
|
|
266
|
+
if (typeof node === "string") {
|
|
267
|
+
if (/^\$\{|^<|^\$[A-Z_]/.test(node)) return node;
|
|
268
|
+
if (SECRET_NAME.test(parentKey) || SECRET_VALUE.test(node)) return mask(node);
|
|
269
|
+
const flag = node.match(/^(--?[A-Za-z0-9_-]*(?:token|key|secret|password|auth)[A-Za-z0-9_-]*=)(.+)$/i);
|
|
270
|
+
if (flag) return flag[1] + mask(flag[2]);
|
|
271
|
+
return node;
|
|
272
|
+
}
|
|
273
|
+
if (Array.isArray(node)) return node.map((item) => clean(item, parentKey));
|
|
274
|
+
if (node && typeof node === "object") {
|
|
275
|
+
const out = {};
|
|
276
|
+
for (const [k, v] of Object.entries(node)) out[k] = clean(v, k);
|
|
277
|
+
return out;
|
|
278
|
+
}
|
|
279
|
+
return node;
|
|
280
|
+
};
|
|
281
|
+
return clean(servers);
|
|
282
|
+
}
|
|
283
|
+
function existingMcpConfigs(cwd = process.cwd()) {
|
|
284
|
+
return knownMcpConfigPaths(cwd).filter((c2) => (0, import_fs.existsSync)(c2.path));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// src/mcp.ts
|
|
288
|
+
var SUPPORTED_PROTOCOLS = ["2025-06-18", "2025-03-26", "2024-11-05"];
|
|
289
|
+
var PRICING_URL = "https://aimodularity.com/agentverify/pricing/";
|
|
290
|
+
var DASHBOARD_URL = "https://aimodularity.com/agentverify/dashboard/";
|
|
291
|
+
var TOOLS = [
|
|
292
|
+
{
|
|
293
|
+
name: "scan_path",
|
|
294
|
+
title: "Scan agent files",
|
|
295
|
+
description: "Scan an AI agent for security risks before it is deployed: dangerous tool permissions, prompt injection paths, exposed secrets, risky MCP servers, missing approval gates, SQL/SSRF/code-execution risks, and framework-specific issues (LangChain, CrewAI, AutoGen, OpenAI Agents SDK, Claude Agent SDK). Give a file or a folder; a folder is scanned as one report using the most important agent files (up to 60 files / 5MB). Returns a verdict, a 0-100 security score (higher is safer) and findings with file and line.",
|
|
296
|
+
inputSchema: {
|
|
297
|
+
type: "object",
|
|
298
|
+
properties: { path: { type: "string", description: "File or folder to scan. Relative paths are resolved from the current project." } },
|
|
299
|
+
required: ["path"],
|
|
300
|
+
additionalProperties: false
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
name: "scan_code",
|
|
305
|
+
title: "Scan agent code",
|
|
306
|
+
description: "Scan a snippet of agent code, a tool definition, a system prompt or an MCP config for security risks. Use this for code that is not saved to a file yet. Returns a verdict, a security score and findings.",
|
|
307
|
+
inputSchema: {
|
|
308
|
+
type: "object",
|
|
309
|
+
properties: {
|
|
310
|
+
content: { type: "string", description: "The code or config to scan." },
|
|
311
|
+
fileName: { type: "string", description: "A file name with the right extension (e.g. agent.py, mcp.json) so the language is detected." }
|
|
312
|
+
},
|
|
313
|
+
required: ["content"],
|
|
314
|
+
additionalProperties: false
|
|
315
|
+
}
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
name: "scan_mcp_config",
|
|
319
|
+
title: "Check my MCP servers",
|
|
320
|
+
description: "Check the MCP servers configured on this computer (Claude Code, Claude Desktop, Cursor, VS Code, Windsurf) for risks such as broad filesystem access, unpinned packages, remote servers without auth, auto-approved tools and API keys stored in plain text. Only the server definitions are read, and secret values are masked on this computer before anything is sent.",
|
|
321
|
+
inputSchema: {
|
|
322
|
+
type: "object",
|
|
323
|
+
properties: { path: { type: "string", description: "Optional: a specific MCP config file. Leave empty to check all known locations." } },
|
|
324
|
+
additionalProperties: false
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
];
|
|
328
|
+
var SEVERITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
329
|
+
var SEVERITY_LABEL = { critical: "CRITICAL", high: "HIGH", medium: "MEDIUM", low: "LOW" };
|
|
330
|
+
function verdictLine(result) {
|
|
331
|
+
const verdict = result.verdict === "VERIFIED" ? "VERIFIED" : result.verdict === "NOT_ASSESSED" ? "NOT ASSESSED" : "NOT VERIFIED";
|
|
332
|
+
return `**Verdict: ${verdict}** \u2014 Security score ${result.riskScore}/100 (higher is safer, ${result.riskLevel})`;
|
|
333
|
+
}
|
|
334
|
+
function fixText(f) {
|
|
335
|
+
if (f.fixLocked) return `Fix: included with Agent Verify Pro (${PRICING_URL})`;
|
|
336
|
+
const steps = (f.recommendedFix ?? "").trim();
|
|
337
|
+
const code = (f.quickFix ?? f.fixCode ?? "").trim();
|
|
338
|
+
if (!steps && !code) return null;
|
|
339
|
+
return [steps ? `Fix: ${steps}` : "", code ? "```\n" + code + "\n```" : ""].filter(Boolean).join("\n");
|
|
340
|
+
}
|
|
341
|
+
function formatReport(result, opts) {
|
|
342
|
+
const findings = [...result.findings ?? []].sort((a, b) => (SEVERITY_ORDER[a.severity] ?? 9) - (SEVERITY_ORDER[b.severity] ?? 9));
|
|
343
|
+
const counts = ["critical", "high", "medium", "low"].map((s) => [s, findings.filter((f) => f.severity === s).length]).filter(([, n]) => n > 0);
|
|
344
|
+
const lines = [`# Agent Verify: ${opts.title}`, "", verdictLine(result)];
|
|
345
|
+
lines.push(findings.length ? `Findings: ${counts.map(([s, n]) => `${n} ${s}`).join(", ")}` : "No findings.");
|
|
346
|
+
if (result.reportInsights?.topBlocker) lines.push(`Fix first: ${result.reportInsights.topBlocker}`);
|
|
347
|
+
for (const note of opts.notes ?? []) lines.push(`Note: ${note}`);
|
|
348
|
+
findings.forEach((f, i) => {
|
|
349
|
+
const where = typeof f.line === "number" && f.line > 0 ? opts.files ? locateLine(opts.files, f.line) : { path: opts.title, line: f.line } : null;
|
|
350
|
+
lines.push("", `## ${i + 1}. [${SEVERITY_LABEL[f.severity] ?? f.severity}] ${f.title} (${f.code})`);
|
|
351
|
+
if (where) lines.push(`Location: ${where.path}:${where.line}`);
|
|
352
|
+
if (f.evidence) lines.push(`Evidence: ${f.evidence}`);
|
|
353
|
+
lines.push(`What is wrong: ${f.whatIsWrong}`);
|
|
354
|
+
if (f.whyItMatters) lines.push(`Why it matters: ${f.whyItMatters}`);
|
|
355
|
+
const fix = fixText(f);
|
|
356
|
+
if (fix) lines.push(fix);
|
|
357
|
+
});
|
|
358
|
+
const locked = findings.filter((f) => f.fixLocked).length;
|
|
359
|
+
if (locked > 0) lines.push("", `Step-by-step fixes and corrected code for ${locked} finding(s) are included with Pro: ${PRICING_URL}`);
|
|
360
|
+
if (result.reportUrl) lines.push("", `Full report: ${result.reportUrl}`);
|
|
361
|
+
lines.push("", "Agent Verify is an automated, evidence-based check, not a guarantee. Re-scan after making changes.");
|
|
362
|
+
return lines.join("\n");
|
|
363
|
+
}
|
|
364
|
+
function textResult(text, isError = false, structured) {
|
|
365
|
+
return { content: [{ type: "text", text }], ...isError ? { isError: true } : {}, ...structured ? { structuredContent: structured } : {} };
|
|
366
|
+
}
|
|
367
|
+
function summary(result) {
|
|
368
|
+
return {
|
|
369
|
+
verdict: result.verdict,
|
|
370
|
+
securityScore: result.riskScore,
|
|
371
|
+
riskLevel: result.riskLevel,
|
|
372
|
+
findings: (result.findings ?? []).map((f) => ({ code: f.code, severity: f.severity, title: f.title, line: f.line ?? null })),
|
|
373
|
+
reportUrl: result.reportUrl ?? null
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
async function callTool(name, args, opts) {
|
|
377
|
+
const tool = TOOLS.find((t) => t.name === name);
|
|
378
|
+
if (!tool) return textResult(`Unknown tool: ${name}`, true);
|
|
379
|
+
if (!opts.apiKey) {
|
|
380
|
+
return textResult(
|
|
381
|
+
"Agent Verify needs an API key. Create a free one in the dashboard (API / CLI tab): " + DASHBOARD_URL + "\nThen set AGENTVERIFY_API_KEY in this MCP server's environment and restart it.",
|
|
382
|
+
true
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
const doScan = opts.scanFn ?? scan;
|
|
386
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
387
|
+
try {
|
|
388
|
+
if (name === "scan_code") {
|
|
389
|
+
const content2 = typeof args.content === "string" ? args.content : "";
|
|
390
|
+
if (!content2.trim()) return textResult('Nothing to scan: "content" is empty.', true);
|
|
391
|
+
const fileName = typeof args.fileName === "string" && args.fileName.trim() ? args.fileName.trim() : "agent";
|
|
392
|
+
const result2 = await doScan({ content: content2, fileName, platform: "MCP" }, { apiKey: opts.apiKey, apiUrl: opts.apiUrl });
|
|
393
|
+
return textResult(formatReport(result2, { title: fileName }), false, summary(result2));
|
|
394
|
+
}
|
|
395
|
+
if (name === "scan_path") {
|
|
396
|
+
const raw = typeof args.path === "string" ? args.path.trim() : "";
|
|
397
|
+
if (!raw) return textResult('Give a file or folder "path" to scan.', true);
|
|
398
|
+
const target = (0, import_path2.resolve)(cwd, raw);
|
|
399
|
+
const picked = pickFiles(target);
|
|
400
|
+
const fileName = picked.files.length === 1 ? picked.files[0].path : `${raw.replace(/[\\/]+$/, "") || "project"} (${picked.files.length} files)`;
|
|
401
|
+
const result2 = await doScan({ content: combineFiles(picked.files), fileName, platform: "MCP" }, { apiKey: opts.apiKey, apiUrl: opts.apiUrl });
|
|
402
|
+
const notes = [...picked.notes];
|
|
403
|
+
if (picked.files.length > 1) notes.unshift(`Scanned ${picked.files.length} files together as one report.`);
|
|
404
|
+
return textResult(formatReport(result2, { title: fileName, files: picked.files, notes }), false, summary(result2));
|
|
405
|
+
}
|
|
406
|
+
const explicit = typeof args.path === "string" && args.path.trim() ? (0, import_path2.resolve)(cwd, args.path.trim()) : "";
|
|
407
|
+
const configs = explicit ? [{ client: "Config file", path: explicit }] : existingMcpConfigs(cwd);
|
|
408
|
+
if (configs.length === 0) return textResult('No MCP config files were found in the usual places (Claude Code, Claude Desktop, Cursor, VS Code, Windsurf). Pass a "path" to check a specific file.');
|
|
409
|
+
const servers = {};
|
|
410
|
+
const read = [];
|
|
411
|
+
for (const cfg of configs) {
|
|
412
|
+
let rawText = "";
|
|
413
|
+
try {
|
|
414
|
+
rawText = (0, import_fs2.readFileSync)(cfg.path, "utf8");
|
|
415
|
+
} catch {
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
const found = extractMcpServers(rawText);
|
|
419
|
+
if (!found) continue;
|
|
420
|
+
read.push(`${cfg.client}: ${Object.keys(found).length} server(s)`);
|
|
421
|
+
for (const [server, def] of Object.entries(found)) servers[`${cfg.client} \u203A ${server}`] = def;
|
|
422
|
+
}
|
|
423
|
+
if (Object.keys(servers).length === 0) return textResult(`Found ${configs.length} config file(s), but none of them define any MCP servers.`);
|
|
424
|
+
const content = JSON.stringify({ mcpServers: redactMcpSecrets(servers) }, null, 2);
|
|
425
|
+
const result = await doScan({ content, fileName: "mcp.json", platform: "MCP" }, { apiKey: opts.apiKey, apiUrl: opts.apiUrl });
|
|
426
|
+
return textResult(
|
|
427
|
+
formatReport(result, { title: `MCP servers (${Object.keys(servers).length})`, notes: [`Checked: ${read.join("; ")}`, "Secret values were masked on this computer before scanning."] }),
|
|
428
|
+
false,
|
|
429
|
+
summary(result)
|
|
430
|
+
);
|
|
431
|
+
} catch (err) {
|
|
432
|
+
return textResult(`Scan failed: ${err instanceof Error ? err.message : String(err)}`, true);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
async function handleMessage(msg, opts) {
|
|
436
|
+
const isNotification = msg.id === void 0 || msg.id === null;
|
|
437
|
+
const reply = (result) => isNotification ? null : { jsonrpc: "2.0", id: msg.id, result };
|
|
438
|
+
const fail = (code, message) => isNotification ? null : { jsonrpc: "2.0", id: msg.id ?? null, error: { code, message } };
|
|
439
|
+
switch (msg.method) {
|
|
440
|
+
case "initialize": {
|
|
441
|
+
const requested = typeof msg.params?.protocolVersion === "string" ? msg.params.protocolVersion : "";
|
|
442
|
+
return reply({
|
|
443
|
+
protocolVersion: SUPPORTED_PROTOCOLS.includes(requested) ? requested : SUPPORTED_PROTOCOLS[0],
|
|
444
|
+
capabilities: { tools: { listChanged: false } },
|
|
445
|
+
serverInfo: { name: "agentverify", title: "Agent Verify", version: opts.version },
|
|
446
|
+
instructions: "Use Agent Verify to check AI agent code, tools, prompts and MCP configs for security risks before deploying. Run scan_path on the agent folder after making changes, and fix critical and high findings first."
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
case "ping":
|
|
450
|
+
return reply({});
|
|
451
|
+
case "tools/list":
|
|
452
|
+
return reply({ tools: TOOLS });
|
|
453
|
+
case "tools/call": {
|
|
454
|
+
const name = typeof msg.params?.name === "string" ? msg.params.name : "";
|
|
455
|
+
const args = msg.params?.arguments && typeof msg.params.arguments === "object" ? msg.params.arguments : {};
|
|
456
|
+
return reply(await callTool(name, args, opts));
|
|
457
|
+
}
|
|
458
|
+
case "resources/list":
|
|
459
|
+
return reply({ resources: [] });
|
|
460
|
+
case "prompts/list":
|
|
461
|
+
return reply({ prompts: [] });
|
|
462
|
+
default:
|
|
463
|
+
if (typeof msg.method === "string" && msg.method.startsWith("notifications/")) return null;
|
|
464
|
+
return fail(-32601, `Method not found: ${msg.method}`);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
function runMcpServer(opts) {
|
|
468
|
+
return new Promise((done) => {
|
|
469
|
+
const rl = (0, import_readline.createInterface)({ input: process.stdin, crlfDelay: Infinity });
|
|
470
|
+
const send = (obj) => process.stdout.write(JSON.stringify(obj) + "\n");
|
|
471
|
+
let pending = 0;
|
|
472
|
+
let closed = false;
|
|
473
|
+
const maybeDone = () => {
|
|
474
|
+
if (closed && pending === 0) done();
|
|
475
|
+
};
|
|
476
|
+
rl.on("line", (line) => {
|
|
477
|
+
const text = line.trim();
|
|
478
|
+
if (!text) return;
|
|
479
|
+
let msg;
|
|
480
|
+
try {
|
|
481
|
+
msg = JSON.parse(text);
|
|
482
|
+
} catch {
|
|
483
|
+
send({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error" } });
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
pending++;
|
|
487
|
+
handleMessage(msg, opts).then((res) => {
|
|
488
|
+
if (res) send(res);
|
|
489
|
+
}).catch((err) => send({ jsonrpc: "2.0", id: msg.id ?? null, error: { code: -32603, message: err instanceof Error ? err.message : "Internal error" } })).finally(() => {
|
|
490
|
+
pending--;
|
|
491
|
+
maybeDone();
|
|
492
|
+
});
|
|
493
|
+
});
|
|
494
|
+
rl.on("close", () => {
|
|
495
|
+
closed = true;
|
|
496
|
+
maybeDone();
|
|
497
|
+
});
|
|
498
|
+
process.stderr.write(`Agent Verify MCP server ${opts.version} ready${opts.apiKey ? "" : " (no API key set: tools will explain how to add one)"}
|
|
499
|
+
`);
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// src/profile.ts
|
|
504
|
+
var import_fs3 = require("fs");
|
|
505
|
+
var SUPPORTED_PROFILES = ["owasp-agentic-skills-2026"];
|
|
506
|
+
var SUPPORTED_ASSESSMENT_SCHEMA = "1.1.0";
|
|
507
|
+
var LOCAL_LIMITS = { maxFiles: 5e3, maxFileBytes: 8 * 1024 * 1024, maxTotalBytes: 32 * 1024 * 1024, maxDepth: 64 };
|
|
508
|
+
var VENDORED_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "__pycache__"]);
|
|
509
|
+
var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
510
|
+
".png",
|
|
511
|
+
".jpg",
|
|
512
|
+
".jpeg",
|
|
513
|
+
".gif",
|
|
514
|
+
".webp",
|
|
515
|
+
".ico",
|
|
516
|
+
".bmp",
|
|
517
|
+
".tif",
|
|
518
|
+
".tiff",
|
|
519
|
+
".avif",
|
|
520
|
+
".pdf",
|
|
521
|
+
".woff",
|
|
522
|
+
".woff2",
|
|
523
|
+
".ttf",
|
|
524
|
+
".otf",
|
|
525
|
+
".eot",
|
|
526
|
+
".zip",
|
|
527
|
+
".gz",
|
|
528
|
+
".tgz",
|
|
529
|
+
".tar",
|
|
530
|
+
".bz2",
|
|
531
|
+
".xz",
|
|
532
|
+
".7z",
|
|
533
|
+
".mp3",
|
|
534
|
+
".mp4",
|
|
535
|
+
".mov",
|
|
536
|
+
".wav",
|
|
537
|
+
".ogg",
|
|
538
|
+
".webm",
|
|
539
|
+
".wasm",
|
|
540
|
+
".so",
|
|
541
|
+
".dll",
|
|
542
|
+
".dylib",
|
|
543
|
+
".exe",
|
|
544
|
+
".class",
|
|
545
|
+
".jar",
|
|
546
|
+
".pyc",
|
|
547
|
+
".sqlite",
|
|
548
|
+
".db"
|
|
549
|
+
]);
|
|
550
|
+
var PackageError = class extends Error {
|
|
551
|
+
};
|
|
552
|
+
function displayPath(path) {
|
|
553
|
+
return JSON.stringify(safeText(path));
|
|
554
|
+
}
|
|
555
|
+
function safeText(text) {
|
|
556
|
+
let out = "";
|
|
557
|
+
for (const ch of text) {
|
|
558
|
+
const cp = ch.codePointAt(0);
|
|
559
|
+
const hidden = cp < 32 || cp >= 127 && cp <= 159 || cp === 173 || cp === 1564 || cp === 6158 || cp === 65279 || cp >= 8203 && cp <= 8207 || cp >= 8234 && cp <= 8238 || cp >= 8288 && cp <= 8292 || cp >= 8294 && cp <= 8297 || cp >= 917504 && cp <= 917999 || cp >= 55296 && cp <= 57343;
|
|
560
|
+
out += hidden ? `\\u{${cp.toString(16).toUpperCase().padStart(4, "0")}}` : ch;
|
|
561
|
+
}
|
|
562
|
+
return out;
|
|
563
|
+
}
|
|
564
|
+
function nameProblem(name) {
|
|
565
|
+
for (let i = 0; i < name.length; i++) {
|
|
566
|
+
const c2 = name.charCodeAt(i);
|
|
567
|
+
if (c2 >= 55296 && c2 <= 56319) {
|
|
568
|
+
const next = name.charCodeAt(i + 1);
|
|
569
|
+
if (next >= 56320 && next <= 57343) {
|
|
570
|
+
i++;
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
return "contains an unpaired UTF-16 surrogate";
|
|
574
|
+
}
|
|
575
|
+
if (c2 >= 56320 && c2 <= 57343) return "contains an unpaired UTF-16 surrogate";
|
|
576
|
+
}
|
|
577
|
+
return null;
|
|
578
|
+
}
|
|
579
|
+
var strictUtf8 = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
|
|
580
|
+
function decodeStrict(bytes) {
|
|
581
|
+
return strictUtf8.decode(bytes);
|
|
582
|
+
}
|
|
583
|
+
function listEntries(dir, shown) {
|
|
584
|
+
const entries = [];
|
|
585
|
+
if (process.platform === "win32") {
|
|
586
|
+
for (const d of (0, import_fs3.readdirSync)(dir, { withFileTypes: true })) {
|
|
587
|
+
const problem = nameProblem(d.name);
|
|
588
|
+
if (problem) throw new PackageError(`An entry in ${displayPath(shown)} ${problem}, so it cannot be sent faithfully.`);
|
|
589
|
+
entries.push({ name: d.name, kind: d.isSymbolicLink() ? "symlink" : d.isDirectory() ? "dir" : d.isFile() ? "file" : "other" });
|
|
590
|
+
}
|
|
591
|
+
} else {
|
|
592
|
+
for (const d of (0, import_fs3.readdirSync)(dir, { withFileTypes: true, encoding: "buffer" })) {
|
|
593
|
+
let name;
|
|
594
|
+
try {
|
|
595
|
+
name = decodeStrict(d.name);
|
|
596
|
+
} catch {
|
|
597
|
+
throw new PackageError(`An entry in ${displayPath(shown)} has a name that is not valid UTF-8, so it cannot be sent faithfully.`);
|
|
598
|
+
}
|
|
599
|
+
entries.push({ name, kind: d.isSymbolicLink() ? "symlink" : d.isDirectory() ? "dir" : d.isFile() ? "file" : "other" });
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
return entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
603
|
+
}
|
|
604
|
+
var symlinkMessage = (rel) => `${displayPath(rel)} is a symbolic link. The CLI does not follow symbolic links, and a package containing one cannot be represented faithfully. Replace it with the file or directory it points to, or remove it.`;
|
|
605
|
+
var specialMessage = (rel) => `${displayPath(rel)} is not a regular file or directory (it is a device, socket or pipe), so it cannot be sent.`;
|
|
606
|
+
function readRegularFile(full, rel, budget) {
|
|
607
|
+
let fd;
|
|
608
|
+
try {
|
|
609
|
+
fd = (0, import_fs3.openSync)(full, import_fs3.constants.O_RDONLY | (import_fs3.constants.O_NOFOLLOW ?? 0));
|
|
610
|
+
} catch (e) {
|
|
611
|
+
const code = e.code;
|
|
612
|
+
if (code === "ELOOP") throw new PackageError(symlinkMessage(rel));
|
|
613
|
+
throw new PackageError(`Could not read ${displayPath(rel)}: ${code ?? "unknown error"}.`);
|
|
614
|
+
}
|
|
615
|
+
try {
|
|
616
|
+
const st = (0, import_fs3.fstatSync)(fd);
|
|
617
|
+
if (!st.isFile()) throw new PackageError(specialMessage(rel));
|
|
618
|
+
if (st.size > LOCAL_LIMITS.maxFileBytes) throw new PackageError(`${displayPath(rel)} is ${st.size} bytes, over the CLI's local read guard of ${LOCAL_LIMITS.maxFileBytes} bytes.`);
|
|
619
|
+
const bytes = (0, import_fs3.readFileSync)(fd);
|
|
620
|
+
if (bytes.length > LOCAL_LIMITS.maxFileBytes) throw new PackageError(`${displayPath(rel)} grew past the CLI's local read guard while it was being read.`);
|
|
621
|
+
budget.total += bytes.length;
|
|
622
|
+
if (budget.total > LOCAL_LIMITS.maxTotalBytes) throw new PackageError(`The package is larger than the CLI's local guard of ${LOCAL_LIMITS.maxTotalBytes} bytes.`);
|
|
623
|
+
return bytes;
|
|
624
|
+
} finally {
|
|
625
|
+
(0, import_fs3.closeSync)(fd);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
var extensionOf = (name) => {
|
|
629
|
+
const i = name.lastIndexOf(".");
|
|
630
|
+
return i <= 0 ? "" : name.slice(i).toLowerCase();
|
|
631
|
+
};
|
|
632
|
+
function buildPackage(root) {
|
|
633
|
+
let rootStat;
|
|
634
|
+
try {
|
|
635
|
+
rootStat = (0, import_fs3.lstatSync)(root);
|
|
636
|
+
} catch {
|
|
637
|
+
throw new PackageError(`${displayPath(root)} does not exist or cannot be read.`);
|
|
638
|
+
}
|
|
639
|
+
if (rootStat.isSymbolicLink()) throw new PackageError(symlinkMessage(root));
|
|
640
|
+
if (!rootStat.isDirectory()) throw new PackageError(`${displayPath(root)} is not a directory. A profile assessment takes a directory.`);
|
|
641
|
+
const files = [];
|
|
642
|
+
const skipped = [];
|
|
643
|
+
const budget = { total: 0 };
|
|
644
|
+
const walk2 = (dir, prefix, depth) => {
|
|
645
|
+
if (depth > LOCAL_LIMITS.maxDepth) throw new PackageError(`The package is nested deeper than ${LOCAL_LIMITS.maxDepth} directories.`);
|
|
646
|
+
for (const entry of listEntries(dir, prefix || ".")) {
|
|
647
|
+
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
648
|
+
const full = `${dir}/${entry.name}`;
|
|
649
|
+
if (entry.kind === "symlink") throw new PackageError(symlinkMessage(rel));
|
|
650
|
+
if (entry.kind === "other") throw new PackageError(specialMessage(rel));
|
|
651
|
+
if (entry.kind === "dir") {
|
|
652
|
+
if (VENDORED_DIRS.has(entry.name)) {
|
|
653
|
+
skipped.push({ path: rel, reason: "vendored directory" });
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
walk2(full, rel, depth + 1);
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
659
|
+
if (BINARY_EXTENSIONS.has(extensionOf(entry.name))) {
|
|
660
|
+
skipped.push({ path: rel, reason: "binary asset" });
|
|
661
|
+
continue;
|
|
662
|
+
}
|
|
663
|
+
if (files.length >= LOCAL_LIMITS.maxFiles) throw new PackageError(`The package has more than ${LOCAL_LIMITS.maxFiles} files, over the CLI's local guard.`);
|
|
664
|
+
const bytes = readRegularFile(full, rel, budget);
|
|
665
|
+
let content;
|
|
666
|
+
try {
|
|
667
|
+
content = decodeStrict(bytes);
|
|
668
|
+
} catch {
|
|
669
|
+
throw new PackageError(`${displayPath(rel)} is not valid UTF-8 text, so it cannot be sent faithfully. If it is a binary asset, give it a recognized binary extension or remove it from the package.`);
|
|
670
|
+
}
|
|
671
|
+
files.push({ path: rel, content });
|
|
672
|
+
}
|
|
673
|
+
};
|
|
674
|
+
walk2(root, "", 0);
|
|
675
|
+
if (files.length === 0) throw new PackageError(`${displayPath(root)} contains no files to assess.`);
|
|
676
|
+
return { files, skipped };
|
|
677
|
+
}
|
|
678
|
+
function serializeRequest(profile, files) {
|
|
679
|
+
return JSON.stringify({ profile, files });
|
|
680
|
+
}
|
|
681
|
+
async function postProfile(apiUrl, apiKey, body, timeoutMs) {
|
|
682
|
+
const controller = new AbortController();
|
|
683
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
684
|
+
try {
|
|
685
|
+
const response = await fetch(apiUrl, {
|
|
686
|
+
method: "POST",
|
|
687
|
+
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json", "User-Agent": "agentverify-cli/2.0.0" },
|
|
688
|
+
body,
|
|
689
|
+
signal: controller.signal
|
|
690
|
+
});
|
|
691
|
+
const text = await response.text();
|
|
692
|
+
let json = null;
|
|
693
|
+
try {
|
|
694
|
+
json = JSON.parse(text);
|
|
695
|
+
} catch {
|
|
696
|
+
json = null;
|
|
697
|
+
}
|
|
698
|
+
return { kind: "response", status: response.status, text, json };
|
|
699
|
+
} catch (e) {
|
|
700
|
+
if (e instanceof Error && e.name === "AbortError") return { kind: "failure", message: `Request timed out after ${timeoutMs}ms` };
|
|
701
|
+
return { kind: "failure", message: `Request failed: ${e instanceof Error ? e.message : String(e)}` };
|
|
702
|
+
} finally {
|
|
703
|
+
clearTimeout(timer);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
707
|
+
var str = (v) => typeof v === "string" ? safeText(v) : typeof v === "number" || typeof v === "boolean" ? String(v) : "";
|
|
708
|
+
function formatAssessment(body, skipped) {
|
|
709
|
+
const a = isObj(body.assessment) ? body.assessment : {};
|
|
710
|
+
const profile = isObj(a.profile) ? a.profile : {};
|
|
711
|
+
const pkg = isObj(a.package) ? a.package : {};
|
|
712
|
+
const lines = [];
|
|
713
|
+
lines.push("Agent Verify profile assessment");
|
|
714
|
+
lines.push(`Profile: ${str(body.profile)}`);
|
|
715
|
+
lines.push(`Framework: ${str(profile.framework)} (upstream ${str(profile.upstreamRepo)} @ ${str(profile.upstreamCommit).slice(0, 12)}, ${str(profile.upstreamStatus)}; profile version ${str(profile.agentverifyProfileVersion)})`);
|
|
716
|
+
lines.push(`Assessment schema: ${str(a.schemaVersion)}`);
|
|
717
|
+
lines.push(`Interpretation: scanner ${str(profile.scannerVersion)}, engine ${str(profile.assessmentEngineVersion)}, risk rubric ${str(profile.riskRubricVersion)}, key allowlist ${str(profile.keyAllowlistVersion)}, normalization ${str(profile.normalizationVersion)}`);
|
|
718
|
+
lines.push(`Package: ${str(pkg.fileCount)} files, digest ${str(pkg.digest)}`);
|
|
719
|
+
lines.push(`Attestation: ${body.attestation === null ? "none (this profile assessment is unsigned)" : "present (not verified by this CLI)"}`);
|
|
720
|
+
lines.push("");
|
|
721
|
+
for (const control of Array.isArray(a.controls) ? a.controls : []) {
|
|
722
|
+
if (!isObj(control)) continue;
|
|
723
|
+
const cov = isObj(control.coverage) ? control.coverage : {};
|
|
724
|
+
lines.push(`${str(control.controlId)} ${str(control.title)}: ${str(control.status)} (confidence ${str(control.confidence)})`);
|
|
725
|
+
lines.push(` ${str(cov.evidenceObserved)} evidence observed, ${str(cov.gapIdentified)} gaps identified, ${str(cov.notAssessed)} not assessed, of ${str(cov.total)} checks`);
|
|
726
|
+
for (const check of Array.isArray(control.checks) ? control.checks : []) {
|
|
727
|
+
if (isObj(check)) lines.push(` ${str(check.checkId)} ${str(check.status)}: ${str(check.title)}`);
|
|
728
|
+
}
|
|
729
|
+
lines.push("");
|
|
730
|
+
}
|
|
731
|
+
const gaps = (Array.isArray(a.evidence) ? a.evidence : []).filter((e) => isObj(e) && e.polarity === "gap");
|
|
732
|
+
lines.push(`Gaps identified (${gaps.length}):`);
|
|
733
|
+
for (const g of gaps) {
|
|
734
|
+
lines.push(` [${str(g.severity)}] ${str(g.kind)}: ${str(g.summary)}`);
|
|
735
|
+
const locations = (Array.isArray(g.locations) ? g.locations : []).filter(isObj).slice(0, 3).map((l) => `${str(l.file)}${l.line !== void 0 ? `:${str(l.line)}` : ""}`);
|
|
736
|
+
if (locations.length) lines.push(` at ${locations.join(", ")}`);
|
|
737
|
+
}
|
|
738
|
+
if (gaps.length === 0) lines.push(" none");
|
|
739
|
+
const notImplemented = Array.isArray(a.notImplementedControls) ? a.notImplementedControls.map(str).join(", ") : "";
|
|
740
|
+
lines.push("");
|
|
741
|
+
lines.push(`Not assessed by this profile version: ${notImplemented || "none"}`);
|
|
742
|
+
for (const note of Array.isArray(a.notes) ? a.notes : []) lines.push(`Note: ${str(note)}`);
|
|
743
|
+
if (skipped.length > 0) {
|
|
744
|
+
lines.push("");
|
|
745
|
+
lines.push(`Skipped from the package (${skipped.length}), not sent:`);
|
|
746
|
+
for (const s of skipped.slice(0, 20)) lines.push(` ${displayPath(s.path)}: ${s.reason}`);
|
|
747
|
+
if (skipped.length > 20) lines.push(` and ${skipped.length - 20} more`);
|
|
748
|
+
}
|
|
749
|
+
return lines;
|
|
750
|
+
}
|
|
751
|
+
|
|
118
752
|
// src/cli.ts
|
|
119
|
-
var VERSION = "
|
|
753
|
+
var VERSION = "2.0.0";
|
|
120
754
|
var API_URL = process.env.AGENTVERIFY_API_URL ?? "https://agentverify-api.agentverify.workers.dev/v1/scan";
|
|
121
755
|
var A2SPA_DOCS_URL = "https://aimodularity.com/A2SPA/docs";
|
|
122
756
|
var EXIT_OK = 0;
|
|
@@ -136,20 +770,20 @@ var c = {
|
|
|
136
770
|
};
|
|
137
771
|
var write = (value = "") => process.stdout.write(`${value}
|
|
138
772
|
`);
|
|
139
|
-
var SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".ts", ".py", ".json", ".yaml", ".yml", ".md", ".mjs", ".cjs"]);
|
|
773
|
+
var SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx", ".py", ".json", ".yaml", ".yml", ".toml", ".md", ".mjs", ".cjs", ".sh"]);
|
|
140
774
|
var MAX_SCAN_FILE_BYTES = 5 * 1024 * 1024;
|
|
141
|
-
var
|
|
775
|
+
var SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", "vendor", "coverage", ".cache"]);
|
|
142
776
|
function collectFiles(dir) {
|
|
143
777
|
const files = [];
|
|
144
778
|
try {
|
|
145
|
-
const entries = (0,
|
|
779
|
+
const entries = (0, import_fs4.readdirSync)(dir);
|
|
146
780
|
for (const entry of entries) {
|
|
147
|
-
if (
|
|
148
|
-
const fullPath = (0,
|
|
149
|
-
const stat = (0,
|
|
781
|
+
if (SKIP_DIRS2.has(entry)) continue;
|
|
782
|
+
const fullPath = (0, import_path3.join)(dir, entry);
|
|
783
|
+
const stat = (0, import_fs4.statSync)(fullPath);
|
|
150
784
|
if (stat.isDirectory()) {
|
|
151
785
|
files.push(...collectFiles(fullPath));
|
|
152
|
-
} else if (SUPPORTED_EXTENSIONS.has((0,
|
|
786
|
+
} else if (SUPPORTED_EXTENSIONS.has((0, import_path3.extname)(entry).toLowerCase())) {
|
|
153
787
|
files.push(fullPath);
|
|
154
788
|
}
|
|
155
789
|
}
|
|
@@ -171,7 +805,8 @@ function parseArgs(argv) {
|
|
|
171
805
|
help: false,
|
|
172
806
|
version: false,
|
|
173
807
|
policy: "",
|
|
174
|
-
summaryFile: ""
|
|
808
|
+
summaryFile: "",
|
|
809
|
+
profiles: []
|
|
175
810
|
};
|
|
176
811
|
if (result.command === "--help" || result.command === "-h") {
|
|
177
812
|
result.command = "";
|
|
@@ -191,6 +826,8 @@ function parseArgs(argv) {
|
|
|
191
826
|
else if (arg === "--allow-not-assessed") result.allowNotAssessed = true;
|
|
192
827
|
else if (arg === "--policy") result.policy = args[++i] ?? "";
|
|
193
828
|
else if (arg === "--summary-file") result.summaryFile = args[++i] ?? "";
|
|
829
|
+
else if (arg === "--profile") result.profiles.push(args[++i] ?? "");
|
|
830
|
+
else if (arg.startsWith("--profile=")) result.profiles.push(arg.slice("--profile=".length));
|
|
194
831
|
else if (arg === "--help" || arg === "-h") result.help = true;
|
|
195
832
|
else if (arg === "--version" || arg === "-v") result.version = true;
|
|
196
833
|
else if (!arg.startsWith("-")) result.dir = arg;
|
|
@@ -206,9 +843,12 @@ ${c.gray}Execution trust analysis for AI agents${c.reset}
|
|
|
206
843
|
${c.bold}Usage:${c.reset}
|
|
207
844
|
agentverify scan [dir] --key <api-key>
|
|
208
845
|
agentverify scan --file <file> --key <api-key>
|
|
846
|
+
agentverify mcp
|
|
209
847
|
|
|
210
848
|
${c.bold}Commands:${c.reset}
|
|
211
849
|
scan Scan agent files in a directory or a single file
|
|
850
|
+
mcp Run as an MCP server so Claude Code, Cursor, VS Code and other AI
|
|
851
|
+
tools can scan agents while you code (reads AGENTVERIFY_API_KEY)
|
|
212
852
|
|
|
213
853
|
${c.bold}Options:${c.reset}
|
|
214
854
|
--key, -k API key (or set AGENTVERIFY_API_KEY env var)
|
|
@@ -222,6 +862,11 @@ ${c.bold}Options:${c.reset}
|
|
|
222
862
|
in addition to the scanner's own verdict (e.g. financial-agent)
|
|
223
863
|
--summary-file <path>
|
|
224
864
|
Write results as JSON to this path (in addition to any other output)
|
|
865
|
+
--profile <id>
|
|
866
|
+
(alpha) Assess a directory against an assessment profile instead of
|
|
867
|
+
scanning files individually. Supported: ${SUPPORTED_PROFILES.join(", ")}
|
|
868
|
+
Takes a directory; not combinable with --file, --ci, --policy,
|
|
869
|
+
--allow-not-assessed, --markdown or --summary-file.
|
|
225
870
|
--version, -v Print version
|
|
226
871
|
--help, -h Show this help message
|
|
227
872
|
|
|
@@ -241,12 +886,23 @@ ${c.bold}Examples:${c.reset}
|
|
|
241
886
|
${c.gray}# Markdown summary for pull requests${c.reset}
|
|
242
887
|
agentverify scan . --key av_your_key --markdown
|
|
243
888
|
|
|
889
|
+
${c.gray}# Let Claude Code scan your agents (one-time setup)${c.reset}
|
|
890
|
+
claude mcp add agentverify -e AGENTVERIFY_API_KEY=av_your_key -- npx -y agentverify mcp
|
|
891
|
+
|
|
892
|
+
${c.gray}# Assess a skill directory against a profile (alpha); --json prints the service's response${c.reset}
|
|
893
|
+
agentverify scan ./my-skill --profile ${SUPPORTED_PROFILES[0]} --key av_your_key
|
|
894
|
+
|
|
244
895
|
${c.bold}Exit codes (--ci):${c.reset}
|
|
245
896
|
0 All scanned files verified
|
|
246
897
|
1 NOT VERIFIED \u2014 a security/verification check failed
|
|
247
898
|
2 NOT ASSESSED \u2014 insufficient evidence for a verdict (see --allow-not-assessed)
|
|
248
899
|
3 Execution error \u2014 the scan itself could not run (bad key, network, missing file)
|
|
249
900
|
|
|
901
|
+
${c.bold}Exit codes (--profile):${c.reset}
|
|
902
|
+
0 The assessment completed. Control and check statuses (including GAP_IDENTIFIED)
|
|
903
|
+
do not affect the exit code.
|
|
904
|
+
3 Usage or execution error \u2014 nothing was assessed
|
|
905
|
+
|
|
250
906
|
${c.bold}Get your API key:${c.reset}
|
|
251
907
|
https://aimodularity.com/agentverify/dashboard
|
|
252
908
|
|
|
@@ -300,6 +956,8 @@ ${c.bold}Fix first:${c.reset}`);
|
|
|
300
956
|
else write(` No blocking fixes detected. Re-scan after any material change.`);
|
|
301
957
|
if (result.reportInsights?.nextAction) write(`
|
|
302
958
|
${c.bold}Next action:${c.reset} ${result.reportInsights.nextAction}`);
|
|
959
|
+
const lockedFixes = (result.findings ?? []).filter((f) => f.fixLocked).length;
|
|
960
|
+
if (lockedFixes > 0) write(`${c.gray}Step-by-step fixes and corrected code for ${lockedFixes} finding${lockedFixes === 1 ? "" : "s"} are included with Pro: https://aimodularity.com/agentverify/pricing/${c.reset}`);
|
|
303
961
|
const threats = getRelevantThreatCategories(result);
|
|
304
962
|
if (threats.length) {
|
|
305
963
|
write(`
|
|
@@ -311,8 +969,8 @@ ${c.bold}Threat categories:${c.reset}`);
|
|
|
311
969
|
write(`${c.gray}Re-scan:${c.reset} agentverify scan --file ${fileName} --key $AGENTVERIFY_API_KEY
|
|
312
970
|
`);
|
|
313
971
|
}
|
|
314
|
-
function markdownSummary(results,
|
|
315
|
-
const lines = ["# Agent Verify", "", `Files scanned: ${
|
|
972
|
+
function markdownSummary(results, summary2) {
|
|
973
|
+
const lines = ["# Agent Verify", "", `Files scanned: ${summary2.total}`, `Verified: ${summary2.verified}`, `Not verified: ${summary2.notVerified}`, `Not assessed: ${summary2.notAssessed}`, `Errors: ${summary2.errors}`, ""];
|
|
316
974
|
for (const item of results) {
|
|
317
975
|
if (!item.result) {
|
|
318
976
|
lines.push(`## ${item.file}`, "", `Error: ${item.error ?? "Unknown error"}`, "");
|
|
@@ -332,20 +990,20 @@ function markdownSummary(results, summary) {
|
|
|
332
990
|
lines.push(`A2SPA docs: ${A2SPA_DOCS_URL}`);
|
|
333
991
|
return lines.join("\n");
|
|
334
992
|
}
|
|
335
|
-
function writeGithubJobSummary(results,
|
|
993
|
+
function writeGithubJobSummary(results, summary2) {
|
|
336
994
|
const summaryPath = process.env.GITHUB_STEP_SUMMARY;
|
|
337
995
|
if (!summaryPath) return;
|
|
338
996
|
try {
|
|
339
|
-
(0,
|
|
997
|
+
(0, import_fs4.appendFileSync)(summaryPath, `${markdownSummary(results, summary2)}
|
|
340
998
|
`);
|
|
341
999
|
} catch (e) {
|
|
342
1000
|
console.error(`Warning: could not write GitHub job summary: ${e instanceof Error ? e.message : e}`);
|
|
343
1001
|
}
|
|
344
1002
|
}
|
|
345
|
-
function writeSummaryFile(path, results,
|
|
1003
|
+
function writeSummaryFile(path, results, summary2) {
|
|
346
1004
|
if (!path) return;
|
|
347
1005
|
try {
|
|
348
|
-
(0,
|
|
1006
|
+
(0, import_fs4.writeFileSync)(path, JSON.stringify({ results, summary: summary2 }, null, 2));
|
|
349
1007
|
} catch (e) {
|
|
350
1008
|
console.error(`Warning: could not write summary file: ${e instanceof Error ? e.message : e}`);
|
|
351
1009
|
}
|
|
@@ -388,7 +1046,7 @@ ${c.bold}Agent Verify${c.reset} ${c.gray}v${VERSION}${c.reset}`);
|
|
|
388
1046
|
files = collectFiles(dir);
|
|
389
1047
|
if (files.length === 0) {
|
|
390
1048
|
write(`${c.yellow}No agent files found in ${dir}${c.reset}`);
|
|
391
|
-
write(`${c.gray}Supported: .js .ts .py .json .yaml .yml .md${c.reset}
|
|
1049
|
+
write(`${c.gray}Supported: .js .jsx .ts .tsx .py .json .yaml .yml .toml .md .sh${c.reset}
|
|
392
1050
|
`);
|
|
393
1051
|
process.exit(0);
|
|
394
1052
|
}
|
|
@@ -403,12 +1061,12 @@ ${c.bold}Agent Verify${c.reset} ${c.gray}v${VERSION}${c.reset}`);
|
|
|
403
1061
|
const batch = files.slice(i, i + CONCURRENCY);
|
|
404
1062
|
const batchResults = await Promise.allSettled(
|
|
405
1063
|
batch.map(async (filePath) => {
|
|
406
|
-
const fileStat = (0,
|
|
1064
|
+
const fileStat = (0, import_fs4.statSync)(filePath);
|
|
407
1065
|
if (fileStat.size > MAX_SCAN_FILE_BYTES) {
|
|
408
1066
|
throw new Error(`File is ${(fileStat.size / 1024 / 1024).toFixed(1)}MB, over the ${MAX_SCAN_FILE_BYTES / 1024 / 1024}MB scan limit \u2014 skipped`);
|
|
409
1067
|
}
|
|
410
|
-
const content = (0,
|
|
411
|
-
const relPath = (0,
|
|
1068
|
+
const content = (0, import_fs4.readFileSync)(filePath, "utf-8");
|
|
1069
|
+
const relPath = (0, import_path3.relative)(args.dir === "." ? process.cwd() : args.dir, filePath);
|
|
412
1070
|
const result = await scan(
|
|
413
1071
|
{ content, fileName: relPath, policyId: args.policy || void 0 },
|
|
414
1072
|
{ apiKey: args.key, apiUrl: API_URL }
|
|
@@ -430,7 +1088,7 @@ ${c.bold}Agent Verify${c.reset} ${c.gray}v${VERSION}${c.reset}`);
|
|
|
430
1088
|
else notVerified++;
|
|
431
1089
|
} else {
|
|
432
1090
|
const filePath = batch[j];
|
|
433
|
-
const relPath = (0,
|
|
1091
|
+
const relPath = (0, import_path3.relative)(args.dir, filePath);
|
|
434
1092
|
const errMsg = outcome.reason instanceof Error ? outcome.reason.message : String(outcome.reason);
|
|
435
1093
|
if (!args.json) {
|
|
436
1094
|
write(` ${c.yellow}\u26A0${c.reset} ${c.yellow}WARNING${c.reset} ${relPath.padEnd(30)} ${c.gray}${errMsg}${c.reset}`);
|
|
@@ -440,11 +1098,11 @@ ${c.bold}Agent Verify${c.reset} ${c.gray}v${VERSION}${c.reset}`);
|
|
|
440
1098
|
}
|
|
441
1099
|
}
|
|
442
1100
|
}
|
|
443
|
-
const
|
|
444
|
-
writeGithubJobSummary(jsonResults,
|
|
445
|
-
writeSummaryFile(args.summaryFile, jsonResults,
|
|
1101
|
+
const summary2 = { total: files.length, verified, notVerified, notAssessed, errors };
|
|
1102
|
+
writeGithubJobSummary(jsonResults, summary2);
|
|
1103
|
+
writeSummaryFile(args.summaryFile, jsonResults, summary2);
|
|
446
1104
|
if (args.json) {
|
|
447
|
-
write(JSON.stringify({ results: jsonResults, summary }, null, 2));
|
|
1105
|
+
write(JSON.stringify({ results: jsonResults, summary: summary2 }, null, 2));
|
|
448
1106
|
if (errors > 0) process.exit(EXIT_EXECUTION_ERROR);
|
|
449
1107
|
if (args.ci) {
|
|
450
1108
|
if (notVerified > 0) process.exit(EXIT_NOT_VERIFIED);
|
|
@@ -453,7 +1111,7 @@ ${c.bold}Agent Verify${c.reset} ${c.gray}v${VERSION}${c.reset}`);
|
|
|
453
1111
|
return;
|
|
454
1112
|
}
|
|
455
1113
|
if (args.markdown) {
|
|
456
|
-
write(markdownSummary(jsonResults,
|
|
1114
|
+
write(markdownSummary(jsonResults, summary2));
|
|
457
1115
|
if (errors > 0) process.exit(EXIT_EXECUTION_ERROR);
|
|
458
1116
|
if (args.ci) {
|
|
459
1117
|
if (notVerified > 0) process.exit(EXIT_NOT_VERIFIED);
|
|
@@ -483,6 +1141,89 @@ ${c.gray}No dashboard report was saved. Check API key access and retry if you ne
|
|
|
483
1141
|
if (notAssessed > 0 && !args.allowNotAssessed) process.exit(EXIT_NOT_ASSESSED);
|
|
484
1142
|
}
|
|
485
1143
|
}
|
|
1144
|
+
var PROFILE_TIMEOUT_MS = 6e4;
|
|
1145
|
+
function profileUsageError(message) {
|
|
1146
|
+
console.error(`
|
|
1147
|
+
${c.red}Error:${c.reset} ${message}`);
|
|
1148
|
+
console.error(`Run ${c.cyan}agentverify --help${c.reset} for usage
|
|
1149
|
+
`);
|
|
1150
|
+
process.exitCode = EXIT_EXECUTION_ERROR;
|
|
1151
|
+
}
|
|
1152
|
+
function profileFailure(json, message, detail = {}) {
|
|
1153
|
+
if (json) write(JSON.stringify({ error: message, ...detail }, null, 2));
|
|
1154
|
+
else console.error(`
|
|
1155
|
+
${c.red}Error:${c.reset} ${safeText(message)}
|
|
1156
|
+
`);
|
|
1157
|
+
process.exitCode = EXIT_EXECUTION_ERROR;
|
|
1158
|
+
}
|
|
1159
|
+
async function runProfile(args) {
|
|
1160
|
+
if (args.profiles.length > 1) return profileUsageError("--profile was given more than once.");
|
|
1161
|
+
const profile = args.profiles[0];
|
|
1162
|
+
if (profile === "") return profileUsageError(`--profile requires a value. Supported profiles: ${SUPPORTED_PROFILES.join(", ")}`);
|
|
1163
|
+
if (!SUPPORTED_PROFILES.includes(profile)) {
|
|
1164
|
+
return profileUsageError(`Unknown profile ${JSON.stringify(safeText(profile))}. Supported profiles: ${SUPPORTED_PROFILES.join(", ")}`);
|
|
1165
|
+
}
|
|
1166
|
+
const conflicts = [];
|
|
1167
|
+
if (args.file) conflicts.push("--file");
|
|
1168
|
+
if (args.ci) conflicts.push("--ci");
|
|
1169
|
+
if (args.allowNotAssessed) conflicts.push("--allow-not-assessed");
|
|
1170
|
+
if (args.policy) conflicts.push("--policy");
|
|
1171
|
+
if (args.markdown) conflicts.push("--markdown");
|
|
1172
|
+
if (args.summaryFile) conflicts.push("--summary-file");
|
|
1173
|
+
if (conflicts.length > 0) return profileUsageError(`--profile cannot be combined with ${conflicts.join(", ")}. A profile assessment takes a directory and reports its own result.`);
|
|
1174
|
+
if (!args.key) {
|
|
1175
|
+
console.error(`
|
|
1176
|
+
${c.red}Error:${c.reset} API key required
|
|
1177
|
+
`);
|
|
1178
|
+
console.error(` Set with --key av_your_key or AGENTVERIFY_API_KEY env var.`);
|
|
1179
|
+
console.error(` Get your key at https://aimodularity.com/agentverify/dashboard/
|
|
1180
|
+
`);
|
|
1181
|
+
process.exitCode = EXIT_EXECUTION_ERROR;
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1184
|
+
let built;
|
|
1185
|
+
try {
|
|
1186
|
+
built = buildPackage(args.dir);
|
|
1187
|
+
} catch (e) {
|
|
1188
|
+
return profileFailure(args.json, e instanceof PackageError ? e.message : `Could not read the package: ${e instanceof Error ? e.message : String(e)}`);
|
|
1189
|
+
}
|
|
1190
|
+
if (args.json && built.skipped.length > 0) {
|
|
1191
|
+
console.error(`Note: ${built.skipped.length} entries were skipped from the package and not sent: ${built.skipped.slice(0, 20).map((s) => `${displayPath(s.path)} (${s.reason})`).join(", ")}${built.skipped.length > 20 ? `, and ${built.skipped.length - 20} more` : ""}`);
|
|
1192
|
+
}
|
|
1193
|
+
const result = await postProfile(API_URL, args.key, serializeRequest(profile, built.files), PROFILE_TIMEOUT_MS);
|
|
1194
|
+
if (result.kind === "failure") return profileFailure(args.json, result.message);
|
|
1195
|
+
const body = result.json;
|
|
1196
|
+
const isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1197
|
+
if (result.status !== 200) {
|
|
1198
|
+
if (isObject(body) && typeof body.error === "string") {
|
|
1199
|
+
if (args.json) {
|
|
1200
|
+
write(JSON.stringify(body, null, 2));
|
|
1201
|
+
} else {
|
|
1202
|
+
console.error(`
|
|
1203
|
+
${c.red}Error:${c.reset} ${safeText(body.error)} (HTTP ${result.status})`);
|
|
1204
|
+
const rejection = body.rejection;
|
|
1205
|
+
if (isObject(rejection)) console.error(` rejection: ${safeText(String(rejection.code ?? ""))}: ${safeText(String(rejection.message ?? ""))}${typeof rejection.path === "string" ? ` (${JSON.stringify(safeText(rejection.path))})` : ""}`);
|
|
1206
|
+
if (Array.isArray(body.supportedProfiles)) console.error(` supported profiles: ${body.supportedProfiles.map((p) => safeText(String(p))).join(", ")}`);
|
|
1207
|
+
console.error("");
|
|
1208
|
+
}
|
|
1209
|
+
process.exitCode = EXIT_EXECUTION_ERROR;
|
|
1210
|
+
return;
|
|
1211
|
+
}
|
|
1212
|
+
return profileFailure(args.json, `Unexpected response from the service (HTTP ${result.status})`, { status: result.status });
|
|
1213
|
+
}
|
|
1214
|
+
if (!isObject(body) || typeof body.profile !== "string" || !isObject(body.assessment)) {
|
|
1215
|
+
return profileFailure(args.json, "The service returned a response that is not a profile assessment", { status: result.status });
|
|
1216
|
+
}
|
|
1217
|
+
if (args.json) {
|
|
1218
|
+
write(JSON.stringify(body, null, 2));
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
const schema = body.assessment.schemaVersion;
|
|
1222
|
+
if (schema !== SUPPORTED_ASSESSMENT_SCHEMA) {
|
|
1223
|
+
return profileFailure(false, `This CLI prints assessment schema ${SUPPORTED_ASSESSMENT_SCHEMA}, but the service returned ${JSON.stringify(safeText(String(schema)))}. Update the CLI, or use --json to receive the raw response.`);
|
|
1224
|
+
}
|
|
1225
|
+
for (const line of formatAssessment(body, built.skipped)) write(line);
|
|
1226
|
+
}
|
|
486
1227
|
async function main() {
|
|
487
1228
|
const args = parseArgs(process.argv);
|
|
488
1229
|
if (args.version) {
|
|
@@ -493,8 +1234,13 @@ async function main() {
|
|
|
493
1234
|
printHelp();
|
|
494
1235
|
process.exit(EXIT_OK);
|
|
495
1236
|
}
|
|
1237
|
+
if (args.command === "mcp") {
|
|
1238
|
+
await runMcpServer({ version: VERSION, apiKey: args.key, apiUrl: API_URL });
|
|
1239
|
+
return;
|
|
1240
|
+
}
|
|
496
1241
|
if (args.command === "scan") {
|
|
497
|
-
await
|
|
1242
|
+
if (args.profiles.length > 0) await runProfile(args);
|
|
1243
|
+
else await runScan(args);
|
|
498
1244
|
} else {
|
|
499
1245
|
console.error(`
|
|
500
1246
|
${c.red}Unknown command: ${args.command}${c.reset}`);
|