agentverify 1.4.0 → 1.6.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 +21 -1
- package/README.md +1 -1
- package/dist/cli.js +363 -12
- package/dist/cli.mjs +361 -8
- package/dist/sdk.d.mts +2 -0
- package/dist/sdk.d.ts +2 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the `agentverify` CLI/SDK package are documented here.
|
|
4
4
|
|
|
5
|
-
## [1.
|
|
5
|
+
## [1.6.0] - 2026-10-27
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Scans now run Agent Verify's v1.6 check set (76 checks, up from 44), including framework packs
|
|
10
|
+
and MCP configuration checks — no CLI change needed;
|
|
11
|
+
results come from the API. See the root CHANGELOG for the full list.
|
|
12
|
+
- Directory scans now also pick up `.jsx`, `.tsx`, `.toml`, and `.sh` files.
|
|
13
|
+
|
|
14
|
+
## [1.5.0] - 2026-09-22
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- `--profile <id>` — assess a directory against an assessment profile instead of running the
|
|
19
|
+
standard scan/verify flow. `owasp-agentic-skills-2026` is the first supported profile
|
|
20
|
+
(assessment schema `1.1.0`, currently alpha): `agentverify scan <dir> --profile owasp-agentic-skills-2026 --key av_your_key`.
|
|
21
|
+
Prints AST02/AST03/AST04 evidence-based results (`EVIDENCE_OBSERVED` / `GAP_IDENTIFIED`) plus
|
|
22
|
+
every other control explicitly as `NOT_ASSESSED`; `--json` prints the service's response
|
|
23
|
+
unmodified.
|
|
24
|
+
|
|
25
|
+
## [1.4.0] - Shipped
|
|
6
26
|
|
|
7
27
|
### Changed — CI exit codes now distinguish a security failure from an execution error (behavior change)
|
|
8
28
|
|
package/README.md
CHANGED
|
@@ -116,7 +116,7 @@ The workflow requires a valid `AGENTVERIFY_API_KEY`. The local Worker code path
|
|
|
116
116
|
|
|
117
117
|
Saved CLI reports sync to the dashboard through the API `cliReports` save path. If the API cannot save the report, the CLI still prints local scan output and tells you dashboard sync did not complete.
|
|
118
118
|
|
|
119
|
-
Verified reports can expose a README badge from the report page. A packaged Agent Verify GitHub Action (`AI-Blockchain-Ventures/agentverify@v1`) already exists and produces text/Markdown/JSON output plus an optional workflow artifact — see below. Automatically posting that output as a PR comment (rather than just producing it for you to post) is still a planned enhancement, not yet built.
|
|
119
|
+
Verified reports can expose a README badge from the report page. A packaged Agent Verify GitHub Action (`AI-Blockchain-Ventures/agentverify@v1.4.0`) already exists and produces text/Markdown/JSON output plus an optional workflow artifact — see below. Automatically posting that output as a PR comment (rather than just producing it for you to post) is still a planned enhancement, not yet built.
|
|
120
120
|
|
|
121
121
|
See the full GitHub Action guide at `docs/github-action.md` for composite action inputs, JSON/Markdown examples, branch protection, optional PR comments, and badge guidance.
|
|
122
122
|
|
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
// src/cli.ts
|
|
5
|
-
var
|
|
5
|
+
var import_fs2 = require("fs");
|
|
6
6
|
var import_path = require("path");
|
|
7
7
|
|
|
8
8
|
// src/sdk.ts
|
|
@@ -115,8 +115,257 @@ async function scan(input, options) {
|
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
// src/profile.ts
|
|
119
|
+
var import_fs = require("fs");
|
|
120
|
+
var SUPPORTED_PROFILES = ["owasp-agentic-skills-2026"];
|
|
121
|
+
var SUPPORTED_ASSESSMENT_SCHEMA = "1.1.0";
|
|
122
|
+
var LOCAL_LIMITS = { maxFiles: 5e3, maxFileBytes: 8 * 1024 * 1024, maxTotalBytes: 32 * 1024 * 1024, maxDepth: 64 };
|
|
123
|
+
var VENDORED_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "__pycache__"]);
|
|
124
|
+
var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
125
|
+
".png",
|
|
126
|
+
".jpg",
|
|
127
|
+
".jpeg",
|
|
128
|
+
".gif",
|
|
129
|
+
".webp",
|
|
130
|
+
".ico",
|
|
131
|
+
".bmp",
|
|
132
|
+
".tif",
|
|
133
|
+
".tiff",
|
|
134
|
+
".avif",
|
|
135
|
+
".pdf",
|
|
136
|
+
".woff",
|
|
137
|
+
".woff2",
|
|
138
|
+
".ttf",
|
|
139
|
+
".otf",
|
|
140
|
+
".eot",
|
|
141
|
+
".zip",
|
|
142
|
+
".gz",
|
|
143
|
+
".tgz",
|
|
144
|
+
".tar",
|
|
145
|
+
".bz2",
|
|
146
|
+
".xz",
|
|
147
|
+
".7z",
|
|
148
|
+
".mp3",
|
|
149
|
+
".mp4",
|
|
150
|
+
".mov",
|
|
151
|
+
".wav",
|
|
152
|
+
".ogg",
|
|
153
|
+
".webm",
|
|
154
|
+
".wasm",
|
|
155
|
+
".so",
|
|
156
|
+
".dll",
|
|
157
|
+
".dylib",
|
|
158
|
+
".exe",
|
|
159
|
+
".class",
|
|
160
|
+
".jar",
|
|
161
|
+
".pyc",
|
|
162
|
+
".sqlite",
|
|
163
|
+
".db"
|
|
164
|
+
]);
|
|
165
|
+
var PackageError = class extends Error {
|
|
166
|
+
};
|
|
167
|
+
function displayPath(path) {
|
|
168
|
+
return JSON.stringify(safeText(path));
|
|
169
|
+
}
|
|
170
|
+
function safeText(text) {
|
|
171
|
+
let out = "";
|
|
172
|
+
for (const ch of text) {
|
|
173
|
+
const cp = ch.codePointAt(0);
|
|
174
|
+
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;
|
|
175
|
+
out += hidden ? `\\u{${cp.toString(16).toUpperCase().padStart(4, "0")}}` : ch;
|
|
176
|
+
}
|
|
177
|
+
return out;
|
|
178
|
+
}
|
|
179
|
+
function nameProblem(name) {
|
|
180
|
+
for (let i = 0; i < name.length; i++) {
|
|
181
|
+
const c2 = name.charCodeAt(i);
|
|
182
|
+
if (c2 >= 55296 && c2 <= 56319) {
|
|
183
|
+
const next = name.charCodeAt(i + 1);
|
|
184
|
+
if (next >= 56320 && next <= 57343) {
|
|
185
|
+
i++;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
return "contains an unpaired UTF-16 surrogate";
|
|
189
|
+
}
|
|
190
|
+
if (c2 >= 56320 && c2 <= 57343) return "contains an unpaired UTF-16 surrogate";
|
|
191
|
+
}
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
var strictUtf8 = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
|
|
195
|
+
function decodeStrict(bytes) {
|
|
196
|
+
return strictUtf8.decode(bytes);
|
|
197
|
+
}
|
|
198
|
+
function listEntries(dir, shown) {
|
|
199
|
+
const entries = [];
|
|
200
|
+
if (process.platform === "win32") {
|
|
201
|
+
for (const d of (0, import_fs.readdirSync)(dir, { withFileTypes: true })) {
|
|
202
|
+
const problem = nameProblem(d.name);
|
|
203
|
+
if (problem) throw new PackageError(`An entry in ${displayPath(shown)} ${problem}, so it cannot be sent faithfully.`);
|
|
204
|
+
entries.push({ name: d.name, kind: d.isSymbolicLink() ? "symlink" : d.isDirectory() ? "dir" : d.isFile() ? "file" : "other" });
|
|
205
|
+
}
|
|
206
|
+
} else {
|
|
207
|
+
for (const d of (0, import_fs.readdirSync)(dir, { withFileTypes: true, encoding: "buffer" })) {
|
|
208
|
+
let name;
|
|
209
|
+
try {
|
|
210
|
+
name = decodeStrict(d.name);
|
|
211
|
+
} catch {
|
|
212
|
+
throw new PackageError(`An entry in ${displayPath(shown)} has a name that is not valid UTF-8, so it cannot be sent faithfully.`);
|
|
213
|
+
}
|
|
214
|
+
entries.push({ name, kind: d.isSymbolicLink() ? "symlink" : d.isDirectory() ? "dir" : d.isFile() ? "file" : "other" });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
218
|
+
}
|
|
219
|
+
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.`;
|
|
220
|
+
var specialMessage = (rel) => `${displayPath(rel)} is not a regular file or directory (it is a device, socket or pipe), so it cannot be sent.`;
|
|
221
|
+
function readRegularFile(full, rel, budget) {
|
|
222
|
+
let fd;
|
|
223
|
+
try {
|
|
224
|
+
fd = (0, import_fs.openSync)(full, import_fs.constants.O_RDONLY | (import_fs.constants.O_NOFOLLOW ?? 0));
|
|
225
|
+
} catch (e) {
|
|
226
|
+
const code = e.code;
|
|
227
|
+
if (code === "ELOOP") throw new PackageError(symlinkMessage(rel));
|
|
228
|
+
throw new PackageError(`Could not read ${displayPath(rel)}: ${code ?? "unknown error"}.`);
|
|
229
|
+
}
|
|
230
|
+
try {
|
|
231
|
+
const st = (0, import_fs.fstatSync)(fd);
|
|
232
|
+
if (!st.isFile()) throw new PackageError(specialMessage(rel));
|
|
233
|
+
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, import_fs.readFileSync)(fd);
|
|
235
|
+
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
|
+
budget.total += bytes.length;
|
|
237
|
+
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
|
+
return bytes;
|
|
239
|
+
} finally {
|
|
240
|
+
(0, import_fs.closeSync)(fd);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
var extensionOf = (name) => {
|
|
244
|
+
const i = name.lastIndexOf(".");
|
|
245
|
+
return i <= 0 ? "" : name.slice(i).toLowerCase();
|
|
246
|
+
};
|
|
247
|
+
function buildPackage(root) {
|
|
248
|
+
let rootStat;
|
|
249
|
+
try {
|
|
250
|
+
rootStat = (0, import_fs.lstatSync)(root);
|
|
251
|
+
} catch {
|
|
252
|
+
throw new PackageError(`${displayPath(root)} does not exist or cannot be read.`);
|
|
253
|
+
}
|
|
254
|
+
if (rootStat.isSymbolicLink()) throw new PackageError(symlinkMessage(root));
|
|
255
|
+
if (!rootStat.isDirectory()) throw new PackageError(`${displayPath(root)} is not a directory. A profile assessment takes a directory.`);
|
|
256
|
+
const files = [];
|
|
257
|
+
const skipped = [];
|
|
258
|
+
const budget = { total: 0 };
|
|
259
|
+
const walk = (dir, prefix, depth) => {
|
|
260
|
+
if (depth > LOCAL_LIMITS.maxDepth) throw new PackageError(`The package is nested deeper than ${LOCAL_LIMITS.maxDepth} directories.`);
|
|
261
|
+
for (const entry of listEntries(dir, prefix || ".")) {
|
|
262
|
+
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
263
|
+
const full = `${dir}/${entry.name}`;
|
|
264
|
+
if (entry.kind === "symlink") throw new PackageError(symlinkMessage(rel));
|
|
265
|
+
if (entry.kind === "other") throw new PackageError(specialMessage(rel));
|
|
266
|
+
if (entry.kind === "dir") {
|
|
267
|
+
if (VENDORED_DIRS.has(entry.name)) {
|
|
268
|
+
skipped.push({ path: rel, reason: "vendored directory" });
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
walk(full, rel, depth + 1);
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
if (BINARY_EXTENSIONS.has(extensionOf(entry.name))) {
|
|
275
|
+
skipped.push({ path: rel, reason: "binary asset" });
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
if (files.length >= LOCAL_LIMITS.maxFiles) throw new PackageError(`The package has more than ${LOCAL_LIMITS.maxFiles} files, over the CLI's local guard.`);
|
|
279
|
+
const bytes = readRegularFile(full, rel, budget);
|
|
280
|
+
let content;
|
|
281
|
+
try {
|
|
282
|
+
content = decodeStrict(bytes);
|
|
283
|
+
} catch {
|
|
284
|
+
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.`);
|
|
285
|
+
}
|
|
286
|
+
files.push({ path: rel, content });
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
walk(root, "", 0);
|
|
290
|
+
if (files.length === 0) throw new PackageError(`${displayPath(root)} contains no files to assess.`);
|
|
291
|
+
return { files, skipped };
|
|
292
|
+
}
|
|
293
|
+
function serializeRequest(profile, files) {
|
|
294
|
+
return JSON.stringify({ profile, files });
|
|
295
|
+
}
|
|
296
|
+
async function postProfile(apiUrl, apiKey, body, timeoutMs) {
|
|
297
|
+
const controller = new AbortController();
|
|
298
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
299
|
+
try {
|
|
300
|
+
const response = await fetch(apiUrl, {
|
|
301
|
+
method: "POST",
|
|
302
|
+
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json", "User-Agent": "agentverify-cli/1.6.0" },
|
|
303
|
+
body,
|
|
304
|
+
signal: controller.signal
|
|
305
|
+
});
|
|
306
|
+
const text = await response.text();
|
|
307
|
+
let json = null;
|
|
308
|
+
try {
|
|
309
|
+
json = JSON.parse(text);
|
|
310
|
+
} catch {
|
|
311
|
+
json = null;
|
|
312
|
+
}
|
|
313
|
+
return { kind: "response", status: response.status, text, json };
|
|
314
|
+
} catch (e) {
|
|
315
|
+
if (e instanceof Error && e.name === "AbortError") return { kind: "failure", message: `Request timed out after ${timeoutMs}ms` };
|
|
316
|
+
return { kind: "failure", message: `Request failed: ${e instanceof Error ? e.message : String(e)}` };
|
|
317
|
+
} finally {
|
|
318
|
+
clearTimeout(timer);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
322
|
+
var str = (v) => typeof v === "string" ? safeText(v) : typeof v === "number" || typeof v === "boolean" ? String(v) : "";
|
|
323
|
+
function formatAssessment(body, skipped) {
|
|
324
|
+
const a = isObj(body.assessment) ? body.assessment : {};
|
|
325
|
+
const profile = isObj(a.profile) ? a.profile : {};
|
|
326
|
+
const pkg = isObj(a.package) ? a.package : {};
|
|
327
|
+
const lines = [];
|
|
328
|
+
lines.push("Agent Verify profile assessment");
|
|
329
|
+
lines.push(`Profile: ${str(body.profile)}`);
|
|
330
|
+
lines.push(`Framework: ${str(profile.framework)} (upstream ${str(profile.upstreamRepo)} @ ${str(profile.upstreamCommit).slice(0, 12)}, ${str(profile.upstreamStatus)}; profile version ${str(profile.agentverifyProfileVersion)})`);
|
|
331
|
+
lines.push(`Assessment schema: ${str(a.schemaVersion)}`);
|
|
332
|
+
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)}`);
|
|
333
|
+
lines.push(`Package: ${str(pkg.fileCount)} files, digest ${str(pkg.digest)}`);
|
|
334
|
+
lines.push(`Attestation: ${body.attestation === null ? "none (this profile assessment is unsigned)" : "present (not verified by this CLI)"}`);
|
|
335
|
+
lines.push("");
|
|
336
|
+
for (const control of Array.isArray(a.controls) ? a.controls : []) {
|
|
337
|
+
if (!isObj(control)) continue;
|
|
338
|
+
const cov = isObj(control.coverage) ? control.coverage : {};
|
|
339
|
+
lines.push(`${str(control.controlId)} ${str(control.title)}: ${str(control.status)} (confidence ${str(control.confidence)})`);
|
|
340
|
+
lines.push(` ${str(cov.evidenceObserved)} evidence observed, ${str(cov.gapIdentified)} gaps identified, ${str(cov.notAssessed)} not assessed, of ${str(cov.total)} checks`);
|
|
341
|
+
for (const check of Array.isArray(control.checks) ? control.checks : []) {
|
|
342
|
+
if (isObj(check)) lines.push(` ${str(check.checkId)} ${str(check.status)}: ${str(check.title)}`);
|
|
343
|
+
}
|
|
344
|
+
lines.push("");
|
|
345
|
+
}
|
|
346
|
+
const gaps = (Array.isArray(a.evidence) ? a.evidence : []).filter((e) => isObj(e) && e.polarity === "gap");
|
|
347
|
+
lines.push(`Gaps identified (${gaps.length}):`);
|
|
348
|
+
for (const g of gaps) {
|
|
349
|
+
lines.push(` [${str(g.severity)}] ${str(g.kind)}: ${str(g.summary)}`);
|
|
350
|
+
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)}` : ""}`);
|
|
351
|
+
if (locations.length) lines.push(` at ${locations.join(", ")}`);
|
|
352
|
+
}
|
|
353
|
+
if (gaps.length === 0) lines.push(" none");
|
|
354
|
+
const notImplemented = Array.isArray(a.notImplementedControls) ? a.notImplementedControls.map(str).join(", ") : "";
|
|
355
|
+
lines.push("");
|
|
356
|
+
lines.push(`Not assessed by this profile version: ${notImplemented || "none"}`);
|
|
357
|
+
for (const note of Array.isArray(a.notes) ? a.notes : []) lines.push(`Note: ${str(note)}`);
|
|
358
|
+
if (skipped.length > 0) {
|
|
359
|
+
lines.push("");
|
|
360
|
+
lines.push(`Skipped from the package (${skipped.length}), not sent:`);
|
|
361
|
+
for (const s of skipped.slice(0, 20)) lines.push(` ${displayPath(s.path)}: ${s.reason}`);
|
|
362
|
+
if (skipped.length > 20) lines.push(` and ${skipped.length - 20} more`);
|
|
363
|
+
}
|
|
364
|
+
return lines;
|
|
365
|
+
}
|
|
366
|
+
|
|
118
367
|
// src/cli.ts
|
|
119
|
-
var VERSION = "1.
|
|
368
|
+
var VERSION = "1.6.0";
|
|
120
369
|
var API_URL = process.env.AGENTVERIFY_API_URL ?? "https://agentverify-api.agentverify.workers.dev/v1/scan";
|
|
121
370
|
var A2SPA_DOCS_URL = "https://aimodularity.com/A2SPA/docs";
|
|
122
371
|
var EXIT_OK = 0;
|
|
@@ -136,17 +385,17 @@ var c = {
|
|
|
136
385
|
};
|
|
137
386
|
var write = (value = "") => process.stdout.write(`${value}
|
|
138
387
|
`);
|
|
139
|
-
var SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".ts", ".py", ".json", ".yaml", ".yml", ".md", ".mjs", ".cjs"]);
|
|
388
|
+
var SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx", ".py", ".json", ".yaml", ".yml", ".toml", ".md", ".mjs", ".cjs", ".sh"]);
|
|
140
389
|
var MAX_SCAN_FILE_BYTES = 5 * 1024 * 1024;
|
|
141
390
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", "vendor", "coverage", ".cache"]);
|
|
142
391
|
function collectFiles(dir) {
|
|
143
392
|
const files = [];
|
|
144
393
|
try {
|
|
145
|
-
const entries = (0,
|
|
394
|
+
const entries = (0, import_fs2.readdirSync)(dir);
|
|
146
395
|
for (const entry of entries) {
|
|
147
396
|
if (SKIP_DIRS.has(entry)) continue;
|
|
148
397
|
const fullPath = (0, import_path.join)(dir, entry);
|
|
149
|
-
const stat = (0,
|
|
398
|
+
const stat = (0, import_fs2.statSync)(fullPath);
|
|
150
399
|
if (stat.isDirectory()) {
|
|
151
400
|
files.push(...collectFiles(fullPath));
|
|
152
401
|
} else if (SUPPORTED_EXTENSIONS.has((0, import_path.extname)(entry).toLowerCase())) {
|
|
@@ -171,7 +420,8 @@ function parseArgs(argv) {
|
|
|
171
420
|
help: false,
|
|
172
421
|
version: false,
|
|
173
422
|
policy: "",
|
|
174
|
-
summaryFile: ""
|
|
423
|
+
summaryFile: "",
|
|
424
|
+
profiles: []
|
|
175
425
|
};
|
|
176
426
|
if (result.command === "--help" || result.command === "-h") {
|
|
177
427
|
result.command = "";
|
|
@@ -191,6 +441,8 @@ function parseArgs(argv) {
|
|
|
191
441
|
else if (arg === "--allow-not-assessed") result.allowNotAssessed = true;
|
|
192
442
|
else if (arg === "--policy") result.policy = args[++i] ?? "";
|
|
193
443
|
else if (arg === "--summary-file") result.summaryFile = args[++i] ?? "";
|
|
444
|
+
else if (arg === "--profile") result.profiles.push(args[++i] ?? "");
|
|
445
|
+
else if (arg.startsWith("--profile=")) result.profiles.push(arg.slice("--profile=".length));
|
|
194
446
|
else if (arg === "--help" || arg === "-h") result.help = true;
|
|
195
447
|
else if (arg === "--version" || arg === "-v") result.version = true;
|
|
196
448
|
else if (!arg.startsWith("-")) result.dir = arg;
|
|
@@ -222,6 +474,11 @@ ${c.bold}Options:${c.reset}
|
|
|
222
474
|
in addition to the scanner's own verdict (e.g. financial-agent)
|
|
223
475
|
--summary-file <path>
|
|
224
476
|
Write results as JSON to this path (in addition to any other output)
|
|
477
|
+
--profile <id>
|
|
478
|
+
(alpha) Assess a directory against an assessment profile instead of
|
|
479
|
+
scanning files individually. Supported: ${SUPPORTED_PROFILES.join(", ")}
|
|
480
|
+
Takes a directory; not combinable with --file, --ci, --policy,
|
|
481
|
+
--allow-not-assessed, --markdown or --summary-file.
|
|
225
482
|
--version, -v Print version
|
|
226
483
|
--help, -h Show this help message
|
|
227
484
|
|
|
@@ -241,12 +498,20 @@ ${c.bold}Examples:${c.reset}
|
|
|
241
498
|
${c.gray}# Markdown summary for pull requests${c.reset}
|
|
242
499
|
agentverify scan . --key av_your_key --markdown
|
|
243
500
|
|
|
501
|
+
${c.gray}# Assess a skill directory against a profile (alpha); --json prints the service's response${c.reset}
|
|
502
|
+
agentverify scan ./my-skill --profile ${SUPPORTED_PROFILES[0]} --key av_your_key
|
|
503
|
+
|
|
244
504
|
${c.bold}Exit codes (--ci):${c.reset}
|
|
245
505
|
0 All scanned files verified
|
|
246
506
|
1 NOT VERIFIED \u2014 a security/verification check failed
|
|
247
507
|
2 NOT ASSESSED \u2014 insufficient evidence for a verdict (see --allow-not-assessed)
|
|
248
508
|
3 Execution error \u2014 the scan itself could not run (bad key, network, missing file)
|
|
249
509
|
|
|
510
|
+
${c.bold}Exit codes (--profile):${c.reset}
|
|
511
|
+
0 The assessment completed. Control and check statuses (including GAP_IDENTIFIED)
|
|
512
|
+
do not affect the exit code.
|
|
513
|
+
3 Usage or execution error \u2014 nothing was assessed
|
|
514
|
+
|
|
250
515
|
${c.bold}Get your API key:${c.reset}
|
|
251
516
|
https://aimodularity.com/agentverify/dashboard
|
|
252
517
|
|
|
@@ -300,6 +565,8 @@ ${c.bold}Fix first:${c.reset}`);
|
|
|
300
565
|
else write(` No blocking fixes detected. Re-scan after any material change.`);
|
|
301
566
|
if (result.reportInsights?.nextAction) write(`
|
|
302
567
|
${c.bold}Next action:${c.reset} ${result.reportInsights.nextAction}`);
|
|
568
|
+
const lockedFixes = (result.findings ?? []).filter((f) => f.fixLocked).length;
|
|
569
|
+
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
570
|
const threats = getRelevantThreatCategories(result);
|
|
304
571
|
if (threats.length) {
|
|
305
572
|
write(`
|
|
@@ -336,7 +603,7 @@ function writeGithubJobSummary(results, summary) {
|
|
|
336
603
|
const summaryPath = process.env.GITHUB_STEP_SUMMARY;
|
|
337
604
|
if (!summaryPath) return;
|
|
338
605
|
try {
|
|
339
|
-
(0,
|
|
606
|
+
(0, import_fs2.appendFileSync)(summaryPath, `${markdownSummary(results, summary)}
|
|
340
607
|
`);
|
|
341
608
|
} catch (e) {
|
|
342
609
|
console.error(`Warning: could not write GitHub job summary: ${e instanceof Error ? e.message : e}`);
|
|
@@ -345,7 +612,7 @@ function writeGithubJobSummary(results, summary) {
|
|
|
345
612
|
function writeSummaryFile(path, results, summary) {
|
|
346
613
|
if (!path) return;
|
|
347
614
|
try {
|
|
348
|
-
(0,
|
|
615
|
+
(0, import_fs2.writeFileSync)(path, JSON.stringify({ results, summary }, null, 2));
|
|
349
616
|
} catch (e) {
|
|
350
617
|
console.error(`Warning: could not write summary file: ${e instanceof Error ? e.message : e}`);
|
|
351
618
|
}
|
|
@@ -388,7 +655,7 @@ ${c.bold}Agent Verify${c.reset} ${c.gray}v${VERSION}${c.reset}`);
|
|
|
388
655
|
files = collectFiles(dir);
|
|
389
656
|
if (files.length === 0) {
|
|
390
657
|
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}
|
|
658
|
+
write(`${c.gray}Supported: .js .jsx .ts .tsx .py .json .yaml .yml .toml .md .sh${c.reset}
|
|
392
659
|
`);
|
|
393
660
|
process.exit(0);
|
|
394
661
|
}
|
|
@@ -403,11 +670,11 @@ ${c.bold}Agent Verify${c.reset} ${c.gray}v${VERSION}${c.reset}`);
|
|
|
403
670
|
const batch = files.slice(i, i + CONCURRENCY);
|
|
404
671
|
const batchResults = await Promise.allSettled(
|
|
405
672
|
batch.map(async (filePath) => {
|
|
406
|
-
const fileStat = (0,
|
|
673
|
+
const fileStat = (0, import_fs2.statSync)(filePath);
|
|
407
674
|
if (fileStat.size > MAX_SCAN_FILE_BYTES) {
|
|
408
675
|
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
676
|
}
|
|
410
|
-
const content = (0,
|
|
677
|
+
const content = (0, import_fs2.readFileSync)(filePath, "utf-8");
|
|
411
678
|
const relPath = (0, import_path.relative)(args.dir === "." ? process.cwd() : args.dir, filePath);
|
|
412
679
|
const result = await scan(
|
|
413
680
|
{ content, fileName: relPath, policyId: args.policy || void 0 },
|
|
@@ -483,6 +750,89 @@ ${c.gray}No dashboard report was saved. Check API key access and retry if you ne
|
|
|
483
750
|
if (notAssessed > 0 && !args.allowNotAssessed) process.exit(EXIT_NOT_ASSESSED);
|
|
484
751
|
}
|
|
485
752
|
}
|
|
753
|
+
var PROFILE_TIMEOUT_MS = 6e4;
|
|
754
|
+
function profileUsageError(message) {
|
|
755
|
+
console.error(`
|
|
756
|
+
${c.red}Error:${c.reset} ${message}`);
|
|
757
|
+
console.error(`Run ${c.cyan}agentverify --help${c.reset} for usage
|
|
758
|
+
`);
|
|
759
|
+
process.exitCode = EXIT_EXECUTION_ERROR;
|
|
760
|
+
}
|
|
761
|
+
function profileFailure(json, message, detail = {}) {
|
|
762
|
+
if (json) write(JSON.stringify({ error: message, ...detail }, null, 2));
|
|
763
|
+
else console.error(`
|
|
764
|
+
${c.red}Error:${c.reset} ${safeText(message)}
|
|
765
|
+
`);
|
|
766
|
+
process.exitCode = EXIT_EXECUTION_ERROR;
|
|
767
|
+
}
|
|
768
|
+
async function runProfile(args) {
|
|
769
|
+
if (args.profiles.length > 1) return profileUsageError("--profile was given more than once.");
|
|
770
|
+
const profile = args.profiles[0];
|
|
771
|
+
if (profile === "") return profileUsageError(`--profile requires a value. Supported profiles: ${SUPPORTED_PROFILES.join(", ")}`);
|
|
772
|
+
if (!SUPPORTED_PROFILES.includes(profile)) {
|
|
773
|
+
return profileUsageError(`Unknown profile ${JSON.stringify(safeText(profile))}. Supported profiles: ${SUPPORTED_PROFILES.join(", ")}`);
|
|
774
|
+
}
|
|
775
|
+
const conflicts = [];
|
|
776
|
+
if (args.file) conflicts.push("--file");
|
|
777
|
+
if (args.ci) conflicts.push("--ci");
|
|
778
|
+
if (args.allowNotAssessed) conflicts.push("--allow-not-assessed");
|
|
779
|
+
if (args.policy) conflicts.push("--policy");
|
|
780
|
+
if (args.markdown) conflicts.push("--markdown");
|
|
781
|
+
if (args.summaryFile) conflicts.push("--summary-file");
|
|
782
|
+
if (conflicts.length > 0) return profileUsageError(`--profile cannot be combined with ${conflicts.join(", ")}. A profile assessment takes a directory and reports its own result.`);
|
|
783
|
+
if (!args.key) {
|
|
784
|
+
console.error(`
|
|
785
|
+
${c.red}Error:${c.reset} API key required
|
|
786
|
+
`);
|
|
787
|
+
console.error(` Set with --key av_your_key or AGENTVERIFY_API_KEY env var.`);
|
|
788
|
+
console.error(` Get your key at https://aimodularity.com/agentverify/dashboard/
|
|
789
|
+
`);
|
|
790
|
+
process.exitCode = EXIT_EXECUTION_ERROR;
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
let built;
|
|
794
|
+
try {
|
|
795
|
+
built = buildPackage(args.dir);
|
|
796
|
+
} catch (e) {
|
|
797
|
+
return profileFailure(args.json, e instanceof PackageError ? e.message : `Could not read the package: ${e instanceof Error ? e.message : String(e)}`);
|
|
798
|
+
}
|
|
799
|
+
if (args.json && built.skipped.length > 0) {
|
|
800
|
+
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` : ""}`);
|
|
801
|
+
}
|
|
802
|
+
const result = await postProfile(API_URL, args.key, serializeRequest(profile, built.files), PROFILE_TIMEOUT_MS);
|
|
803
|
+
if (result.kind === "failure") return profileFailure(args.json, result.message);
|
|
804
|
+
const body = result.json;
|
|
805
|
+
const isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
806
|
+
if (result.status !== 200) {
|
|
807
|
+
if (isObject(body) && typeof body.error === "string") {
|
|
808
|
+
if (args.json) {
|
|
809
|
+
write(JSON.stringify(body, null, 2));
|
|
810
|
+
} else {
|
|
811
|
+
console.error(`
|
|
812
|
+
${c.red}Error:${c.reset} ${safeText(body.error)} (HTTP ${result.status})`);
|
|
813
|
+
const rejection = body.rejection;
|
|
814
|
+
if (isObject(rejection)) console.error(` rejection: ${safeText(String(rejection.code ?? ""))}: ${safeText(String(rejection.message ?? ""))}${typeof rejection.path === "string" ? ` (${JSON.stringify(safeText(rejection.path))})` : ""}`);
|
|
815
|
+
if (Array.isArray(body.supportedProfiles)) console.error(` supported profiles: ${body.supportedProfiles.map((p) => safeText(String(p))).join(", ")}`);
|
|
816
|
+
console.error("");
|
|
817
|
+
}
|
|
818
|
+
process.exitCode = EXIT_EXECUTION_ERROR;
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
return profileFailure(args.json, `Unexpected response from the service (HTTP ${result.status})`, { status: result.status });
|
|
822
|
+
}
|
|
823
|
+
if (!isObject(body) || typeof body.profile !== "string" || !isObject(body.assessment)) {
|
|
824
|
+
return profileFailure(args.json, "The service returned a response that is not a profile assessment", { status: result.status });
|
|
825
|
+
}
|
|
826
|
+
if (args.json) {
|
|
827
|
+
write(JSON.stringify(body, null, 2));
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
const schema = body.assessment.schemaVersion;
|
|
831
|
+
if (schema !== SUPPORTED_ASSESSMENT_SCHEMA) {
|
|
832
|
+
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.`);
|
|
833
|
+
}
|
|
834
|
+
for (const line of formatAssessment(body, built.skipped)) write(line);
|
|
835
|
+
}
|
|
486
836
|
async function main() {
|
|
487
837
|
const args = parseArgs(process.argv);
|
|
488
838
|
if (args.version) {
|
|
@@ -494,7 +844,8 @@ async function main() {
|
|
|
494
844
|
process.exit(EXIT_OK);
|
|
495
845
|
}
|
|
496
846
|
if (args.command === "scan") {
|
|
497
|
-
await
|
|
847
|
+
if (args.profiles.length > 0) await runProfile(args);
|
|
848
|
+
else await runScan(args);
|
|
498
849
|
} else {
|
|
499
850
|
console.error(`
|
|
500
851
|
${c.red}Unknown command: ${args.command}${c.reset}`);
|
package/dist/cli.mjs
CHANGED
|
@@ -4,9 +4,260 @@ import {
|
|
|
4
4
|
} from "./chunk-XVA2M2HS.mjs";
|
|
5
5
|
|
|
6
6
|
// src/cli.ts
|
|
7
|
-
import { readFileSync, readdirSync, statSync, appendFileSync, writeFileSync } from "fs";
|
|
7
|
+
import { readFileSync as readFileSync2, readdirSync as readdirSync2, statSync, appendFileSync, writeFileSync } from "fs";
|
|
8
8
|
import { join, relative, extname } from "path";
|
|
9
|
-
|
|
9
|
+
|
|
10
|
+
// src/profile.ts
|
|
11
|
+
import { closeSync, constants, fstatSync, lstatSync, openSync, readdirSync, readFileSync } from "fs";
|
|
12
|
+
var SUPPORTED_PROFILES = ["owasp-agentic-skills-2026"];
|
|
13
|
+
var SUPPORTED_ASSESSMENT_SCHEMA = "1.1.0";
|
|
14
|
+
var LOCAL_LIMITS = { maxFiles: 5e3, maxFileBytes: 8 * 1024 * 1024, maxTotalBytes: 32 * 1024 * 1024, maxDepth: 64 };
|
|
15
|
+
var VENDORED_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "__pycache__"]);
|
|
16
|
+
var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
17
|
+
".png",
|
|
18
|
+
".jpg",
|
|
19
|
+
".jpeg",
|
|
20
|
+
".gif",
|
|
21
|
+
".webp",
|
|
22
|
+
".ico",
|
|
23
|
+
".bmp",
|
|
24
|
+
".tif",
|
|
25
|
+
".tiff",
|
|
26
|
+
".avif",
|
|
27
|
+
".pdf",
|
|
28
|
+
".woff",
|
|
29
|
+
".woff2",
|
|
30
|
+
".ttf",
|
|
31
|
+
".otf",
|
|
32
|
+
".eot",
|
|
33
|
+
".zip",
|
|
34
|
+
".gz",
|
|
35
|
+
".tgz",
|
|
36
|
+
".tar",
|
|
37
|
+
".bz2",
|
|
38
|
+
".xz",
|
|
39
|
+
".7z",
|
|
40
|
+
".mp3",
|
|
41
|
+
".mp4",
|
|
42
|
+
".mov",
|
|
43
|
+
".wav",
|
|
44
|
+
".ogg",
|
|
45
|
+
".webm",
|
|
46
|
+
".wasm",
|
|
47
|
+
".so",
|
|
48
|
+
".dll",
|
|
49
|
+
".dylib",
|
|
50
|
+
".exe",
|
|
51
|
+
".class",
|
|
52
|
+
".jar",
|
|
53
|
+
".pyc",
|
|
54
|
+
".sqlite",
|
|
55
|
+
".db"
|
|
56
|
+
]);
|
|
57
|
+
var PackageError = class extends Error {
|
|
58
|
+
};
|
|
59
|
+
function displayPath(path) {
|
|
60
|
+
return JSON.stringify(safeText(path));
|
|
61
|
+
}
|
|
62
|
+
function safeText(text) {
|
|
63
|
+
let out = "";
|
|
64
|
+
for (const ch of text) {
|
|
65
|
+
const cp = ch.codePointAt(0);
|
|
66
|
+
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;
|
|
67
|
+
out += hidden ? `\\u{${cp.toString(16).toUpperCase().padStart(4, "0")}}` : ch;
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
function nameProblem(name) {
|
|
72
|
+
for (let i = 0; i < name.length; i++) {
|
|
73
|
+
const c2 = name.charCodeAt(i);
|
|
74
|
+
if (c2 >= 55296 && c2 <= 56319) {
|
|
75
|
+
const next = name.charCodeAt(i + 1);
|
|
76
|
+
if (next >= 56320 && next <= 57343) {
|
|
77
|
+
i++;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
return "contains an unpaired UTF-16 surrogate";
|
|
81
|
+
}
|
|
82
|
+
if (c2 >= 56320 && c2 <= 57343) return "contains an unpaired UTF-16 surrogate";
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
var strictUtf8 = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
|
|
87
|
+
function decodeStrict(bytes) {
|
|
88
|
+
return strictUtf8.decode(bytes);
|
|
89
|
+
}
|
|
90
|
+
function listEntries(dir, shown) {
|
|
91
|
+
const entries = [];
|
|
92
|
+
if (process.platform === "win32") {
|
|
93
|
+
for (const d of readdirSync(dir, { withFileTypes: true })) {
|
|
94
|
+
const problem = nameProblem(d.name);
|
|
95
|
+
if (problem) throw new PackageError(`An entry in ${displayPath(shown)} ${problem}, so it cannot be sent faithfully.`);
|
|
96
|
+
entries.push({ name: d.name, kind: d.isSymbolicLink() ? "symlink" : d.isDirectory() ? "dir" : d.isFile() ? "file" : "other" });
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
for (const d of readdirSync(dir, { withFileTypes: true, encoding: "buffer" })) {
|
|
100
|
+
let name;
|
|
101
|
+
try {
|
|
102
|
+
name = decodeStrict(d.name);
|
|
103
|
+
} catch {
|
|
104
|
+
throw new PackageError(`An entry in ${displayPath(shown)} has a name that is not valid UTF-8, so it cannot be sent faithfully.`);
|
|
105
|
+
}
|
|
106
|
+
entries.push({ name, kind: d.isSymbolicLink() ? "symlink" : d.isDirectory() ? "dir" : d.isFile() ? "file" : "other" });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
110
|
+
}
|
|
111
|
+
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.`;
|
|
112
|
+
var specialMessage = (rel) => `${displayPath(rel)} is not a regular file or directory (it is a device, socket or pipe), so it cannot be sent.`;
|
|
113
|
+
function readRegularFile(full, rel, budget) {
|
|
114
|
+
let fd;
|
|
115
|
+
try {
|
|
116
|
+
fd = openSync(full, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
117
|
+
} catch (e) {
|
|
118
|
+
const code = e.code;
|
|
119
|
+
if (code === "ELOOP") throw new PackageError(symlinkMessage(rel));
|
|
120
|
+
throw new PackageError(`Could not read ${displayPath(rel)}: ${code ?? "unknown error"}.`);
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
const st = fstatSync(fd);
|
|
124
|
+
if (!st.isFile()) throw new PackageError(specialMessage(rel));
|
|
125
|
+
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.`);
|
|
126
|
+
const bytes = readFileSync(fd);
|
|
127
|
+
if (bytes.length > LOCAL_LIMITS.maxFileBytes) throw new PackageError(`${displayPath(rel)} grew past the CLI's local read guard while it was being read.`);
|
|
128
|
+
budget.total += bytes.length;
|
|
129
|
+
if (budget.total > LOCAL_LIMITS.maxTotalBytes) throw new PackageError(`The package is larger than the CLI's local guard of ${LOCAL_LIMITS.maxTotalBytes} bytes.`);
|
|
130
|
+
return bytes;
|
|
131
|
+
} finally {
|
|
132
|
+
closeSync(fd);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
var extensionOf = (name) => {
|
|
136
|
+
const i = name.lastIndexOf(".");
|
|
137
|
+
return i <= 0 ? "" : name.slice(i).toLowerCase();
|
|
138
|
+
};
|
|
139
|
+
function buildPackage(root) {
|
|
140
|
+
let rootStat;
|
|
141
|
+
try {
|
|
142
|
+
rootStat = lstatSync(root);
|
|
143
|
+
} catch {
|
|
144
|
+
throw new PackageError(`${displayPath(root)} does not exist or cannot be read.`);
|
|
145
|
+
}
|
|
146
|
+
if (rootStat.isSymbolicLink()) throw new PackageError(symlinkMessage(root));
|
|
147
|
+
if (!rootStat.isDirectory()) throw new PackageError(`${displayPath(root)} is not a directory. A profile assessment takes a directory.`);
|
|
148
|
+
const files = [];
|
|
149
|
+
const skipped = [];
|
|
150
|
+
const budget = { total: 0 };
|
|
151
|
+
const walk = (dir, prefix, depth) => {
|
|
152
|
+
if (depth > LOCAL_LIMITS.maxDepth) throw new PackageError(`The package is nested deeper than ${LOCAL_LIMITS.maxDepth} directories.`);
|
|
153
|
+
for (const entry of listEntries(dir, prefix || ".")) {
|
|
154
|
+
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
155
|
+
const full = `${dir}/${entry.name}`;
|
|
156
|
+
if (entry.kind === "symlink") throw new PackageError(symlinkMessage(rel));
|
|
157
|
+
if (entry.kind === "other") throw new PackageError(specialMessage(rel));
|
|
158
|
+
if (entry.kind === "dir") {
|
|
159
|
+
if (VENDORED_DIRS.has(entry.name)) {
|
|
160
|
+
skipped.push({ path: rel, reason: "vendored directory" });
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
walk(full, rel, depth + 1);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (BINARY_EXTENSIONS.has(extensionOf(entry.name))) {
|
|
167
|
+
skipped.push({ path: rel, reason: "binary asset" });
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (files.length >= LOCAL_LIMITS.maxFiles) throw new PackageError(`The package has more than ${LOCAL_LIMITS.maxFiles} files, over the CLI's local guard.`);
|
|
171
|
+
const bytes = readRegularFile(full, rel, budget);
|
|
172
|
+
let content;
|
|
173
|
+
try {
|
|
174
|
+
content = decodeStrict(bytes);
|
|
175
|
+
} catch {
|
|
176
|
+
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.`);
|
|
177
|
+
}
|
|
178
|
+
files.push({ path: rel, content });
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
walk(root, "", 0);
|
|
182
|
+
if (files.length === 0) throw new PackageError(`${displayPath(root)} contains no files to assess.`);
|
|
183
|
+
return { files, skipped };
|
|
184
|
+
}
|
|
185
|
+
function serializeRequest(profile, files) {
|
|
186
|
+
return JSON.stringify({ profile, files });
|
|
187
|
+
}
|
|
188
|
+
async function postProfile(apiUrl, apiKey, body, timeoutMs) {
|
|
189
|
+
const controller = new AbortController();
|
|
190
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
191
|
+
try {
|
|
192
|
+
const response = await fetch(apiUrl, {
|
|
193
|
+
method: "POST",
|
|
194
|
+
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json", "User-Agent": "agentverify-cli/1.6.0" },
|
|
195
|
+
body,
|
|
196
|
+
signal: controller.signal
|
|
197
|
+
});
|
|
198
|
+
const text = await response.text();
|
|
199
|
+
let json = null;
|
|
200
|
+
try {
|
|
201
|
+
json = JSON.parse(text);
|
|
202
|
+
} catch {
|
|
203
|
+
json = null;
|
|
204
|
+
}
|
|
205
|
+
return { kind: "response", status: response.status, text, json };
|
|
206
|
+
} catch (e) {
|
|
207
|
+
if (e instanceof Error && e.name === "AbortError") return { kind: "failure", message: `Request timed out after ${timeoutMs}ms` };
|
|
208
|
+
return { kind: "failure", message: `Request failed: ${e instanceof Error ? e.message : String(e)}` };
|
|
209
|
+
} finally {
|
|
210
|
+
clearTimeout(timer);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
214
|
+
var str = (v) => typeof v === "string" ? safeText(v) : typeof v === "number" || typeof v === "boolean" ? String(v) : "";
|
|
215
|
+
function formatAssessment(body, skipped) {
|
|
216
|
+
const a = isObj(body.assessment) ? body.assessment : {};
|
|
217
|
+
const profile = isObj(a.profile) ? a.profile : {};
|
|
218
|
+
const pkg = isObj(a.package) ? a.package : {};
|
|
219
|
+
const lines = [];
|
|
220
|
+
lines.push("Agent Verify profile assessment");
|
|
221
|
+
lines.push(`Profile: ${str(body.profile)}`);
|
|
222
|
+
lines.push(`Framework: ${str(profile.framework)} (upstream ${str(profile.upstreamRepo)} @ ${str(profile.upstreamCommit).slice(0, 12)}, ${str(profile.upstreamStatus)}; profile version ${str(profile.agentverifyProfileVersion)})`);
|
|
223
|
+
lines.push(`Assessment schema: ${str(a.schemaVersion)}`);
|
|
224
|
+
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)}`);
|
|
225
|
+
lines.push(`Package: ${str(pkg.fileCount)} files, digest ${str(pkg.digest)}`);
|
|
226
|
+
lines.push(`Attestation: ${body.attestation === null ? "none (this profile assessment is unsigned)" : "present (not verified by this CLI)"}`);
|
|
227
|
+
lines.push("");
|
|
228
|
+
for (const control of Array.isArray(a.controls) ? a.controls : []) {
|
|
229
|
+
if (!isObj(control)) continue;
|
|
230
|
+
const cov = isObj(control.coverage) ? control.coverage : {};
|
|
231
|
+
lines.push(`${str(control.controlId)} ${str(control.title)}: ${str(control.status)} (confidence ${str(control.confidence)})`);
|
|
232
|
+
lines.push(` ${str(cov.evidenceObserved)} evidence observed, ${str(cov.gapIdentified)} gaps identified, ${str(cov.notAssessed)} not assessed, of ${str(cov.total)} checks`);
|
|
233
|
+
for (const check of Array.isArray(control.checks) ? control.checks : []) {
|
|
234
|
+
if (isObj(check)) lines.push(` ${str(check.checkId)} ${str(check.status)}: ${str(check.title)}`);
|
|
235
|
+
}
|
|
236
|
+
lines.push("");
|
|
237
|
+
}
|
|
238
|
+
const gaps = (Array.isArray(a.evidence) ? a.evidence : []).filter((e) => isObj(e) && e.polarity === "gap");
|
|
239
|
+
lines.push(`Gaps identified (${gaps.length}):`);
|
|
240
|
+
for (const g of gaps) {
|
|
241
|
+
lines.push(` [${str(g.severity)}] ${str(g.kind)}: ${str(g.summary)}`);
|
|
242
|
+
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)}` : ""}`);
|
|
243
|
+
if (locations.length) lines.push(` at ${locations.join(", ")}`);
|
|
244
|
+
}
|
|
245
|
+
if (gaps.length === 0) lines.push(" none");
|
|
246
|
+
const notImplemented = Array.isArray(a.notImplementedControls) ? a.notImplementedControls.map(str).join(", ") : "";
|
|
247
|
+
lines.push("");
|
|
248
|
+
lines.push(`Not assessed by this profile version: ${notImplemented || "none"}`);
|
|
249
|
+
for (const note of Array.isArray(a.notes) ? a.notes : []) lines.push(`Note: ${str(note)}`);
|
|
250
|
+
if (skipped.length > 0) {
|
|
251
|
+
lines.push("");
|
|
252
|
+
lines.push(`Skipped from the package (${skipped.length}), not sent:`);
|
|
253
|
+
for (const s of skipped.slice(0, 20)) lines.push(` ${displayPath(s.path)}: ${s.reason}`);
|
|
254
|
+
if (skipped.length > 20) lines.push(` and ${skipped.length - 20} more`);
|
|
255
|
+
}
|
|
256
|
+
return lines;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// src/cli.ts
|
|
260
|
+
var VERSION = "1.6.0";
|
|
10
261
|
var API_URL = process.env.AGENTVERIFY_API_URL ?? "https://agentverify-api.agentverify.workers.dev/v1/scan";
|
|
11
262
|
var A2SPA_DOCS_URL = "https://aimodularity.com/A2SPA/docs";
|
|
12
263
|
var EXIT_OK = 0;
|
|
@@ -26,13 +277,13 @@ var c = {
|
|
|
26
277
|
};
|
|
27
278
|
var write = (value = "") => process.stdout.write(`${value}
|
|
28
279
|
`);
|
|
29
|
-
var SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".ts", ".py", ".json", ".yaml", ".yml", ".md", ".mjs", ".cjs"]);
|
|
280
|
+
var SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx", ".py", ".json", ".yaml", ".yml", ".toml", ".md", ".mjs", ".cjs", ".sh"]);
|
|
30
281
|
var MAX_SCAN_FILE_BYTES = 5 * 1024 * 1024;
|
|
31
282
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", "vendor", "coverage", ".cache"]);
|
|
32
283
|
function collectFiles(dir) {
|
|
33
284
|
const files = [];
|
|
34
285
|
try {
|
|
35
|
-
const entries =
|
|
286
|
+
const entries = readdirSync2(dir);
|
|
36
287
|
for (const entry of entries) {
|
|
37
288
|
if (SKIP_DIRS.has(entry)) continue;
|
|
38
289
|
const fullPath = join(dir, entry);
|
|
@@ -61,7 +312,8 @@ function parseArgs(argv) {
|
|
|
61
312
|
help: false,
|
|
62
313
|
version: false,
|
|
63
314
|
policy: "",
|
|
64
|
-
summaryFile: ""
|
|
315
|
+
summaryFile: "",
|
|
316
|
+
profiles: []
|
|
65
317
|
};
|
|
66
318
|
if (result.command === "--help" || result.command === "-h") {
|
|
67
319
|
result.command = "";
|
|
@@ -81,6 +333,8 @@ function parseArgs(argv) {
|
|
|
81
333
|
else if (arg === "--allow-not-assessed") result.allowNotAssessed = true;
|
|
82
334
|
else if (arg === "--policy") result.policy = args[++i] ?? "";
|
|
83
335
|
else if (arg === "--summary-file") result.summaryFile = args[++i] ?? "";
|
|
336
|
+
else if (arg === "--profile") result.profiles.push(args[++i] ?? "");
|
|
337
|
+
else if (arg.startsWith("--profile=")) result.profiles.push(arg.slice("--profile=".length));
|
|
84
338
|
else if (arg === "--help" || arg === "-h") result.help = true;
|
|
85
339
|
else if (arg === "--version" || arg === "-v") result.version = true;
|
|
86
340
|
else if (!arg.startsWith("-")) result.dir = arg;
|
|
@@ -112,6 +366,11 @@ ${c.bold}Options:${c.reset}
|
|
|
112
366
|
in addition to the scanner's own verdict (e.g. financial-agent)
|
|
113
367
|
--summary-file <path>
|
|
114
368
|
Write results as JSON to this path (in addition to any other output)
|
|
369
|
+
--profile <id>
|
|
370
|
+
(alpha) Assess a directory against an assessment profile instead of
|
|
371
|
+
scanning files individually. Supported: ${SUPPORTED_PROFILES.join(", ")}
|
|
372
|
+
Takes a directory; not combinable with --file, --ci, --policy,
|
|
373
|
+
--allow-not-assessed, --markdown or --summary-file.
|
|
115
374
|
--version, -v Print version
|
|
116
375
|
--help, -h Show this help message
|
|
117
376
|
|
|
@@ -131,12 +390,20 @@ ${c.bold}Examples:${c.reset}
|
|
|
131
390
|
${c.gray}# Markdown summary for pull requests${c.reset}
|
|
132
391
|
agentverify scan . --key av_your_key --markdown
|
|
133
392
|
|
|
393
|
+
${c.gray}# Assess a skill directory against a profile (alpha); --json prints the service's response${c.reset}
|
|
394
|
+
agentverify scan ./my-skill --profile ${SUPPORTED_PROFILES[0]} --key av_your_key
|
|
395
|
+
|
|
134
396
|
${c.bold}Exit codes (--ci):${c.reset}
|
|
135
397
|
0 All scanned files verified
|
|
136
398
|
1 NOT VERIFIED \u2014 a security/verification check failed
|
|
137
399
|
2 NOT ASSESSED \u2014 insufficient evidence for a verdict (see --allow-not-assessed)
|
|
138
400
|
3 Execution error \u2014 the scan itself could not run (bad key, network, missing file)
|
|
139
401
|
|
|
402
|
+
${c.bold}Exit codes (--profile):${c.reset}
|
|
403
|
+
0 The assessment completed. Control and check statuses (including GAP_IDENTIFIED)
|
|
404
|
+
do not affect the exit code.
|
|
405
|
+
3 Usage or execution error \u2014 nothing was assessed
|
|
406
|
+
|
|
140
407
|
${c.bold}Get your API key:${c.reset}
|
|
141
408
|
https://aimodularity.com/agentverify/dashboard
|
|
142
409
|
|
|
@@ -190,6 +457,8 @@ ${c.bold}Fix first:${c.reset}`);
|
|
|
190
457
|
else write(` No blocking fixes detected. Re-scan after any material change.`);
|
|
191
458
|
if (result.reportInsights?.nextAction) write(`
|
|
192
459
|
${c.bold}Next action:${c.reset} ${result.reportInsights.nextAction}`);
|
|
460
|
+
const lockedFixes = (result.findings ?? []).filter((f) => f.fixLocked).length;
|
|
461
|
+
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}`);
|
|
193
462
|
const threats = getRelevantThreatCategories(result);
|
|
194
463
|
if (threats.length) {
|
|
195
464
|
write(`
|
|
@@ -278,7 +547,7 @@ ${c.bold}Agent Verify${c.reset} ${c.gray}v${VERSION}${c.reset}`);
|
|
|
278
547
|
files = collectFiles(dir);
|
|
279
548
|
if (files.length === 0) {
|
|
280
549
|
write(`${c.yellow}No agent files found in ${dir}${c.reset}`);
|
|
281
|
-
write(`${c.gray}Supported: .js .ts .py .json .yaml .yml .md${c.reset}
|
|
550
|
+
write(`${c.gray}Supported: .js .jsx .ts .tsx .py .json .yaml .yml .toml .md .sh${c.reset}
|
|
282
551
|
`);
|
|
283
552
|
process.exit(0);
|
|
284
553
|
}
|
|
@@ -297,7 +566,7 @@ ${c.bold}Agent Verify${c.reset} ${c.gray}v${VERSION}${c.reset}`);
|
|
|
297
566
|
if (fileStat.size > MAX_SCAN_FILE_BYTES) {
|
|
298
567
|
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`);
|
|
299
568
|
}
|
|
300
|
-
const content =
|
|
569
|
+
const content = readFileSync2(filePath, "utf-8");
|
|
301
570
|
const relPath = relative(args.dir === "." ? process.cwd() : args.dir, filePath);
|
|
302
571
|
const result = await scan(
|
|
303
572
|
{ content, fileName: relPath, policyId: args.policy || void 0 },
|
|
@@ -373,6 +642,89 @@ ${c.gray}No dashboard report was saved. Check API key access and retry if you ne
|
|
|
373
642
|
if (notAssessed > 0 && !args.allowNotAssessed) process.exit(EXIT_NOT_ASSESSED);
|
|
374
643
|
}
|
|
375
644
|
}
|
|
645
|
+
var PROFILE_TIMEOUT_MS = 6e4;
|
|
646
|
+
function profileUsageError(message) {
|
|
647
|
+
console.error(`
|
|
648
|
+
${c.red}Error:${c.reset} ${message}`);
|
|
649
|
+
console.error(`Run ${c.cyan}agentverify --help${c.reset} for usage
|
|
650
|
+
`);
|
|
651
|
+
process.exitCode = EXIT_EXECUTION_ERROR;
|
|
652
|
+
}
|
|
653
|
+
function profileFailure(json, message, detail = {}) {
|
|
654
|
+
if (json) write(JSON.stringify({ error: message, ...detail }, null, 2));
|
|
655
|
+
else console.error(`
|
|
656
|
+
${c.red}Error:${c.reset} ${safeText(message)}
|
|
657
|
+
`);
|
|
658
|
+
process.exitCode = EXIT_EXECUTION_ERROR;
|
|
659
|
+
}
|
|
660
|
+
async function runProfile(args) {
|
|
661
|
+
if (args.profiles.length > 1) return profileUsageError("--profile was given more than once.");
|
|
662
|
+
const profile = args.profiles[0];
|
|
663
|
+
if (profile === "") return profileUsageError(`--profile requires a value. Supported profiles: ${SUPPORTED_PROFILES.join(", ")}`);
|
|
664
|
+
if (!SUPPORTED_PROFILES.includes(profile)) {
|
|
665
|
+
return profileUsageError(`Unknown profile ${JSON.stringify(safeText(profile))}. Supported profiles: ${SUPPORTED_PROFILES.join(", ")}`);
|
|
666
|
+
}
|
|
667
|
+
const conflicts = [];
|
|
668
|
+
if (args.file) conflicts.push("--file");
|
|
669
|
+
if (args.ci) conflicts.push("--ci");
|
|
670
|
+
if (args.allowNotAssessed) conflicts.push("--allow-not-assessed");
|
|
671
|
+
if (args.policy) conflicts.push("--policy");
|
|
672
|
+
if (args.markdown) conflicts.push("--markdown");
|
|
673
|
+
if (args.summaryFile) conflicts.push("--summary-file");
|
|
674
|
+
if (conflicts.length > 0) return profileUsageError(`--profile cannot be combined with ${conflicts.join(", ")}. A profile assessment takes a directory and reports its own result.`);
|
|
675
|
+
if (!args.key) {
|
|
676
|
+
console.error(`
|
|
677
|
+
${c.red}Error:${c.reset} API key required
|
|
678
|
+
`);
|
|
679
|
+
console.error(` Set with --key av_your_key or AGENTVERIFY_API_KEY env var.`);
|
|
680
|
+
console.error(` Get your key at https://aimodularity.com/agentverify/dashboard/
|
|
681
|
+
`);
|
|
682
|
+
process.exitCode = EXIT_EXECUTION_ERROR;
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
let built;
|
|
686
|
+
try {
|
|
687
|
+
built = buildPackage(args.dir);
|
|
688
|
+
} catch (e) {
|
|
689
|
+
return profileFailure(args.json, e instanceof PackageError ? e.message : `Could not read the package: ${e instanceof Error ? e.message : String(e)}`);
|
|
690
|
+
}
|
|
691
|
+
if (args.json && built.skipped.length > 0) {
|
|
692
|
+
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` : ""}`);
|
|
693
|
+
}
|
|
694
|
+
const result = await postProfile(API_URL, args.key, serializeRequest(profile, built.files), PROFILE_TIMEOUT_MS);
|
|
695
|
+
if (result.kind === "failure") return profileFailure(args.json, result.message);
|
|
696
|
+
const body = result.json;
|
|
697
|
+
const isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
698
|
+
if (result.status !== 200) {
|
|
699
|
+
if (isObject(body) && typeof body.error === "string") {
|
|
700
|
+
if (args.json) {
|
|
701
|
+
write(JSON.stringify(body, null, 2));
|
|
702
|
+
} else {
|
|
703
|
+
console.error(`
|
|
704
|
+
${c.red}Error:${c.reset} ${safeText(body.error)} (HTTP ${result.status})`);
|
|
705
|
+
const rejection = body.rejection;
|
|
706
|
+
if (isObject(rejection)) console.error(` rejection: ${safeText(String(rejection.code ?? ""))}: ${safeText(String(rejection.message ?? ""))}${typeof rejection.path === "string" ? ` (${JSON.stringify(safeText(rejection.path))})` : ""}`);
|
|
707
|
+
if (Array.isArray(body.supportedProfiles)) console.error(` supported profiles: ${body.supportedProfiles.map((p) => safeText(String(p))).join(", ")}`);
|
|
708
|
+
console.error("");
|
|
709
|
+
}
|
|
710
|
+
process.exitCode = EXIT_EXECUTION_ERROR;
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
return profileFailure(args.json, `Unexpected response from the service (HTTP ${result.status})`, { status: result.status });
|
|
714
|
+
}
|
|
715
|
+
if (!isObject(body) || typeof body.profile !== "string" || !isObject(body.assessment)) {
|
|
716
|
+
return profileFailure(args.json, "The service returned a response that is not a profile assessment", { status: result.status });
|
|
717
|
+
}
|
|
718
|
+
if (args.json) {
|
|
719
|
+
write(JSON.stringify(body, null, 2));
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
const schema = body.assessment.schemaVersion;
|
|
723
|
+
if (schema !== SUPPORTED_ASSESSMENT_SCHEMA) {
|
|
724
|
+
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.`);
|
|
725
|
+
}
|
|
726
|
+
for (const line of formatAssessment(body, built.skipped)) write(line);
|
|
727
|
+
}
|
|
376
728
|
async function main() {
|
|
377
729
|
const args = parseArgs(process.argv);
|
|
378
730
|
if (args.version) {
|
|
@@ -384,7 +736,8 @@ async function main() {
|
|
|
384
736
|
process.exit(EXIT_OK);
|
|
385
737
|
}
|
|
386
738
|
if (args.command === "scan") {
|
|
387
|
-
await
|
|
739
|
+
if (args.profiles.length > 0) await runProfile(args);
|
|
740
|
+
else await runScan(args);
|
|
388
741
|
} else {
|
|
389
742
|
console.error(`
|
|
390
743
|
${c.red}Unknown command: ${args.command}${c.reset}`);
|
package/dist/sdk.d.mts
CHANGED
package/dist/sdk.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentverify",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "CLI and SDK for Agent Verify — execution trust analysis for AI agents",
|
|
5
5
|
"bin": {
|
|
6
6
|
"agentverify": "dist/cli.js"
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"scripts": {
|
|
21
21
|
"build": "tsup src/cli.ts src/sdk.ts --format cjs,esm --dts --clean",
|
|
22
22
|
"dev": "tsup src/cli.ts src/sdk.ts --format cjs,esm --dts --watch",
|
|
23
|
-
"test": "npm run build && node test/cli.test.mjs",
|
|
23
|
+
"test": "npm run build && node test/cli.test.mjs && node --test test/profile.test.mjs",
|
|
24
24
|
"prepublishOnly": "npm run build"
|
|
25
25
|
},
|
|
26
26
|
"engines": { "node": ">=18.0.0" },
|