agentverify 1.6.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 +13 -0
- package/dist/cli.js +431 -36
- package/dist/cli.mjs +427 -32
- 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 +2 -0
- package/dist/sdk.d.ts +2 -0
- package/package.json +28 -8
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the `agentverify` CLI/SDK package are documented here.
|
|
4
4
|
|
|
5
|
+
## [2.0.0] - 2026-10-30
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `agentverify mcp`: run Agent Verify as an MCP server (stdio) for Claude Code, Cursor, Windsurf and VS Code.
|
|
10
|
+
Tools: `scan_path`, `scan_code`, `scan_mcp_config`. Reads `AGENTVERIFY_API_KEY`.
|
|
11
|
+
`claude mcp add agentverify -e AGENTVERIFY_API_KEY=av_your_key -- npx -y agentverify mcp`
|
|
12
|
+
- Folder scans from the MCP server are combined into one report (60 files / 5MB, agent files first, tests last) and
|
|
13
|
+
each finding is mapped back to its file and line.
|
|
14
|
+
- MCP config checks read only the server definitions and mask secret values locally before sending.
|
|
15
|
+
- `Finding.line` is now part of the SDK type.
|
|
16
|
+
- `agentverify/guard`: Agent Guard runtime protection (`createGuard`, `wrap`, `wrapAll`, `GuardBlockedError`).
|
|
17
|
+
|
|
5
18
|
## [1.6.0] - 2026-10-27
|
|
6
19
|
|
|
7
20
|
### Added
|
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,393 @@ async function scan(input, options) {
|
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
// src/
|
|
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
|
|
119
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");
|
|
120
505
|
var SUPPORTED_PROFILES = ["owasp-agentic-skills-2026"];
|
|
121
506
|
var SUPPORTED_ASSESSMENT_SCHEMA = "1.1.0";
|
|
122
507
|
var LOCAL_LIMITS = { maxFiles: 5e3, maxFileBytes: 8 * 1024 * 1024, maxTotalBytes: 32 * 1024 * 1024, maxDepth: 64 };
|
|
@@ -198,13 +583,13 @@ function decodeStrict(bytes) {
|
|
|
198
583
|
function listEntries(dir, shown) {
|
|
199
584
|
const entries = [];
|
|
200
585
|
if (process.platform === "win32") {
|
|
201
|
-
for (const d of (0,
|
|
586
|
+
for (const d of (0, import_fs3.readdirSync)(dir, { withFileTypes: true })) {
|
|
202
587
|
const problem = nameProblem(d.name);
|
|
203
588
|
if (problem) throw new PackageError(`An entry in ${displayPath(shown)} ${problem}, so it cannot be sent faithfully.`);
|
|
204
589
|
entries.push({ name: d.name, kind: d.isSymbolicLink() ? "symlink" : d.isDirectory() ? "dir" : d.isFile() ? "file" : "other" });
|
|
205
590
|
}
|
|
206
591
|
} else {
|
|
207
|
-
for (const d of (0,
|
|
592
|
+
for (const d of (0, import_fs3.readdirSync)(dir, { withFileTypes: true, encoding: "buffer" })) {
|
|
208
593
|
let name;
|
|
209
594
|
try {
|
|
210
595
|
name = decodeStrict(d.name);
|
|
@@ -221,23 +606,23 @@ var specialMessage = (rel) => `${displayPath(rel)} is not a regular file or dire
|
|
|
221
606
|
function readRegularFile(full, rel, budget) {
|
|
222
607
|
let fd;
|
|
223
608
|
try {
|
|
224
|
-
fd = (0,
|
|
609
|
+
fd = (0, import_fs3.openSync)(full, import_fs3.constants.O_RDONLY | (import_fs3.constants.O_NOFOLLOW ?? 0));
|
|
225
610
|
} catch (e) {
|
|
226
611
|
const code = e.code;
|
|
227
612
|
if (code === "ELOOP") throw new PackageError(symlinkMessage(rel));
|
|
228
613
|
throw new PackageError(`Could not read ${displayPath(rel)}: ${code ?? "unknown error"}.`);
|
|
229
614
|
}
|
|
230
615
|
try {
|
|
231
|
-
const st = (0,
|
|
616
|
+
const st = (0, import_fs3.fstatSync)(fd);
|
|
232
617
|
if (!st.isFile()) throw new PackageError(specialMessage(rel));
|
|
233
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.`);
|
|
234
|
-
const bytes = (0,
|
|
619
|
+
const bytes = (0, import_fs3.readFileSync)(fd);
|
|
235
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.`);
|
|
236
621
|
budget.total += bytes.length;
|
|
237
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.`);
|
|
238
623
|
return bytes;
|
|
239
624
|
} finally {
|
|
240
|
-
(0,
|
|
625
|
+
(0, import_fs3.closeSync)(fd);
|
|
241
626
|
}
|
|
242
627
|
}
|
|
243
628
|
var extensionOf = (name) => {
|
|
@@ -247,7 +632,7 @@ var extensionOf = (name) => {
|
|
|
247
632
|
function buildPackage(root) {
|
|
248
633
|
let rootStat;
|
|
249
634
|
try {
|
|
250
|
-
rootStat = (0,
|
|
635
|
+
rootStat = (0, import_fs3.lstatSync)(root);
|
|
251
636
|
} catch {
|
|
252
637
|
throw new PackageError(`${displayPath(root)} does not exist or cannot be read.`);
|
|
253
638
|
}
|
|
@@ -256,7 +641,7 @@ function buildPackage(root) {
|
|
|
256
641
|
const files = [];
|
|
257
642
|
const skipped = [];
|
|
258
643
|
const budget = { total: 0 };
|
|
259
|
-
const
|
|
644
|
+
const walk2 = (dir, prefix, depth) => {
|
|
260
645
|
if (depth > LOCAL_LIMITS.maxDepth) throw new PackageError(`The package is nested deeper than ${LOCAL_LIMITS.maxDepth} directories.`);
|
|
261
646
|
for (const entry of listEntries(dir, prefix || ".")) {
|
|
262
647
|
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
@@ -268,7 +653,7 @@ function buildPackage(root) {
|
|
|
268
653
|
skipped.push({ path: rel, reason: "vendored directory" });
|
|
269
654
|
continue;
|
|
270
655
|
}
|
|
271
|
-
|
|
656
|
+
walk2(full, rel, depth + 1);
|
|
272
657
|
continue;
|
|
273
658
|
}
|
|
274
659
|
if (BINARY_EXTENSIONS.has(extensionOf(entry.name))) {
|
|
@@ -286,7 +671,7 @@ function buildPackage(root) {
|
|
|
286
671
|
files.push({ path: rel, content });
|
|
287
672
|
}
|
|
288
673
|
};
|
|
289
|
-
|
|
674
|
+
walk2(root, "", 0);
|
|
290
675
|
if (files.length === 0) throw new PackageError(`${displayPath(root)} contains no files to assess.`);
|
|
291
676
|
return { files, skipped };
|
|
292
677
|
}
|
|
@@ -299,7 +684,7 @@ async function postProfile(apiUrl, apiKey, body, timeoutMs) {
|
|
|
299
684
|
try {
|
|
300
685
|
const response = await fetch(apiUrl, {
|
|
301
686
|
method: "POST",
|
|
302
|
-
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json", "User-Agent": "agentverify-cli/
|
|
687
|
+
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json", "User-Agent": "agentverify-cli/2.0.0" },
|
|
303
688
|
body,
|
|
304
689
|
signal: controller.signal
|
|
305
690
|
});
|
|
@@ -365,7 +750,7 @@ function formatAssessment(body, skipped) {
|
|
|
365
750
|
}
|
|
366
751
|
|
|
367
752
|
// src/cli.ts
|
|
368
|
-
var VERSION = "
|
|
753
|
+
var VERSION = "2.0.0";
|
|
369
754
|
var API_URL = process.env.AGENTVERIFY_API_URL ?? "https://agentverify-api.agentverify.workers.dev/v1/scan";
|
|
370
755
|
var A2SPA_DOCS_URL = "https://aimodularity.com/A2SPA/docs";
|
|
371
756
|
var EXIT_OK = 0;
|
|
@@ -387,18 +772,18 @@ var write = (value = "") => process.stdout.write(`${value}
|
|
|
387
772
|
`);
|
|
388
773
|
var SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx", ".py", ".json", ".yaml", ".yml", ".toml", ".md", ".mjs", ".cjs", ".sh"]);
|
|
389
774
|
var MAX_SCAN_FILE_BYTES = 5 * 1024 * 1024;
|
|
390
|
-
var
|
|
775
|
+
var SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", "vendor", "coverage", ".cache"]);
|
|
391
776
|
function collectFiles(dir) {
|
|
392
777
|
const files = [];
|
|
393
778
|
try {
|
|
394
|
-
const entries = (0,
|
|
779
|
+
const entries = (0, import_fs4.readdirSync)(dir);
|
|
395
780
|
for (const entry of entries) {
|
|
396
|
-
if (
|
|
397
|
-
const fullPath = (0,
|
|
398
|
-
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);
|
|
399
784
|
if (stat.isDirectory()) {
|
|
400
785
|
files.push(...collectFiles(fullPath));
|
|
401
|
-
} else if (SUPPORTED_EXTENSIONS.has((0,
|
|
786
|
+
} else if (SUPPORTED_EXTENSIONS.has((0, import_path3.extname)(entry).toLowerCase())) {
|
|
402
787
|
files.push(fullPath);
|
|
403
788
|
}
|
|
404
789
|
}
|
|
@@ -458,9 +843,12 @@ ${c.gray}Execution trust analysis for AI agents${c.reset}
|
|
|
458
843
|
${c.bold}Usage:${c.reset}
|
|
459
844
|
agentverify scan [dir] --key <api-key>
|
|
460
845
|
agentverify scan --file <file> --key <api-key>
|
|
846
|
+
agentverify mcp
|
|
461
847
|
|
|
462
848
|
${c.bold}Commands:${c.reset}
|
|
463
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)
|
|
464
852
|
|
|
465
853
|
${c.bold}Options:${c.reset}
|
|
466
854
|
--key, -k API key (or set AGENTVERIFY_API_KEY env var)
|
|
@@ -498,6 +886,9 @@ ${c.bold}Examples:${c.reset}
|
|
|
498
886
|
${c.gray}# Markdown summary for pull requests${c.reset}
|
|
499
887
|
agentverify scan . --key av_your_key --markdown
|
|
500
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
|
+
|
|
501
892
|
${c.gray}# Assess a skill directory against a profile (alpha); --json prints the service's response${c.reset}
|
|
502
893
|
agentverify scan ./my-skill --profile ${SUPPORTED_PROFILES[0]} --key av_your_key
|
|
503
894
|
|
|
@@ -578,8 +969,8 @@ ${c.bold}Threat categories:${c.reset}`);
|
|
|
578
969
|
write(`${c.gray}Re-scan:${c.reset} agentverify scan --file ${fileName} --key $AGENTVERIFY_API_KEY
|
|
579
970
|
`);
|
|
580
971
|
}
|
|
581
|
-
function markdownSummary(results,
|
|
582
|
-
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}`, ""];
|
|
583
974
|
for (const item of results) {
|
|
584
975
|
if (!item.result) {
|
|
585
976
|
lines.push(`## ${item.file}`, "", `Error: ${item.error ?? "Unknown error"}`, "");
|
|
@@ -599,20 +990,20 @@ function markdownSummary(results, summary) {
|
|
|
599
990
|
lines.push(`A2SPA docs: ${A2SPA_DOCS_URL}`);
|
|
600
991
|
return lines.join("\n");
|
|
601
992
|
}
|
|
602
|
-
function writeGithubJobSummary(results,
|
|
993
|
+
function writeGithubJobSummary(results, summary2) {
|
|
603
994
|
const summaryPath = process.env.GITHUB_STEP_SUMMARY;
|
|
604
995
|
if (!summaryPath) return;
|
|
605
996
|
try {
|
|
606
|
-
(0,
|
|
997
|
+
(0, import_fs4.appendFileSync)(summaryPath, `${markdownSummary(results, summary2)}
|
|
607
998
|
`);
|
|
608
999
|
} catch (e) {
|
|
609
1000
|
console.error(`Warning: could not write GitHub job summary: ${e instanceof Error ? e.message : e}`);
|
|
610
1001
|
}
|
|
611
1002
|
}
|
|
612
|
-
function writeSummaryFile(path, results,
|
|
1003
|
+
function writeSummaryFile(path, results, summary2) {
|
|
613
1004
|
if (!path) return;
|
|
614
1005
|
try {
|
|
615
|
-
(0,
|
|
1006
|
+
(0, import_fs4.writeFileSync)(path, JSON.stringify({ results, summary: summary2 }, null, 2));
|
|
616
1007
|
} catch (e) {
|
|
617
1008
|
console.error(`Warning: could not write summary file: ${e instanceof Error ? e.message : e}`);
|
|
618
1009
|
}
|
|
@@ -670,12 +1061,12 @@ ${c.bold}Agent Verify${c.reset} ${c.gray}v${VERSION}${c.reset}`);
|
|
|
670
1061
|
const batch = files.slice(i, i + CONCURRENCY);
|
|
671
1062
|
const batchResults = await Promise.allSettled(
|
|
672
1063
|
batch.map(async (filePath) => {
|
|
673
|
-
const fileStat = (0,
|
|
1064
|
+
const fileStat = (0, import_fs4.statSync)(filePath);
|
|
674
1065
|
if (fileStat.size > MAX_SCAN_FILE_BYTES) {
|
|
675
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`);
|
|
676
1067
|
}
|
|
677
|
-
const content = (0,
|
|
678
|
-
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);
|
|
679
1070
|
const result = await scan(
|
|
680
1071
|
{ content, fileName: relPath, policyId: args.policy || void 0 },
|
|
681
1072
|
{ apiKey: args.key, apiUrl: API_URL }
|
|
@@ -697,7 +1088,7 @@ ${c.bold}Agent Verify${c.reset} ${c.gray}v${VERSION}${c.reset}`);
|
|
|
697
1088
|
else notVerified++;
|
|
698
1089
|
} else {
|
|
699
1090
|
const filePath = batch[j];
|
|
700
|
-
const relPath = (0,
|
|
1091
|
+
const relPath = (0, import_path3.relative)(args.dir, filePath);
|
|
701
1092
|
const errMsg = outcome.reason instanceof Error ? outcome.reason.message : String(outcome.reason);
|
|
702
1093
|
if (!args.json) {
|
|
703
1094
|
write(` ${c.yellow}\u26A0${c.reset} ${c.yellow}WARNING${c.reset} ${relPath.padEnd(30)} ${c.gray}${errMsg}${c.reset}`);
|
|
@@ -707,11 +1098,11 @@ ${c.bold}Agent Verify${c.reset} ${c.gray}v${VERSION}${c.reset}`);
|
|
|
707
1098
|
}
|
|
708
1099
|
}
|
|
709
1100
|
}
|
|
710
|
-
const
|
|
711
|
-
writeGithubJobSummary(jsonResults,
|
|
712
|
-
writeSummaryFile(args.summaryFile, jsonResults,
|
|
1101
|
+
const summary2 = { total: files.length, verified, notVerified, notAssessed, errors };
|
|
1102
|
+
writeGithubJobSummary(jsonResults, summary2);
|
|
1103
|
+
writeSummaryFile(args.summaryFile, jsonResults, summary2);
|
|
713
1104
|
if (args.json) {
|
|
714
|
-
write(JSON.stringify({ results: jsonResults, summary }, null, 2));
|
|
1105
|
+
write(JSON.stringify({ results: jsonResults, summary: summary2 }, null, 2));
|
|
715
1106
|
if (errors > 0) process.exit(EXIT_EXECUTION_ERROR);
|
|
716
1107
|
if (args.ci) {
|
|
717
1108
|
if (notVerified > 0) process.exit(EXIT_NOT_VERIFIED);
|
|
@@ -720,7 +1111,7 @@ ${c.bold}Agent Verify${c.reset} ${c.gray}v${VERSION}${c.reset}`);
|
|
|
720
1111
|
return;
|
|
721
1112
|
}
|
|
722
1113
|
if (args.markdown) {
|
|
723
|
-
write(markdownSummary(jsonResults,
|
|
1114
|
+
write(markdownSummary(jsonResults, summary2));
|
|
724
1115
|
if (errors > 0) process.exit(EXIT_EXECUTION_ERROR);
|
|
725
1116
|
if (args.ci) {
|
|
726
1117
|
if (notVerified > 0) process.exit(EXIT_NOT_VERIFIED);
|
|
@@ -843,6 +1234,10 @@ async function main() {
|
|
|
843
1234
|
printHelp();
|
|
844
1235
|
process.exit(EXIT_OK);
|
|
845
1236
|
}
|
|
1237
|
+
if (args.command === "mcp") {
|
|
1238
|
+
await runMcpServer({ version: VERSION, apiKey: args.key, apiUrl: API_URL });
|
|
1239
|
+
return;
|
|
1240
|
+
}
|
|
846
1241
|
if (args.command === "scan") {
|
|
847
1242
|
if (args.profiles.length > 0) await runProfile(args);
|
|
848
1243
|
else await runScan(args);
|