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.mjs
CHANGED
|
@@ -1,14 +1,269 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
scan
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-XVA2M2HS.mjs";
|
|
5
5
|
|
|
6
6
|
// src/cli.ts
|
|
7
|
-
import { readFileSync, readdirSync, statSync } 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";
|
|
263
|
+
var EXIT_OK = 0;
|
|
264
|
+
var EXIT_NOT_VERIFIED = 1;
|
|
265
|
+
var EXIT_NOT_ASSESSED = 2;
|
|
266
|
+
var EXIT_EXECUTION_ERROR = 3;
|
|
12
267
|
var c = {
|
|
13
268
|
reset: "\x1B[0m",
|
|
14
269
|
bold: "\x1B[1m",
|
|
@@ -22,12 +277,13 @@ var c = {
|
|
|
22
277
|
};
|
|
23
278
|
var write = (value = "") => process.stdout.write(`${value}
|
|
24
279
|
`);
|
|
25
|
-
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"]);
|
|
281
|
+
var MAX_SCAN_FILE_BYTES = 5 * 1024 * 1024;
|
|
26
282
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", "vendor", "coverage", ".cache"]);
|
|
27
283
|
function collectFiles(dir) {
|
|
28
284
|
const files = [];
|
|
29
285
|
try {
|
|
30
|
-
const entries =
|
|
286
|
+
const entries = readdirSync2(dir);
|
|
31
287
|
for (const entry of entries) {
|
|
32
288
|
if (SKIP_DIRS.has(entry)) continue;
|
|
33
289
|
const fullPath = join(dir, entry);
|
|
@@ -54,7 +310,10 @@ function parseArgs(argv) {
|
|
|
54
310
|
ci: false,
|
|
55
311
|
allowNotAssessed: false,
|
|
56
312
|
help: false,
|
|
57
|
-
version: false
|
|
313
|
+
version: false,
|
|
314
|
+
policy: "",
|
|
315
|
+
summaryFile: "",
|
|
316
|
+
profiles: []
|
|
58
317
|
};
|
|
59
318
|
if (result.command === "--help" || result.command === "-h") {
|
|
60
319
|
result.command = "";
|
|
@@ -72,6 +331,10 @@ function parseArgs(argv) {
|
|
|
72
331
|
else if (arg === "--markdown") result.markdown = true;
|
|
73
332
|
else if (arg === "--ci") result.ci = true;
|
|
74
333
|
else if (arg === "--allow-not-assessed") result.allowNotAssessed = true;
|
|
334
|
+
else if (arg === "--policy") result.policy = args[++i] ?? "";
|
|
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));
|
|
75
338
|
else if (arg === "--help" || arg === "-h") result.help = true;
|
|
76
339
|
else if (arg === "--version" || arg === "-v") result.version = true;
|
|
77
340
|
else if (!arg.startsWith("-")) result.dir = arg;
|
|
@@ -99,6 +362,15 @@ ${c.bold}Options:${c.reset}
|
|
|
99
362
|
--ci CI mode: concise output and non-zero exit for NOT VERIFIED
|
|
100
363
|
--allow-not-assessed
|
|
101
364
|
In CI mode, do not fail only because content is NOT_ASSESSED
|
|
365
|
+
--policy <id> Evaluate a built-in policy profile against this scan's evidence
|
|
366
|
+
in addition to the scanner's own verdict (e.g. financial-agent)
|
|
367
|
+
--summary-file <path>
|
|
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.
|
|
102
374
|
--version, -v Print version
|
|
103
375
|
--help, -h Show this help message
|
|
104
376
|
|
|
@@ -118,6 +390,20 @@ ${c.bold}Examples:${c.reset}
|
|
|
118
390
|
${c.gray}# Markdown summary for pull requests${c.reset}
|
|
119
391
|
agentverify scan . --key av_your_key --markdown
|
|
120
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
|
+
|
|
396
|
+
${c.bold}Exit codes (--ci):${c.reset}
|
|
397
|
+
0 All scanned files verified
|
|
398
|
+
1 NOT VERIFIED \u2014 a security/verification check failed
|
|
399
|
+
2 NOT ASSESSED \u2014 insufficient evidence for a verdict (see --allow-not-assessed)
|
|
400
|
+
3 Execution error \u2014 the scan itself could not run (bad key, network, missing file)
|
|
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
|
+
|
|
121
407
|
${c.bold}Get your API key:${c.reset}
|
|
122
408
|
https://aimodularity.com/agentverify/dashboard
|
|
123
409
|
|
|
@@ -139,6 +425,16 @@ function getFixFirst(result) {
|
|
|
139
425
|
function getRelevantThreatCategories(result) {
|
|
140
426
|
return (result.threatCategories ?? []).filter((item) => item.status !== "not_assessed").slice(0, 6).map((item) => `${item.label} (${item.status.replace("_", " ")})`);
|
|
141
427
|
}
|
|
428
|
+
function criticalCount(result) {
|
|
429
|
+
return result.findings.filter((f) => f.severity === "critical").length;
|
|
430
|
+
}
|
|
431
|
+
function highCount(result) {
|
|
432
|
+
return result.findings.filter((f) => f.severity === "high").length;
|
|
433
|
+
}
|
|
434
|
+
function fingerprintPreview(result) {
|
|
435
|
+
const hash = result.artifactFingerprint?.artifactHash;
|
|
436
|
+
return hash ? `${hash.slice(0, 12)}\u2026` : null;
|
|
437
|
+
}
|
|
142
438
|
function printDetailedSummary(result, fileName) {
|
|
143
439
|
const verified = result.verdict === "VERIFIED";
|
|
144
440
|
const verdictColor = verified ? c.green : result.verdict === "NOT_ASSESSED" ? c.yellow : c.red;
|
|
@@ -149,6 +445,10 @@ ${c.bold}${c.cyan}Agent Verify${c.reset}`);
|
|
|
149
445
|
write(`${c.bold}Verdict:${c.reset} ${verdictColor}${result.verdict === "NOT_VERIFIED" ? "NOT VERIFIED" : result.verdict.replace("_", " ")}${c.reset}`);
|
|
150
446
|
write(`${c.bold}Risk level:${c.reset} ${result.riskLevel}`);
|
|
151
447
|
write(`${c.bold}Confidence:${c.reset} ${result.confidence}/100`);
|
|
448
|
+
write(`${c.bold}Critical / High findings:${c.reset} ${criticalCount(result)} / ${highCount(result)}`);
|
|
449
|
+
if (result.metadata?.scannerVersion) write(`${c.bold}Ruleset version:${c.reset} ${result.metadata.scannerVersion}`);
|
|
450
|
+
if (fingerprintPreview(result)) write(`${c.bold}Artifact fingerprint:${c.reset} ${fingerprintPreview(result)}`);
|
|
451
|
+
if (result.policyProfile) write(`${c.bold}Policy (${result.policyProfile}):${c.reset} ${result.policyResult === "PASS" ? `${c.green}PASS${c.reset}` : `${c.red}FAIL${c.reset}`}`);
|
|
152
452
|
write(`${c.bold}Top blocker:${c.reset} ${getTopBlocker(result)}`);
|
|
153
453
|
write(`
|
|
154
454
|
${c.bold}Fix first:${c.reset}`);
|
|
@@ -157,6 +457,8 @@ ${c.bold}Fix first:${c.reset}`);
|
|
|
157
457
|
else write(` No blocking fixes detected. Re-scan after any material change.`);
|
|
158
458
|
if (result.reportInsights?.nextAction) write(`
|
|
159
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}`);
|
|
160
462
|
const threats = getRelevantThreatCategories(result);
|
|
161
463
|
if (threats.length) {
|
|
162
464
|
write(`
|
|
@@ -175,7 +477,11 @@ function markdownSummary(results, summary) {
|
|
|
175
477
|
lines.push(`## ${item.file}`, "", `Error: ${item.error ?? "Unknown error"}`, "");
|
|
176
478
|
continue;
|
|
177
479
|
}
|
|
178
|
-
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}`, `
|
|
480
|
+
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)}`);
|
|
481
|
+
if (item.result.metadata?.scannerVersion) lines.push(`Ruleset version: ${item.result.metadata.scannerVersion}`);
|
|
482
|
+
if (fingerprintPreview(item.result)) lines.push(`Artifact fingerprint: \`${fingerprintPreview(item.result)}\``);
|
|
483
|
+
if (item.result.policyProfile) lines.push(`Policy (${item.result.policyProfile}): ${item.result.policyResult}`);
|
|
484
|
+
lines.push(`Top blocker: ${getTopBlocker(item.result)}`, "");
|
|
179
485
|
const fixes = getFixFirst(item.result);
|
|
180
486
|
if (fixes.length) lines.push("Fix first:", ...fixes.map((fix, index) => `${index + 1}. ${fix}`), "");
|
|
181
487
|
const threats = getRelevantThreatCategories(item.result);
|
|
@@ -185,6 +491,24 @@ function markdownSummary(results, summary) {
|
|
|
185
491
|
lines.push(`A2SPA docs: ${A2SPA_DOCS_URL}`);
|
|
186
492
|
return lines.join("\n");
|
|
187
493
|
}
|
|
494
|
+
function writeGithubJobSummary(results, summary) {
|
|
495
|
+
const summaryPath = process.env.GITHUB_STEP_SUMMARY;
|
|
496
|
+
if (!summaryPath) return;
|
|
497
|
+
try {
|
|
498
|
+
appendFileSync(summaryPath, `${markdownSummary(results, summary)}
|
|
499
|
+
`);
|
|
500
|
+
} catch (e) {
|
|
501
|
+
console.error(`Warning: could not write GitHub job summary: ${e instanceof Error ? e.message : e}`);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
function writeSummaryFile(path, results, summary) {
|
|
505
|
+
if (!path) return;
|
|
506
|
+
try {
|
|
507
|
+
writeFileSync(path, JSON.stringify({ results, summary }, null, 2));
|
|
508
|
+
} catch (e) {
|
|
509
|
+
console.error(`Warning: could not write summary file: ${e instanceof Error ? e.message : e}`);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
188
512
|
function printVerdict(result, fileName) {
|
|
189
513
|
const verified = result.verdict === "VERIFIED";
|
|
190
514
|
const notAssessed = result.verdict === "NOT_ASSESSED";
|
|
@@ -207,7 +531,7 @@ ${c.red}Error:${c.reset} API key required
|
|
|
207
531
|
console.error(` Get your key at https://aimodularity.com/agentverify/dashboard/`);
|
|
208
532
|
console.error(` Example: AGENTVERIFY_API_KEY=av_your_key agentverify scan .
|
|
209
533
|
`);
|
|
210
|
-
process.exit(
|
|
534
|
+
process.exit(EXIT_EXECUTION_ERROR);
|
|
211
535
|
}
|
|
212
536
|
let files = [];
|
|
213
537
|
if (args.file) {
|
|
@@ -223,7 +547,7 @@ ${c.bold}Agent Verify${c.reset} ${c.gray}v${VERSION}${c.reset}`);
|
|
|
223
547
|
files = collectFiles(dir);
|
|
224
548
|
if (files.length === 0) {
|
|
225
549
|
write(`${c.yellow}No agent files found in ${dir}${c.reset}`);
|
|
226
|
-
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}
|
|
227
551
|
`);
|
|
228
552
|
process.exit(0);
|
|
229
553
|
}
|
|
@@ -238,10 +562,14 @@ ${c.bold}Agent Verify${c.reset} ${c.gray}v${VERSION}${c.reset}`);
|
|
|
238
562
|
const batch = files.slice(i, i + CONCURRENCY);
|
|
239
563
|
const batchResults = await Promise.allSettled(
|
|
240
564
|
batch.map(async (filePath) => {
|
|
241
|
-
const
|
|
565
|
+
const fileStat = statSync(filePath);
|
|
566
|
+
if (fileStat.size > MAX_SCAN_FILE_BYTES) {
|
|
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`);
|
|
568
|
+
}
|
|
569
|
+
const content = readFileSync2(filePath, "utf-8");
|
|
242
570
|
const relPath = relative(args.dir === "." ? process.cwd() : args.dir, filePath);
|
|
243
571
|
const result = await scan(
|
|
244
|
-
{ content, fileName: relPath },
|
|
572
|
+
{ content, fileName: relPath, policyId: args.policy || void 0 },
|
|
245
573
|
{ apiKey: args.key, apiUrl: API_URL }
|
|
246
574
|
);
|
|
247
575
|
return { filePath, relPath, result };
|
|
@@ -272,21 +600,23 @@ ${c.bold}Agent Verify${c.reset} ${c.gray}v${VERSION}${c.reset}`);
|
|
|
272
600
|
}
|
|
273
601
|
}
|
|
274
602
|
const summary = { total: files.length, verified, notVerified, notAssessed, errors };
|
|
603
|
+
writeGithubJobSummary(jsonResults, summary);
|
|
604
|
+
writeSummaryFile(args.summaryFile, jsonResults, summary);
|
|
275
605
|
if (args.json) {
|
|
276
606
|
write(JSON.stringify({ results: jsonResults, summary }, null, 2));
|
|
277
|
-
if (errors > 0) process.exit(
|
|
607
|
+
if (errors > 0) process.exit(EXIT_EXECUTION_ERROR);
|
|
278
608
|
if (args.ci) {
|
|
279
|
-
if (notVerified > 0) process.exit(
|
|
280
|
-
if (notAssessed > 0 && !args.allowNotAssessed) process.exit(
|
|
609
|
+
if (notVerified > 0) process.exit(EXIT_NOT_VERIFIED);
|
|
610
|
+
if (notAssessed > 0 && !args.allowNotAssessed) process.exit(EXIT_NOT_ASSESSED);
|
|
281
611
|
}
|
|
282
612
|
return;
|
|
283
613
|
}
|
|
284
614
|
if (args.markdown) {
|
|
285
615
|
write(markdownSummary(jsonResults, summary));
|
|
286
|
-
if (errors > 0) process.exit(
|
|
616
|
+
if (errors > 0) process.exit(EXIT_EXECUTION_ERROR);
|
|
287
617
|
if (args.ci) {
|
|
288
|
-
if (notVerified > 0) process.exit(
|
|
289
|
-
if (notAssessed > 0 && !args.allowNotAssessed) process.exit(
|
|
618
|
+
if (notVerified > 0) process.exit(EXIT_NOT_VERIFIED);
|
|
619
|
+
if (notAssessed > 0 && !args.allowNotAssessed) process.exit(EXIT_NOT_ASSESSED);
|
|
290
620
|
}
|
|
291
621
|
return;
|
|
292
622
|
}
|
|
@@ -306,34 +636,118 @@ ${c.gray}Saved CLI reports appear in your dashboard: https://aimodularity.com/ag
|
|
|
306
636
|
${c.gray}No dashboard report was saved. Check API key access and retry if you need dashboard sync.${c.reset}`);
|
|
307
637
|
write(`${c.gray}A2SPA docs: ${A2SPA_DOCS_URL}${c.reset}
|
|
308
638
|
`);
|
|
309
|
-
if (errors > 0) process.exit(
|
|
639
|
+
if (errors > 0) process.exit(EXIT_EXECUTION_ERROR);
|
|
310
640
|
if (args.ci) {
|
|
311
|
-
if (notVerified > 0) process.exit(
|
|
312
|
-
if (notAssessed > 0 && !args.allowNotAssessed) process.exit(
|
|
641
|
+
if (notVerified > 0) process.exit(EXIT_NOT_VERIFIED);
|
|
642
|
+
if (notAssessed > 0 && !args.allowNotAssessed) process.exit(EXIT_NOT_ASSESSED);
|
|
643
|
+
}
|
|
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.`);
|
|
313
725
|
}
|
|
726
|
+
for (const line of formatAssessment(body, built.skipped)) write(line);
|
|
314
727
|
}
|
|
315
728
|
async function main() {
|
|
316
729
|
const args = parseArgs(process.argv);
|
|
317
730
|
if (args.version) {
|
|
318
731
|
write(`agentverify v${VERSION}`);
|
|
319
|
-
process.exit(
|
|
732
|
+
process.exit(EXIT_OK);
|
|
320
733
|
}
|
|
321
734
|
if (args.help || !args.command) {
|
|
322
735
|
printHelp();
|
|
323
|
-
process.exit(
|
|
736
|
+
process.exit(EXIT_OK);
|
|
324
737
|
}
|
|
325
738
|
if (args.command === "scan") {
|
|
326
|
-
await
|
|
739
|
+
if (args.profiles.length > 0) await runProfile(args);
|
|
740
|
+
else await runScan(args);
|
|
327
741
|
} else {
|
|
328
742
|
console.error(`
|
|
329
743
|
${c.red}Unknown command: ${args.command}${c.reset}`);
|
|
330
744
|
console.error(`Run ${c.cyan}agentverify --help${c.reset} for usage
|
|
331
745
|
`);
|
|
332
|
-
process.exit(
|
|
746
|
+
process.exit(EXIT_EXECUTION_ERROR);
|
|
333
747
|
}
|
|
334
748
|
}
|
|
335
749
|
main().catch((err) => {
|
|
336
750
|
console.error(`
|
|
337
751
|
${c.red}Fatal error:${c.reset}`, err instanceof Error ? err.message : err);
|
|
338
|
-
process.exit(
|
|
752
|
+
process.exit(EXIT_EXECUTION_ERROR);
|
|
339
753
|
});
|
package/dist/sdk.d.mts
CHANGED
|
@@ -2,6 +2,27 @@ interface ScanInput {
|
|
|
2
2
|
content: string;
|
|
3
3
|
fileName?: string;
|
|
4
4
|
platform?: string;
|
|
5
|
+
/** One of the built-in policy profile ids (e.g. "financial-agent") — evaluated server-side against this scan's evidence, in addition to (never instead of) the scanner's own verdict. */
|
|
6
|
+
policyId?: string;
|
|
7
|
+
}
|
|
8
|
+
interface ArtifactFingerprint {
|
|
9
|
+
artifactHash: string;
|
|
10
|
+
artifactHashAlgorithm: string;
|
|
11
|
+
artifactFingerprintVersion: string;
|
|
12
|
+
}
|
|
13
|
+
interface ReportIntegrity {
|
|
14
|
+
reportHash: string;
|
|
15
|
+
algorithm: string;
|
|
16
|
+
schemaVersion: string;
|
|
17
|
+
scannerVersion: string;
|
|
18
|
+
scanId: string;
|
|
19
|
+
timestamp: string;
|
|
20
|
+
}
|
|
21
|
+
interface SignedAttestation {
|
|
22
|
+
payload: Record<string, unknown>;
|
|
23
|
+
signature: string;
|
|
24
|
+
algorithm: string;
|
|
25
|
+
publicKey: Record<string, unknown>;
|
|
5
26
|
}
|
|
6
27
|
type Verdict = 'VERIFIED' | 'NOT_VERIFIED' | 'NOT_ASSESSED';
|
|
7
28
|
type RiskLevel = 'Low Risk' | 'Moderate Risk' | 'High Risk';
|
|
@@ -31,6 +52,8 @@ interface Finding {
|
|
|
31
52
|
recommendedFix: string;
|
|
32
53
|
fixCode?: string;
|
|
33
54
|
quickFix?: string;
|
|
55
|
+
/** Set by the API when this finding has a fix your plan does not include (Pro). */
|
|
56
|
+
fixLocked?: boolean;
|
|
34
57
|
compliance?: {
|
|
35
58
|
owasp?: string[];
|
|
36
59
|
nist?: string[];
|
|
@@ -103,6 +126,11 @@ interface ScanResult {
|
|
|
103
126
|
};
|
|
104
127
|
saved: boolean;
|
|
105
128
|
reportUrl?: string | null;
|
|
129
|
+
artifactFingerprint?: ArtifactFingerprint;
|
|
130
|
+
reportIntegrity?: ReportIntegrity;
|
|
131
|
+
policyProfile?: string | null;
|
|
132
|
+
policyResult?: string | null;
|
|
133
|
+
attestation?: SignedAttestation | null;
|
|
106
134
|
}
|
|
107
135
|
interface ScanOptions {
|
|
108
136
|
apiKey: string;
|