agentverify 1.3.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 +48 -0
- package/README.md +12 -1
- package/dist/{chunk-PY3NS2YH.mjs → chunk-XVA2M2HS.mjs} +8 -2
- package/dist/cli.js +445 -27
- package/dist/cli.mjs +439 -25
- package/dist/sdk.d.mts +28 -0
- package/dist/sdk.d.ts +28 -0
- package/dist/sdk.js +8 -2
- package/dist/sdk.mjs +1 -1
- package/package.json +4 -4
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
|
|
@@ -55,7 +55,12 @@ function normalizeScanResult(data) {
|
|
|
55
55
|
scanDuration: 0
|
|
56
56
|
},
|
|
57
57
|
saved: data.saved === true,
|
|
58
|
-
reportUrl: data.reportUrl ?? null
|
|
58
|
+
reportUrl: data.reportUrl ?? null,
|
|
59
|
+
artifactFingerprint: data.artifactFingerprint,
|
|
60
|
+
reportIntegrity: data.reportIntegrity,
|
|
61
|
+
policyProfile: data.policyProfile ?? null,
|
|
62
|
+
policyResult: data.policyResult ?? null,
|
|
63
|
+
attestation: data.attestation ?? null
|
|
59
64
|
};
|
|
60
65
|
}
|
|
61
66
|
async function scan(input, options) {
|
|
@@ -81,7 +86,8 @@ async function scan(input, options) {
|
|
|
81
86
|
body: JSON.stringify({
|
|
82
87
|
content: input.content,
|
|
83
88
|
fileName: input.fileName ?? "agent",
|
|
84
|
-
platform: input.platform
|
|
89
|
+
platform: input.platform,
|
|
90
|
+
policyId: input.policyId
|
|
85
91
|
}),
|
|
86
92
|
signal: controller.signal
|
|
87
93
|
});
|
|
@@ -109,10 +115,263 @@ async function scan(input, options) {
|
|
|
109
115
|
}
|
|
110
116
|
}
|
|
111
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
|
+
|
|
112
367
|
// src/cli.ts
|
|
113
|
-
var VERSION = "1.
|
|
368
|
+
var VERSION = "1.6.0";
|
|
114
369
|
var API_URL = process.env.AGENTVERIFY_API_URL ?? "https://agentverify-api.agentverify.workers.dev/v1/scan";
|
|
115
370
|
var A2SPA_DOCS_URL = "https://aimodularity.com/A2SPA/docs";
|
|
371
|
+
var EXIT_OK = 0;
|
|
372
|
+
var EXIT_NOT_VERIFIED = 1;
|
|
373
|
+
var EXIT_NOT_ASSESSED = 2;
|
|
374
|
+
var EXIT_EXECUTION_ERROR = 3;
|
|
116
375
|
var c = {
|
|
117
376
|
reset: "\x1B[0m",
|
|
118
377
|
bold: "\x1B[1m",
|
|
@@ -126,16 +385,17 @@ var c = {
|
|
|
126
385
|
};
|
|
127
386
|
var write = (value = "") => process.stdout.write(`${value}
|
|
128
387
|
`);
|
|
129
|
-
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"]);
|
|
389
|
+
var MAX_SCAN_FILE_BYTES = 5 * 1024 * 1024;
|
|
130
390
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", "vendor", "coverage", ".cache"]);
|
|
131
391
|
function collectFiles(dir) {
|
|
132
392
|
const files = [];
|
|
133
393
|
try {
|
|
134
|
-
const entries = (0,
|
|
394
|
+
const entries = (0, import_fs2.readdirSync)(dir);
|
|
135
395
|
for (const entry of entries) {
|
|
136
396
|
if (SKIP_DIRS.has(entry)) continue;
|
|
137
397
|
const fullPath = (0, import_path.join)(dir, entry);
|
|
138
|
-
const stat = (0,
|
|
398
|
+
const stat = (0, import_fs2.statSync)(fullPath);
|
|
139
399
|
if (stat.isDirectory()) {
|
|
140
400
|
files.push(...collectFiles(fullPath));
|
|
141
401
|
} else if (SUPPORTED_EXTENSIONS.has((0, import_path.extname)(entry).toLowerCase())) {
|
|
@@ -158,7 +418,10 @@ function parseArgs(argv) {
|
|
|
158
418
|
ci: false,
|
|
159
419
|
allowNotAssessed: false,
|
|
160
420
|
help: false,
|
|
161
|
-
version: false
|
|
421
|
+
version: false,
|
|
422
|
+
policy: "",
|
|
423
|
+
summaryFile: "",
|
|
424
|
+
profiles: []
|
|
162
425
|
};
|
|
163
426
|
if (result.command === "--help" || result.command === "-h") {
|
|
164
427
|
result.command = "";
|
|
@@ -176,6 +439,10 @@ function parseArgs(argv) {
|
|
|
176
439
|
else if (arg === "--markdown") result.markdown = true;
|
|
177
440
|
else if (arg === "--ci") result.ci = true;
|
|
178
441
|
else if (arg === "--allow-not-assessed") result.allowNotAssessed = true;
|
|
442
|
+
else if (arg === "--policy") result.policy = args[++i] ?? "";
|
|
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));
|
|
179
446
|
else if (arg === "--help" || arg === "-h") result.help = true;
|
|
180
447
|
else if (arg === "--version" || arg === "-v") result.version = true;
|
|
181
448
|
else if (!arg.startsWith("-")) result.dir = arg;
|
|
@@ -203,6 +470,15 @@ ${c.bold}Options:${c.reset}
|
|
|
203
470
|
--ci CI mode: concise output and non-zero exit for NOT VERIFIED
|
|
204
471
|
--allow-not-assessed
|
|
205
472
|
In CI mode, do not fail only because content is NOT_ASSESSED
|
|
473
|
+
--policy <id> Evaluate a built-in policy profile against this scan's evidence
|
|
474
|
+
in addition to the scanner's own verdict (e.g. financial-agent)
|
|
475
|
+
--summary-file <path>
|
|
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.
|
|
206
482
|
--version, -v Print version
|
|
207
483
|
--help, -h Show this help message
|
|
208
484
|
|
|
@@ -222,6 +498,20 @@ ${c.bold}Examples:${c.reset}
|
|
|
222
498
|
${c.gray}# Markdown summary for pull requests${c.reset}
|
|
223
499
|
agentverify scan . --key av_your_key --markdown
|
|
224
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
|
+
|
|
504
|
+
${c.bold}Exit codes (--ci):${c.reset}
|
|
505
|
+
0 All scanned files verified
|
|
506
|
+
1 NOT VERIFIED \u2014 a security/verification check failed
|
|
507
|
+
2 NOT ASSESSED \u2014 insufficient evidence for a verdict (see --allow-not-assessed)
|
|
508
|
+
3 Execution error \u2014 the scan itself could not run (bad key, network, missing file)
|
|
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
|
+
|
|
225
515
|
${c.bold}Get your API key:${c.reset}
|
|
226
516
|
https://aimodularity.com/agentverify/dashboard
|
|
227
517
|
|
|
@@ -243,6 +533,16 @@ function getFixFirst(result) {
|
|
|
243
533
|
function getRelevantThreatCategories(result) {
|
|
244
534
|
return (result.threatCategories ?? []).filter((item) => item.status !== "not_assessed").slice(0, 6).map((item) => `${item.label} (${item.status.replace("_", " ")})`);
|
|
245
535
|
}
|
|
536
|
+
function criticalCount(result) {
|
|
537
|
+
return result.findings.filter((f) => f.severity === "critical").length;
|
|
538
|
+
}
|
|
539
|
+
function highCount(result) {
|
|
540
|
+
return result.findings.filter((f) => f.severity === "high").length;
|
|
541
|
+
}
|
|
542
|
+
function fingerprintPreview(result) {
|
|
543
|
+
const hash = result.artifactFingerprint?.artifactHash;
|
|
544
|
+
return hash ? `${hash.slice(0, 12)}\u2026` : null;
|
|
545
|
+
}
|
|
246
546
|
function printDetailedSummary(result, fileName) {
|
|
247
547
|
const verified = result.verdict === "VERIFIED";
|
|
248
548
|
const verdictColor = verified ? c.green : result.verdict === "NOT_ASSESSED" ? c.yellow : c.red;
|
|
@@ -253,6 +553,10 @@ ${c.bold}${c.cyan}Agent Verify${c.reset}`);
|
|
|
253
553
|
write(`${c.bold}Verdict:${c.reset} ${verdictColor}${result.verdict === "NOT_VERIFIED" ? "NOT VERIFIED" : result.verdict.replace("_", " ")}${c.reset}`);
|
|
254
554
|
write(`${c.bold}Risk level:${c.reset} ${result.riskLevel}`);
|
|
255
555
|
write(`${c.bold}Confidence:${c.reset} ${result.confidence}/100`);
|
|
556
|
+
write(`${c.bold}Critical / High findings:${c.reset} ${criticalCount(result)} / ${highCount(result)}`);
|
|
557
|
+
if (result.metadata?.scannerVersion) write(`${c.bold}Ruleset version:${c.reset} ${result.metadata.scannerVersion}`);
|
|
558
|
+
if (fingerprintPreview(result)) write(`${c.bold}Artifact fingerprint:${c.reset} ${fingerprintPreview(result)}`);
|
|
559
|
+
if (result.policyProfile) write(`${c.bold}Policy (${result.policyProfile}):${c.reset} ${result.policyResult === "PASS" ? `${c.green}PASS${c.reset}` : `${c.red}FAIL${c.reset}`}`);
|
|
256
560
|
write(`${c.bold}Top blocker:${c.reset} ${getTopBlocker(result)}`);
|
|
257
561
|
write(`
|
|
258
562
|
${c.bold}Fix first:${c.reset}`);
|
|
@@ -261,6 +565,8 @@ ${c.bold}Fix first:${c.reset}`);
|
|
|
261
565
|
else write(` No blocking fixes detected. Re-scan after any material change.`);
|
|
262
566
|
if (result.reportInsights?.nextAction) write(`
|
|
263
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}`);
|
|
264
570
|
const threats = getRelevantThreatCategories(result);
|
|
265
571
|
if (threats.length) {
|
|
266
572
|
write(`
|
|
@@ -279,7 +585,11 @@ function markdownSummary(results, summary) {
|
|
|
279
585
|
lines.push(`## ${item.file}`, "", `Error: ${item.error ?? "Unknown error"}`, "");
|
|
280
586
|
continue;
|
|
281
587
|
}
|
|
282
|
-
lines.push(`## ${item.file}`, "", `Score: ${item.result.riskScore}/100`, `Verdict: ${item.result.verdict === "NOT_VERIFIED" ? "NOT VERIFIED" : item.result.verdict.replace("_", " ")}`, `Risk level: ${item.result.riskLevel}`, `
|
|
588
|
+
lines.push(`## ${item.file}`, "", `Score: ${item.result.riskScore}/100`, `Verdict: ${item.result.verdict === "NOT_VERIFIED" ? "NOT VERIFIED" : item.result.verdict.replace("_", " ")}`, `Risk level: ${item.result.riskLevel}`, `Critical / High findings: ${criticalCount(item.result)} / ${highCount(item.result)}`);
|
|
589
|
+
if (item.result.metadata?.scannerVersion) lines.push(`Ruleset version: ${item.result.metadata.scannerVersion}`);
|
|
590
|
+
if (fingerprintPreview(item.result)) lines.push(`Artifact fingerprint: \`${fingerprintPreview(item.result)}\``);
|
|
591
|
+
if (item.result.policyProfile) lines.push(`Policy (${item.result.policyProfile}): ${item.result.policyResult}`);
|
|
592
|
+
lines.push(`Top blocker: ${getTopBlocker(item.result)}`, "");
|
|
283
593
|
const fixes = getFixFirst(item.result);
|
|
284
594
|
if (fixes.length) lines.push("Fix first:", ...fixes.map((fix, index) => `${index + 1}. ${fix}`), "");
|
|
285
595
|
const threats = getRelevantThreatCategories(item.result);
|
|
@@ -289,6 +599,24 @@ function markdownSummary(results, summary) {
|
|
|
289
599
|
lines.push(`A2SPA docs: ${A2SPA_DOCS_URL}`);
|
|
290
600
|
return lines.join("\n");
|
|
291
601
|
}
|
|
602
|
+
function writeGithubJobSummary(results, summary) {
|
|
603
|
+
const summaryPath = process.env.GITHUB_STEP_SUMMARY;
|
|
604
|
+
if (!summaryPath) return;
|
|
605
|
+
try {
|
|
606
|
+
(0, import_fs2.appendFileSync)(summaryPath, `${markdownSummary(results, summary)}
|
|
607
|
+
`);
|
|
608
|
+
} catch (e) {
|
|
609
|
+
console.error(`Warning: could not write GitHub job summary: ${e instanceof Error ? e.message : e}`);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
function writeSummaryFile(path, results, summary) {
|
|
613
|
+
if (!path) return;
|
|
614
|
+
try {
|
|
615
|
+
(0, import_fs2.writeFileSync)(path, JSON.stringify({ results, summary }, null, 2));
|
|
616
|
+
} catch (e) {
|
|
617
|
+
console.error(`Warning: could not write summary file: ${e instanceof Error ? e.message : e}`);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
292
620
|
function printVerdict(result, fileName) {
|
|
293
621
|
const verified = result.verdict === "VERIFIED";
|
|
294
622
|
const notAssessed = result.verdict === "NOT_ASSESSED";
|
|
@@ -311,7 +639,7 @@ ${c.red}Error:${c.reset} API key required
|
|
|
311
639
|
console.error(` Get your key at https://aimodularity.com/agentverify/dashboard/`);
|
|
312
640
|
console.error(` Example: AGENTVERIFY_API_KEY=av_your_key agentverify scan .
|
|
313
641
|
`);
|
|
314
|
-
process.exit(
|
|
642
|
+
process.exit(EXIT_EXECUTION_ERROR);
|
|
315
643
|
}
|
|
316
644
|
let files = [];
|
|
317
645
|
if (args.file) {
|
|
@@ -327,7 +655,7 @@ ${c.bold}Agent Verify${c.reset} ${c.gray}v${VERSION}${c.reset}`);
|
|
|
327
655
|
files = collectFiles(dir);
|
|
328
656
|
if (files.length === 0) {
|
|
329
657
|
write(`${c.yellow}No agent files found in ${dir}${c.reset}`);
|
|
330
|
-
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}
|
|
331
659
|
`);
|
|
332
660
|
process.exit(0);
|
|
333
661
|
}
|
|
@@ -342,10 +670,14 @@ ${c.bold}Agent Verify${c.reset} ${c.gray}v${VERSION}${c.reset}`);
|
|
|
342
670
|
const batch = files.slice(i, i + CONCURRENCY);
|
|
343
671
|
const batchResults = await Promise.allSettled(
|
|
344
672
|
batch.map(async (filePath) => {
|
|
345
|
-
const
|
|
673
|
+
const fileStat = (0, import_fs2.statSync)(filePath);
|
|
674
|
+
if (fileStat.size > MAX_SCAN_FILE_BYTES) {
|
|
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`);
|
|
676
|
+
}
|
|
677
|
+
const content = (0, import_fs2.readFileSync)(filePath, "utf-8");
|
|
346
678
|
const relPath = (0, import_path.relative)(args.dir === "." ? process.cwd() : args.dir, filePath);
|
|
347
679
|
const result = await scan(
|
|
348
|
-
{ content, fileName: relPath },
|
|
680
|
+
{ content, fileName: relPath, policyId: args.policy || void 0 },
|
|
349
681
|
{ apiKey: args.key, apiUrl: API_URL }
|
|
350
682
|
);
|
|
351
683
|
return { filePath, relPath, result };
|
|
@@ -376,21 +708,23 @@ ${c.bold}Agent Verify${c.reset} ${c.gray}v${VERSION}${c.reset}`);
|
|
|
376
708
|
}
|
|
377
709
|
}
|
|
378
710
|
const summary = { total: files.length, verified, notVerified, notAssessed, errors };
|
|
711
|
+
writeGithubJobSummary(jsonResults, summary);
|
|
712
|
+
writeSummaryFile(args.summaryFile, jsonResults, summary);
|
|
379
713
|
if (args.json) {
|
|
380
714
|
write(JSON.stringify({ results: jsonResults, summary }, null, 2));
|
|
381
|
-
if (errors > 0) process.exit(
|
|
715
|
+
if (errors > 0) process.exit(EXIT_EXECUTION_ERROR);
|
|
382
716
|
if (args.ci) {
|
|
383
|
-
if (notVerified > 0) process.exit(
|
|
384
|
-
if (notAssessed > 0 && !args.allowNotAssessed) process.exit(
|
|
717
|
+
if (notVerified > 0) process.exit(EXIT_NOT_VERIFIED);
|
|
718
|
+
if (notAssessed > 0 && !args.allowNotAssessed) process.exit(EXIT_NOT_ASSESSED);
|
|
385
719
|
}
|
|
386
720
|
return;
|
|
387
721
|
}
|
|
388
722
|
if (args.markdown) {
|
|
389
723
|
write(markdownSummary(jsonResults, summary));
|
|
390
|
-
if (errors > 0) process.exit(
|
|
724
|
+
if (errors > 0) process.exit(EXIT_EXECUTION_ERROR);
|
|
391
725
|
if (args.ci) {
|
|
392
|
-
if (notVerified > 0) process.exit(
|
|
393
|
-
if (notAssessed > 0 && !args.allowNotAssessed) process.exit(
|
|
726
|
+
if (notVerified > 0) process.exit(EXIT_NOT_VERIFIED);
|
|
727
|
+
if (notAssessed > 0 && !args.allowNotAssessed) process.exit(EXIT_NOT_ASSESSED);
|
|
394
728
|
}
|
|
395
729
|
return;
|
|
396
730
|
}
|
|
@@ -410,34 +744,118 @@ ${c.gray}Saved CLI reports appear in your dashboard: https://aimodularity.com/ag
|
|
|
410
744
|
${c.gray}No dashboard report was saved. Check API key access and retry if you need dashboard sync.${c.reset}`);
|
|
411
745
|
write(`${c.gray}A2SPA docs: ${A2SPA_DOCS_URL}${c.reset}
|
|
412
746
|
`);
|
|
413
|
-
if (errors > 0) process.exit(
|
|
747
|
+
if (errors > 0) process.exit(EXIT_EXECUTION_ERROR);
|
|
414
748
|
if (args.ci) {
|
|
415
|
-
if (notVerified > 0) process.exit(
|
|
416
|
-
if (notAssessed > 0 && !args.allowNotAssessed) process.exit(
|
|
749
|
+
if (notVerified > 0) process.exit(EXIT_NOT_VERIFIED);
|
|
750
|
+
if (notAssessed > 0 && !args.allowNotAssessed) process.exit(EXIT_NOT_ASSESSED);
|
|
751
|
+
}
|
|
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.`);
|
|
417
833
|
}
|
|
834
|
+
for (const line of formatAssessment(body, built.skipped)) write(line);
|
|
418
835
|
}
|
|
419
836
|
async function main() {
|
|
420
837
|
const args = parseArgs(process.argv);
|
|
421
838
|
if (args.version) {
|
|
422
839
|
write(`agentverify v${VERSION}`);
|
|
423
|
-
process.exit(
|
|
840
|
+
process.exit(EXIT_OK);
|
|
424
841
|
}
|
|
425
842
|
if (args.help || !args.command) {
|
|
426
843
|
printHelp();
|
|
427
|
-
process.exit(
|
|
844
|
+
process.exit(EXIT_OK);
|
|
428
845
|
}
|
|
429
846
|
if (args.command === "scan") {
|
|
430
|
-
await
|
|
847
|
+
if (args.profiles.length > 0) await runProfile(args);
|
|
848
|
+
else await runScan(args);
|
|
431
849
|
} else {
|
|
432
850
|
console.error(`
|
|
433
851
|
${c.red}Unknown command: ${args.command}${c.reset}`);
|
|
434
852
|
console.error(`Run ${c.cyan}agentverify --help${c.reset} for usage
|
|
435
853
|
`);
|
|
436
|
-
process.exit(
|
|
854
|
+
process.exit(EXIT_EXECUTION_ERROR);
|
|
437
855
|
}
|
|
438
856
|
}
|
|
439
857
|
main().catch((err) => {
|
|
440
858
|
console.error(`
|
|
441
859
|
${c.red}Fatal error:${c.reset}`, err instanceof Error ? err.message : err);
|
|
442
|
-
process.exit(
|
|
860
|
+
process.exit(EXIT_EXECUTION_ERROR);
|
|
443
861
|
});
|