@zksecurity/zkao-cli 0.5.1 → 0.11.0-next.3
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/README.md +1 -0
- package/dist/index.js +383 -115
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -14,6 +14,7 @@ npx @zksecurity/zkao-cli --help
|
|
|
14
14
|
zkao login # browser approval; saves credentials
|
|
15
15
|
zkao repos
|
|
16
16
|
zkao scans launch --repo <repoId> --budget 500 --preset <ref>
|
|
17
|
+
zkao scans launch --repo <repoId> --preset "builtin:Diff Scan" --base main --commit <sha>
|
|
17
18
|
zkao scans wait <scanId> # block until the scan finishes
|
|
18
19
|
zkao scans cancel <scanId> # cancel a running or queued scan
|
|
19
20
|
zkao findings list --scan <scanId>
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,8 @@ import { createRequire } from "module";
|
|
|
6
6
|
import { Command } from "commander";
|
|
7
7
|
import {
|
|
8
8
|
ZkaoApiError,
|
|
9
|
-
ZkaoClient
|
|
9
|
+
ZkaoClient,
|
|
10
|
+
getTokenInfo
|
|
10
11
|
} from "@zksecurity/zkao-sdk";
|
|
11
12
|
|
|
12
13
|
// src/config.ts
|
|
@@ -39,18 +40,93 @@ function readFile() {
|
|
|
39
40
|
);
|
|
40
41
|
}
|
|
41
42
|
}
|
|
42
|
-
function
|
|
43
|
-
let
|
|
43
|
+
function readFileForWrite() {
|
|
44
|
+
let file;
|
|
44
45
|
try {
|
|
45
|
-
|
|
46
|
+
file = readFile();
|
|
46
47
|
} catch {
|
|
47
48
|
console.error(`zkao: replacing unparseable config at ${CONFIG_PATH}`);
|
|
48
|
-
|
|
49
|
+
return {};
|
|
50
|
+
}
|
|
51
|
+
if (file.projectId && file.token && !file.projects?.[file.projectId]) {
|
|
52
|
+
file.projects = {
|
|
53
|
+
...file.projects,
|
|
54
|
+
[file.projectId]: stripUndefinedProject({
|
|
55
|
+
token: file.token,
|
|
56
|
+
baseUrl: file.baseUrl
|
|
57
|
+
})
|
|
58
|
+
};
|
|
49
59
|
}
|
|
50
|
-
|
|
51
|
-
|
|
60
|
+
return file;
|
|
61
|
+
}
|
|
62
|
+
function writeFile(file) {
|
|
63
|
+
writeSecretFile(CONFIG_PATH, `${JSON.stringify(file, null, 2)}
|
|
52
64
|
`);
|
|
53
|
-
|
|
65
|
+
}
|
|
66
|
+
function activate(file, projectId, saved) {
|
|
67
|
+
file.projectId = projectId;
|
|
68
|
+
file.token = saved.token;
|
|
69
|
+
if (saved.baseUrl === void 0) {
|
|
70
|
+
delete file.baseUrl;
|
|
71
|
+
} else {
|
|
72
|
+
file.baseUrl = saved.baseUrl;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function writeConfig(update) {
|
|
76
|
+
const file = readFileForWrite();
|
|
77
|
+
const projectId = update.projectId ?? file.projectId;
|
|
78
|
+
const saved = update.projectId ? file.projects?.[update.projectId] : void 0;
|
|
79
|
+
if (update.token === void 0 && update.projectId !== void 0 && saved) {
|
|
80
|
+
activate(file, update.projectId, {
|
|
81
|
+
...saved,
|
|
82
|
+
baseUrl: update.baseUrl ?? saved.baseUrl
|
|
83
|
+
});
|
|
84
|
+
} else {
|
|
85
|
+
const switching = update.projectId !== void 0 && update.projectId !== file.projectId;
|
|
86
|
+
Object.assign(file, stripUndefined(update));
|
|
87
|
+
if (switching && update.token === void 0) {
|
|
88
|
+
delete file.token;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (projectId && file.token && file.projectId === projectId) {
|
|
92
|
+
file.projects = {
|
|
93
|
+
...file.projects,
|
|
94
|
+
[projectId]: {
|
|
95
|
+
...file.projects?.[projectId],
|
|
96
|
+
token: file.token,
|
|
97
|
+
baseUrl: file.baseUrl
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
writeFile(file);
|
|
102
|
+
return { token: file.token, projectId: file.projectId, baseUrl: file.baseUrl };
|
|
103
|
+
}
|
|
104
|
+
function saveProjectCredentials(projectId, saved) {
|
|
105
|
+
const file = readFileForWrite();
|
|
106
|
+
file.projects = { ...file.projects, [projectId]: stripUndefinedProject(saved) };
|
|
107
|
+
activate(file, projectId, saved);
|
|
108
|
+
writeFile(file);
|
|
109
|
+
}
|
|
110
|
+
function useProject(projectId) {
|
|
111
|
+
const file = readFileForWrite();
|
|
112
|
+
const saved = file.projects?.[projectId];
|
|
113
|
+
if (!saved) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
`No saved credentials for project ${projectId}. Run \`zkao login --project ${projectId}\`.`
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
activate(file, projectId, saved);
|
|
119
|
+
writeFile(file);
|
|
120
|
+
return saved;
|
|
121
|
+
}
|
|
122
|
+
function savedProjects() {
|
|
123
|
+
const file = readFile();
|
|
124
|
+
return { activeProjectId: file.projectId, projects: file.projects ?? {} };
|
|
125
|
+
}
|
|
126
|
+
function stripUndefinedProject(saved) {
|
|
127
|
+
return Object.fromEntries(
|
|
128
|
+
Object.entries(saved).filter(([, v]) => v !== void 0)
|
|
129
|
+
);
|
|
54
130
|
}
|
|
55
131
|
function writeSecretFile(path, contents) {
|
|
56
132
|
mkdirSync(dirname(path), { recursive: true, mode: 448 });
|
|
@@ -67,50 +143,91 @@ function stripUndefined(obj) {
|
|
|
67
143
|
}
|
|
68
144
|
function resolveConfig(flags) {
|
|
69
145
|
const file = readFile();
|
|
146
|
+
const projectId = flags.projectId ?? process.env.ZKAO_PROJECT_ID ?? file.projectId;
|
|
147
|
+
const saved = projectId ? file.projects?.[projectId] : void 0;
|
|
148
|
+
const activeMatches = file.projectId === void 0 || file.projectId === projectId;
|
|
70
149
|
return {
|
|
71
|
-
token: flags.token ?? process.env.ZKAO_API_TOKEN ?? file.token,
|
|
72
|
-
projectId
|
|
73
|
-
baseUrl: flags.baseUrl ?? resolveBaseUrlFromEnv() ??
|
|
150
|
+
token: flags.token ?? process.env.ZKAO_API_TOKEN ?? saved?.token ?? (activeMatches ? file.token : void 0),
|
|
151
|
+
projectId,
|
|
152
|
+
baseUrl: flags.baseUrl ?? resolveBaseUrlFromEnv() ?? // The top-level base URL is the active project's; a saved project
|
|
153
|
+
// carries its own (none means production).
|
|
154
|
+
(saved ? saved.baseUrl : file.baseUrl)
|
|
74
155
|
};
|
|
75
156
|
}
|
|
76
157
|
var PENDING_PATH = join(homedir(), ".zkao", "pending-login.json");
|
|
77
|
-
function
|
|
78
|
-
|
|
79
|
-
|
|
158
|
+
function isPendingLogin(value) {
|
|
159
|
+
if (typeof value !== "object" || value === null) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
const v = value;
|
|
163
|
+
return typeof v.deviceCode === "string" && typeof v.origin === "string" && typeof v.interval === "number" && typeof v.expiresAtMs === "number";
|
|
80
164
|
}
|
|
81
|
-
function
|
|
165
|
+
function readPendingLogins() {
|
|
82
166
|
let raw;
|
|
83
167
|
try {
|
|
84
168
|
raw = readFileSync(PENDING_PATH, "utf8");
|
|
85
169
|
} catch (err) {
|
|
86
170
|
if (err.code === "ENOENT") {
|
|
87
|
-
return
|
|
171
|
+
return [];
|
|
88
172
|
}
|
|
89
173
|
throw err;
|
|
90
174
|
}
|
|
175
|
+
let parsed;
|
|
91
176
|
try {
|
|
92
|
-
|
|
177
|
+
parsed = JSON.parse(raw);
|
|
93
178
|
} catch {
|
|
94
179
|
console.error(
|
|
95
180
|
`zkao: ignoring unparseable pending login at ${PENDING_PATH}`
|
|
96
181
|
);
|
|
97
|
-
return
|
|
182
|
+
return [];
|
|
98
183
|
}
|
|
184
|
+
if (isPendingLogin(parsed)) {
|
|
185
|
+
return [parsed];
|
|
186
|
+
}
|
|
187
|
+
const logins = parsed?.logins;
|
|
188
|
+
return logins && typeof logins === "object" ? Object.values(logins).filter(isPendingLogin) : [];
|
|
99
189
|
}
|
|
100
|
-
function
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
190
|
+
function writePendingLogins(logins) {
|
|
191
|
+
if (logins.length === 0) {
|
|
192
|
+
try {
|
|
193
|
+
unlinkSync(PENDING_PATH);
|
|
194
|
+
} catch (err) {
|
|
195
|
+
if (err.code !== "ENOENT") {
|
|
196
|
+
throw err;
|
|
197
|
+
}
|
|
106
198
|
}
|
|
199
|
+
return;
|
|
107
200
|
}
|
|
201
|
+
const file = {
|
|
202
|
+
logins: Object.fromEntries(logins.map((l) => [l.deviceCode, l]))
|
|
203
|
+
};
|
|
204
|
+
writeSecretFile(PENDING_PATH, `${JSON.stringify(file, null, 2)}
|
|
205
|
+
`);
|
|
206
|
+
}
|
|
207
|
+
function addPendingLogin(pending) {
|
|
208
|
+
const now = Date.now();
|
|
209
|
+
writePendingLogins([
|
|
210
|
+
...readPendingLogins().filter(
|
|
211
|
+
(l) => l.expiresAtMs > now && l.deviceCode !== pending.deviceCode
|
|
212
|
+
),
|
|
213
|
+
pending
|
|
214
|
+
]);
|
|
215
|
+
}
|
|
216
|
+
function removePendingLogins(deviceCodes) {
|
|
217
|
+
const now = Date.now();
|
|
218
|
+
const drop = new Set(deviceCodes);
|
|
219
|
+
writePendingLogins(
|
|
220
|
+
readPendingLogins().filter(
|
|
221
|
+
(l) => l.expiresAtMs > now && !drop.has(l.deviceCode)
|
|
222
|
+
)
|
|
223
|
+
);
|
|
108
224
|
}
|
|
109
225
|
|
|
110
226
|
// src/login.ts
|
|
111
227
|
import { spawn } from "child_process";
|
|
112
228
|
import { hostname } from "os";
|
|
113
229
|
import { DEFAULT_BASE_URL } from "@zksecurity/zkao-sdk";
|
|
230
|
+
var DEFAULT_LOGIN_SCOPES = ["read", "scans:launch", "findings:write"];
|
|
114
231
|
function appOrigin(baseUrl) {
|
|
115
232
|
const url = new URL(baseUrl);
|
|
116
233
|
const prefix = url.pathname.replace(/\/+$/, "").replace(/\/api\/v\d+$/, "");
|
|
@@ -157,15 +274,39 @@ Approve only if you started this login.
|
|
|
157
274
|
`
|
|
158
275
|
);
|
|
159
276
|
}
|
|
277
|
+
function describeProject(projectId, names) {
|
|
278
|
+
if (!names.name) {
|
|
279
|
+
return projectId;
|
|
280
|
+
}
|
|
281
|
+
const org = names.organization ? ` of organization "${names.organization}"` : "";
|
|
282
|
+
return `"${names.name}"${org} (${projectId})`;
|
|
283
|
+
}
|
|
160
284
|
function persistCredentials(poll, baseUrl) {
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
285
|
+
const tokens = poll.tokens?.length ? poll.tokens : [poll];
|
|
286
|
+
for (const issued of [...tokens].reverse()) {
|
|
287
|
+
saveProjectCredentials(issued.projectId, {
|
|
288
|
+
token: issued.token,
|
|
289
|
+
baseUrl,
|
|
290
|
+
name: issued.projectName,
|
|
291
|
+
organization: issued.organizationName
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
const describe = (issued) => describeProject(issued.projectId, {
|
|
295
|
+
name: issued.projectName,
|
|
296
|
+
organization: issued.organizationName
|
|
165
297
|
});
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
298
|
+
if (tokens.length === 1) {
|
|
299
|
+
console.log(`Authorized for project ${describe(poll)}.`);
|
|
300
|
+
} else {
|
|
301
|
+
console.log(`Authorized for ${tokens.length} projects:`);
|
|
302
|
+
for (const issued of tokens) {
|
|
303
|
+
console.log(` ${describe(issued)}`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
console.log(`Active project: ${describe(poll)}.`);
|
|
307
|
+
console.log(
|
|
308
|
+
"Credentials for other projects are kept. `zkao config show` lists them; `zkao config use <projectId>` switches."
|
|
309
|
+
);
|
|
169
310
|
}
|
|
170
311
|
async function login(flags) {
|
|
171
312
|
const cfg = resolveConfig({
|
|
@@ -175,98 +316,130 @@ async function login(flags) {
|
|
|
175
316
|
});
|
|
176
317
|
const apiBase = cfg.baseUrl ?? DEFAULT_BASE_URL;
|
|
177
318
|
const origin = appOrigin(apiBase);
|
|
319
|
+
const project = flags.project ?? process.env.ZKAO_PROJECT_ID;
|
|
178
320
|
const start = await postJson(`${origin}/api/auth/device`, {
|
|
179
|
-
label: hostname()
|
|
321
|
+
label: hostname(),
|
|
322
|
+
scopes: flags.scopes ?? DEFAULT_LOGIN_SCOPES,
|
|
323
|
+
...project ? { project } : {}
|
|
180
324
|
});
|
|
181
325
|
printInstructions(start);
|
|
182
326
|
if (!flags.noBrowser) {
|
|
183
327
|
openBrowser(start.verificationUriComplete);
|
|
184
328
|
}
|
|
185
|
-
|
|
329
|
+
const pending = {
|
|
186
330
|
deviceCode: start.deviceCode,
|
|
331
|
+
userCode: start.userCode,
|
|
187
332
|
origin,
|
|
188
333
|
interval: Math.max(1, start.interval),
|
|
189
334
|
expiresAtMs: Date.now() + start.expiresIn * 1e3,
|
|
190
335
|
baseUrl: cfg.baseUrl
|
|
191
|
-
}
|
|
336
|
+
};
|
|
337
|
+
addPendingLogin(pending);
|
|
192
338
|
if (flags.wait === false) {
|
|
193
339
|
console.log(
|
|
194
340
|
"Once you approve in the browser, run `zkao login --resume` to finish.\n"
|
|
195
341
|
);
|
|
196
342
|
return;
|
|
197
343
|
}
|
|
198
|
-
await pollUntilDone(
|
|
199
|
-
origin,
|
|
200
|
-
deviceCode: start.deviceCode,
|
|
201
|
-
intervalMs: Math.max(1, start.interval) * 1e3,
|
|
202
|
-
deadlineMs: Date.now() + start.expiresIn * 1e3,
|
|
203
|
-
baseUrl: cfg.baseUrl
|
|
204
|
-
});
|
|
344
|
+
await pollUntilDone([pending], pending.expiresAtMs);
|
|
205
345
|
}
|
|
206
346
|
async function resumeLogin(flags) {
|
|
207
|
-
const
|
|
208
|
-
|
|
347
|
+
const all = readPendingLogins();
|
|
348
|
+
const now = Date.now();
|
|
349
|
+
const live = all.filter((l) => l.expiresAtMs > now);
|
|
350
|
+
if (live.length < all.length) {
|
|
351
|
+
removePendingLogins([]);
|
|
352
|
+
}
|
|
353
|
+
if (live.length === 0) {
|
|
209
354
|
throw new Error(
|
|
210
|
-
"No pending login. Run `zkao login --no-wait` to start one first."
|
|
355
|
+
all.length > 0 ? "The login request expired. Run `zkao login` again." : "No pending login. Run `zkao login --no-wait` to start one first."
|
|
211
356
|
);
|
|
212
357
|
}
|
|
213
|
-
if (Date.now() >= pending.expiresAtMs) {
|
|
214
|
-
clearPendingLogin();
|
|
215
|
-
throw new Error("The login request expired. Run `zkao login` again.");
|
|
216
|
-
}
|
|
217
358
|
const waitMs = (flags.timeout ?? 0) * 1e3;
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
baseUrl: pending.baseUrl,
|
|
224
|
-
reportStillPending: true
|
|
225
|
-
});
|
|
359
|
+
const lastExpiry = Math.max(...live.map((l) => l.expiresAtMs));
|
|
360
|
+
await pollUntilDone(live, Math.min(lastExpiry, Date.now() + waitMs), true);
|
|
361
|
+
}
|
|
362
|
+
function label(pending) {
|
|
363
|
+
return pending.userCode ? `code ${pending.userCode}` : "an earlier login";
|
|
226
364
|
}
|
|
227
|
-
async function pollUntilDone(
|
|
228
|
-
let
|
|
365
|
+
async function pollUntilDone(logins, deadlineMs, reportStillPending = false) {
|
|
366
|
+
let open = [...logins];
|
|
367
|
+
let intervalMs = Math.min(...open.map((l) => Math.max(1, l.interval))) * 1e3;
|
|
368
|
+
let lastFailure = "The login request expired. Run `zkao login` again.";
|
|
229
369
|
let polled = false;
|
|
230
|
-
|
|
370
|
+
while (open.length > 0) {
|
|
231
371
|
if (polled) {
|
|
232
|
-
const remaining =
|
|
372
|
+
const remaining = deadlineMs - Date.now();
|
|
233
373
|
if (remaining <= 0) {
|
|
234
374
|
break;
|
|
235
375
|
}
|
|
236
376
|
await sleep(Math.min(intervalMs, remaining));
|
|
237
377
|
}
|
|
238
378
|
polled = true;
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
379
|
+
const resolved = [];
|
|
380
|
+
const dropped = [];
|
|
381
|
+
for (const pending of open) {
|
|
382
|
+
if (Date.now() >= pending.expiresAtMs) {
|
|
383
|
+
resolved.push(pending.deviceCode);
|
|
384
|
+
dropped.push(`${label(pending)} expired`);
|
|
385
|
+
lastFailure = "The login request expired. Run `zkao login` again.";
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
const poll = await postJson(
|
|
389
|
+
`${pending.origin}/api/auth/device/token`,
|
|
390
|
+
{ deviceCode: pending.deviceCode }
|
|
391
|
+
);
|
|
392
|
+
switch (poll.status) {
|
|
393
|
+
case "pending":
|
|
394
|
+
break;
|
|
395
|
+
case "slow_down":
|
|
247
396
|
intervalMs += 2e3;
|
|
397
|
+
break;
|
|
398
|
+
case "denied":
|
|
399
|
+
resolved.push(pending.deviceCode);
|
|
400
|
+
dropped.push(`${label(pending)} was denied`);
|
|
401
|
+
lastFailure = "Authorization was denied in the browser.";
|
|
402
|
+
break;
|
|
403
|
+
case "expired":
|
|
404
|
+
resolved.push(pending.deviceCode);
|
|
405
|
+
dropped.push(`${label(pending)} expired`);
|
|
406
|
+
lastFailure = "The login request expired. Run `zkao login` again.";
|
|
407
|
+
break;
|
|
408
|
+
case "approved": {
|
|
409
|
+
removePendingLogins([...resolved, pending.deviceCode]);
|
|
410
|
+
persistCredentials(poll, pending.baseUrl);
|
|
411
|
+
const rest = open.filter(
|
|
412
|
+
(l) => l !== pending && !resolved.includes(l.deviceCode)
|
|
413
|
+
);
|
|
414
|
+
if (rest.length > 0) {
|
|
415
|
+
console.log(
|
|
416
|
+
`Still pending: ${rest.map(label).join(", ")}. Run \`zkao login --resume\` again to finish ${rest.length > 1 ? "them" : "it"}.`
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
return;
|
|
248
420
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
`Unexpected device status: ${JSON.stringify(_exhaustive)}`
|
|
263
|
-
);
|
|
421
|
+
default: {
|
|
422
|
+
const _exhaustive = poll;
|
|
423
|
+
throw new Error(
|
|
424
|
+
`Unexpected device status: ${JSON.stringify(_exhaustive)}`
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
if (resolved.length > 0) {
|
|
430
|
+
removePendingLogins(resolved);
|
|
431
|
+
open = open.filter((l) => !resolved.includes(l.deviceCode));
|
|
432
|
+
if (open.length > 0) {
|
|
433
|
+
console.error(`zkao: dropped pending login: ${dropped.join(", ")}.`);
|
|
264
434
|
}
|
|
265
435
|
}
|
|
266
436
|
}
|
|
267
|
-
if (
|
|
437
|
+
if (open.length === 0) {
|
|
438
|
+
throw new Error(lastFailure);
|
|
439
|
+
}
|
|
440
|
+
if (reportStillPending) {
|
|
268
441
|
console.log(
|
|
269
|
-
"Still waiting for approval. Approve in the browser, then run `zkao login --resume` again."
|
|
442
|
+
open.length > 1 ? `Still waiting for approval of ${open.length} logins (${open.map(label).join(", ")}). Approve in the browser, then run \`zkao login --resume\` again.` : "Still waiting for approval. Approve in the browser, then run `zkao login --resume` again."
|
|
270
443
|
);
|
|
271
444
|
return;
|
|
272
445
|
}
|
|
@@ -280,6 +453,9 @@ var CACHE_PATH = join2(dirname2(configPath()), "update-check.json");
|
|
|
280
453
|
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
281
454
|
var REGISTRY_TIMEOUT_MS = 1500;
|
|
282
455
|
var PACKAGE = "@zksecurity/zkao-cli";
|
|
456
|
+
function channelOf(version2) {
|
|
457
|
+
return version2.includes("-") ? "next" : "latest";
|
|
458
|
+
}
|
|
283
459
|
function debug(message, err) {
|
|
284
460
|
if (process.env.ZKAO_DEBUG) {
|
|
285
461
|
console.error(`zkao: ${message}: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -288,7 +464,7 @@ function debug(message, err) {
|
|
|
288
464
|
function disabled() {
|
|
289
465
|
return Boolean(process.env.ZKAO_NO_UPDATE_CHECK);
|
|
290
466
|
}
|
|
291
|
-
function readCache() {
|
|
467
|
+
function readCache(channel) {
|
|
292
468
|
let raw;
|
|
293
469
|
try {
|
|
294
470
|
raw = readFileSync2(CACHE_PATH, "utf8");
|
|
@@ -300,7 +476,8 @@ function readCache() {
|
|
|
300
476
|
}
|
|
301
477
|
try {
|
|
302
478
|
const parsed = JSON.parse(raw);
|
|
303
|
-
|
|
479
|
+
const valid = typeof parsed.latest === "string" && typeof parsed.checkedAtMs === "number";
|
|
480
|
+
return valid && (parsed.channel ?? "latest") === channel ? parsed : null;
|
|
304
481
|
} catch (err) {
|
|
305
482
|
debug("ignoring an unparseable update cache", err);
|
|
306
483
|
return null;
|
|
@@ -317,47 +494,53 @@ function writeCache(cache) {
|
|
|
317
494
|
}
|
|
318
495
|
function isNewerVersion(latest, current) {
|
|
319
496
|
const parse = (v) => {
|
|
320
|
-
const match = /^(\d+)\.(\d+)\.(\d+)
|
|
321
|
-
return match ?
|
|
497
|
+
const match = /^(\d+)\.(\d+)\.(\d+)(?:-next\.(\d+))?$/.exec(v.trim());
|
|
498
|
+
return match ? {
|
|
499
|
+
core: [Number(match[1]), Number(match[2]), Number(match[3])],
|
|
500
|
+
pre: match[4] === void 0 ? null : Number(match[4])
|
|
501
|
+
} : null;
|
|
322
502
|
};
|
|
323
503
|
const a = parse(latest);
|
|
324
504
|
const b = parse(current);
|
|
325
|
-
if (!(a && b)) {
|
|
505
|
+
if (!(a && b) || a.pre !== null && b.pre === null) {
|
|
326
506
|
return false;
|
|
327
507
|
}
|
|
328
|
-
const [
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
508
|
+
for (const [x, y] of a.core.map((n, i) => [n, b.core[i] ?? 0])) {
|
|
509
|
+
if (x !== y) {
|
|
510
|
+
return x > y;
|
|
511
|
+
}
|
|
332
512
|
}
|
|
333
|
-
if (
|
|
334
|
-
return
|
|
513
|
+
if (a.pre === null || b.pre === null) {
|
|
514
|
+
return a.pre === null && b.pre !== null;
|
|
335
515
|
}
|
|
336
|
-
return
|
|
516
|
+
return a.pre > b.pre;
|
|
337
517
|
}
|
|
338
518
|
function printUpdateNotice(currentVersion) {
|
|
339
519
|
if (disabled()) {
|
|
340
520
|
return;
|
|
341
521
|
}
|
|
342
|
-
const
|
|
522
|
+
const channel = channelOf(currentVersion);
|
|
523
|
+
const cache = readCache(channel);
|
|
343
524
|
if (!(cache && isNewerVersion(cache.latest, currentVersion))) {
|
|
344
525
|
return;
|
|
345
526
|
}
|
|
527
|
+
const spec = channel === "next" ? `${PACKAGE}@next` : PACKAGE;
|
|
346
528
|
console.error(
|
|
347
|
-
`zkao: version ${cache.latest} is available (running ${currentVersion}). Update with \`npm install -g ${
|
|
529
|
+
`zkao: version ${cache.latest} is available (running ${currentVersion}). Update with \`npm install -g ${spec}\`. If you are an agent working from a zkao skill file, update that too: it may describe fewer commands than the API now offers.`
|
|
348
530
|
);
|
|
349
531
|
}
|
|
350
|
-
async function refreshUpdateCache() {
|
|
532
|
+
async function refreshUpdateCache(currentVersion) {
|
|
351
533
|
if (disabled()) {
|
|
352
534
|
return;
|
|
353
535
|
}
|
|
354
|
-
const
|
|
536
|
+
const channel = channelOf(currentVersion);
|
|
537
|
+
const cache = readCache(channel);
|
|
355
538
|
if (cache && Date.now() - cache.checkedAtMs < CHECK_INTERVAL_MS) {
|
|
356
539
|
return;
|
|
357
540
|
}
|
|
358
|
-
const keepPrevious = () => writeCache({ checkedAtMs: Date.now(), latest: cache?.latest ?? "" });
|
|
541
|
+
const keepPrevious = () => writeCache({ checkedAtMs: Date.now(), latest: cache?.latest ?? "", channel });
|
|
359
542
|
try {
|
|
360
|
-
const res = await fetch(`https://registry.npmjs.org/${PACKAGE}
|
|
543
|
+
const res = await fetch(`https://registry.npmjs.org/${PACKAGE}/${channel}`, {
|
|
361
544
|
headers: { accept: "application/json" },
|
|
362
545
|
signal: AbortSignal.timeout(REGISTRY_TIMEOUT_MS)
|
|
363
546
|
});
|
|
@@ -372,7 +555,7 @@ async function refreshUpdateCache() {
|
|
|
372
555
|
keepPrevious();
|
|
373
556
|
return;
|
|
374
557
|
}
|
|
375
|
-
writeCache({ checkedAtMs: Date.now(), latest: body.version });
|
|
558
|
+
writeCache({ checkedAtMs: Date.now(), latest: body.version, channel });
|
|
376
559
|
} catch (err) {
|
|
377
560
|
debug("could not reach the npm registry", err);
|
|
378
561
|
keepPrevious();
|
|
@@ -390,11 +573,16 @@ function fail(message) {
|
|
|
390
573
|
console.error(message);
|
|
391
574
|
process.exit(1);
|
|
392
575
|
}
|
|
393
|
-
function
|
|
576
|
+
function globalConfig() {
|
|
394
577
|
const g = program.opts();
|
|
395
|
-
|
|
578
|
+
return resolveConfig({ token: g.token, projectId: g.project, baseUrl: g.baseUrl });
|
|
579
|
+
}
|
|
580
|
+
function client() {
|
|
581
|
+
const cfg = globalConfig();
|
|
396
582
|
if (!cfg.token) {
|
|
397
|
-
fail(
|
|
583
|
+
fail(
|
|
584
|
+
cfg.projectId ? `No saved credentials for project ${cfg.projectId}. Run \`zkao login --project ${cfg.projectId}\`, or pass --token / set ZKAO_API_TOKEN.` : "No API token. Run `zkao login`, pass --token, or set ZKAO_API_TOKEN."
|
|
585
|
+
);
|
|
398
586
|
}
|
|
399
587
|
if (!cfg.projectId) {
|
|
400
588
|
fail("No project id. Pass --project, set ZKAO_PROJECT_ID, or run `zkao config set --project <id>`.");
|
|
@@ -434,6 +622,9 @@ program.command("login").description(
|
|
|
434
622
|
"--no-wait",
|
|
435
623
|
"start the flow, print the URL/code, and exit; finish with --resume"
|
|
436
624
|
).option("--resume", "poll a login previously started with --no-wait").option(
|
|
625
|
+
"--scope <scopes...>",
|
|
626
|
+
`scopes to request; the approver can change them (default: ${DEFAULT_LOGIN_SCOPES.join(" ")})`
|
|
627
|
+
).option(
|
|
437
628
|
"--timeout <seconds>",
|
|
438
629
|
"with --resume, wait up to this long for approval (default: poll once)",
|
|
439
630
|
toInt
|
|
@@ -449,12 +640,33 @@ program.command("login").description(
|
|
|
449
640
|
project: g.project,
|
|
450
641
|
baseUrl: g.baseUrl,
|
|
451
642
|
noBrowser: opts.browser === false,
|
|
643
|
+
scopes: opts.scope,
|
|
452
644
|
wait: opts.wait !== false
|
|
453
645
|
});
|
|
454
646
|
}
|
|
455
647
|
);
|
|
648
|
+
program.command("whoami").description("Show which project and organization the current token belongs to").action(async () => {
|
|
649
|
+
const cfg = globalConfig();
|
|
650
|
+
if (!cfg.token) {
|
|
651
|
+
fail("No API token. Run `zkao login`, pass --token, or set ZKAO_API_TOKEN.");
|
|
652
|
+
}
|
|
653
|
+
try {
|
|
654
|
+
const info = await getTokenInfo({ token: cfg.token, baseUrl: cfg.baseUrl });
|
|
655
|
+
if (cfg.projectId && cfg.projectId !== info.project.id) {
|
|
656
|
+
console.error(
|
|
657
|
+
`zkao: this token belongs to project ${info.project.id}, not the configured ${cfg.projectId}.`
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
out(info);
|
|
661
|
+
} catch (err) {
|
|
662
|
+
if (err instanceof ZkaoApiError) {
|
|
663
|
+
fail(`Error ${err.status} (${err.code}): ${err.message}`);
|
|
664
|
+
}
|
|
665
|
+
throw err;
|
|
666
|
+
}
|
|
667
|
+
});
|
|
456
668
|
var config = program.command("config").description("Manage stored credentials");
|
|
457
|
-
config.command("set").description(`Persist settings to ${configPath()}`).option("--token <token>", "project API token").option("--project <id>", "project id").option("--base-url <url>", "API base URL").action((opts) => {
|
|
669
|
+
config.command("set").description(`Persist settings to ${configPath()}`).option("--token <token>", "project API token").option("--project <id>", "project id").option("--base-url <url>", "API base URL").action(async (opts) => {
|
|
458
670
|
const g = program.opts();
|
|
459
671
|
const update = {
|
|
460
672
|
token: opts.token ?? g.token,
|
|
@@ -464,17 +676,52 @@ config.command("set").description(`Persist settings to ${configPath()}`).option(
|
|
|
464
676
|
if (update.token === void 0 && update.projectId === void 0 && update.baseUrl === void 0) {
|
|
465
677
|
fail("Nothing to save. Pass --token, --project, or --base-url.");
|
|
466
678
|
}
|
|
679
|
+
if (update.token !== void 0 && update.projectId === void 0) {
|
|
680
|
+
const baseUrl = resolveConfig({ baseUrl: update.baseUrl }).baseUrl;
|
|
681
|
+
try {
|
|
682
|
+
const info = await getTokenInfo({ token: update.token, baseUrl });
|
|
683
|
+
const names = {
|
|
684
|
+
name: info.project.name,
|
|
685
|
+
organization: info.project.organization.name
|
|
686
|
+
};
|
|
687
|
+
saveProjectCredentials(info.project.id, { token: update.token, baseUrl, ...names });
|
|
688
|
+
console.log(
|
|
689
|
+
`Saved to ${configPath()}. Active project: ${describeProject(info.project.id, names)}`
|
|
690
|
+
);
|
|
691
|
+
return;
|
|
692
|
+
} catch (err) {
|
|
693
|
+
console.error(
|
|
694
|
+
`zkao: could not look up the token's project (${err instanceof Error ? err.message : String(err)}); saving it without one.`
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
467
698
|
writeConfig(update);
|
|
468
699
|
console.log(`Saved to ${configPath()}`);
|
|
469
700
|
});
|
|
470
701
|
config.command("show").description("Show the resolved settings (token masked)").action(() => {
|
|
471
702
|
const cfg = resolveConfig({});
|
|
703
|
+
const { activeProjectId, projects } = savedProjects();
|
|
472
704
|
out({
|
|
473
705
|
token: cfg.token ? `${cfg.token.slice(0, 14)}\u2026` : null,
|
|
474
706
|
projectId: cfg.projectId ?? null,
|
|
475
|
-
baseUrl: cfg.baseUrl ?? "https://zkao.io/api/v1 (default)"
|
|
707
|
+
baseUrl: cfg.baseUrl ?? "https://zkao.io/api/v1 (default)",
|
|
708
|
+
savedProjects: Object.entries(projects).map(([id, saved]) => ({
|
|
709
|
+
projectId: id,
|
|
710
|
+
name: saved.name ?? null,
|
|
711
|
+
organization: saved.organization ?? null,
|
|
712
|
+
baseUrl: saved.baseUrl ?? "https://zkao.io/api/v1 (default)",
|
|
713
|
+
active: id === activeProjectId
|
|
714
|
+
}))
|
|
476
715
|
});
|
|
477
716
|
});
|
|
717
|
+
config.command("use <projectId>").description("Switch the active project to one with saved credentials").action((projectId) => {
|
|
718
|
+
try {
|
|
719
|
+
const saved = useProject(projectId);
|
|
720
|
+
console.log(`Active project: ${describeProject(projectId, saved)}`);
|
|
721
|
+
} catch (err) {
|
|
722
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
723
|
+
}
|
|
724
|
+
});
|
|
478
725
|
program.command("repos").description("List repositories in the project").action(() => run((c) => c.listRepositories()));
|
|
479
726
|
program.command("repos:wait <repositoryId>").description("Wait until a repository is ready to scan").option("--timeout <ms>", "give up after this long", toInt).action(
|
|
480
727
|
(repositoryId, opts) => run(
|
|
@@ -542,7 +789,14 @@ scans.command("wait <scanId>").description("Poll a scan until it finishes (honor
|
|
|
542
789
|
}).finally(() => process.removeListener("SIGINT", onSigint))
|
|
543
790
|
);
|
|
544
791
|
});
|
|
545
|
-
scans.command("launch").description("Launch a scan").requiredOption("--repo <id>", "repository id").
|
|
792
|
+
scans.command("launch").description("Launch a scan").requiredOption("--repo <id>", "repository id").option(
|
|
793
|
+
"--budget <credits>",
|
|
794
|
+
"max budget, in credits (default: the budget zkao recommends for this scan type on this repository)",
|
|
795
|
+
toInt
|
|
796
|
+
).option("--preset <ref>", "scan preset ref (see `zkao presets`)").option("--branch <name>", "branch to scan").option("--commit <sha>", "specific commit to scan").option(
|
|
797
|
+
"--base <ref>",
|
|
798
|
+
"base commit or ref of a diff scan; the scan audits only the change from it (requires a diff preset)"
|
|
799
|
+
).option("--message <text>", "commit message (metadata)").option(
|
|
546
800
|
"--guidance <file|->",
|
|
547
801
|
"per-scan guidance from a file (or - for stdin); replaces the repo's guidance for this scan"
|
|
548
802
|
).option(
|
|
@@ -558,8 +812,8 @@ scans.command("launch").description("Launch a scan").requiredOption("--repo <id>
|
|
|
558
812
|
presetRef: opts.preset,
|
|
559
813
|
branch: opts.branch ?? null,
|
|
560
814
|
commitHash: opts.commit ?? null,
|
|
815
|
+
baseCommit: opts.base,
|
|
561
816
|
commitMessage: opts.message ?? null,
|
|
562
|
-
optInFlowIds: opts.flow.length > 0 ? opts.flow : void 0,
|
|
563
817
|
auditAreaKeys: opts.area.length > 0 ? opts.area : void 0,
|
|
564
818
|
...opts.guidance !== void 0 ? { guidance: readContentArg(opts.guidance) } : {}
|
|
565
819
|
};
|
|
@@ -594,16 +848,30 @@ findings.command("resolution <findingId> <status>").description("Set resolution
|
|
|
594
848
|
findings.command("publish <findingId>").description("Publish a finding as a public artifact").option("--note <noteId>", "note to pin as the published note").option("--password", "generate an access password").action(
|
|
595
849
|
(findingId, opts) => run((c) => c.publishFinding(findingId, { noteId: opts.note, withPassword: opts.password }))
|
|
596
850
|
);
|
|
597
|
-
var billing = program.command("billing").description("Read the
|
|
598
|
-
billing.command("balance").description("
|
|
851
|
+
var billing = program.command("billing").description("Read the credit balance available to the project and its usage");
|
|
852
|
+
billing.command("balance").description("The organization's shared credit balance, what active scans hold, and what is available").action(() => run((c) => c.getBillingBalance()));
|
|
599
853
|
billing.command("usage").description("Credit ledger movements (defaults to the last 30 days)").option("--from <date>", "ISO date or timestamp to start from").option("--to <date>", "ISO date or timestamp to stop at").option("--limit <n>", "max events to return", toInt).action(
|
|
600
854
|
(opts) => run((c) => c.getBillingUsage(opts))
|
|
601
855
|
);
|
|
602
856
|
billing.command("summary").description("Per-month credits spent and purchased, newest first").option("--months <n>", "how many months to return", toInt).action(
|
|
603
857
|
(opts) => run((c) => c.getBillingSummary({ months: opts.months }))
|
|
604
858
|
);
|
|
859
|
+
var admin = program.command("admin", { hidden: true }).description("Site-admin operations on the project");
|
|
860
|
+
admin.command("manual-audit").description("Record a zkSecurity audit report as a manual audit").requiredOption("--repo <id>", "repository id").requiredOption("--commit <sha>", "commit the audit reviewed").requiredOption(
|
|
861
|
+
"--findings <file|->",
|
|
862
|
+
"findings YAML (report-tool: `just export-findings <slug>`)"
|
|
863
|
+
).option("--report <file|->", "report body markdown (report-tool: `just export-overview <slug>`)").option("--commit-message <text>", "commit message to show with the commit").action(
|
|
864
|
+
(opts) => run(
|
|
865
|
+
(c) => c.createManualAudit({
|
|
866
|
+
repositoryId: opts.repo,
|
|
867
|
+
commitHash: opts.commit,
|
|
868
|
+
findingsYaml: readContentArg(opts.findings),
|
|
869
|
+
reportMarkdown: opts.report === void 0 ? null : readContentArg(opts.report),
|
|
870
|
+
commitMessage: opts.commitMessage ?? null
|
|
871
|
+
})
|
|
872
|
+
)
|
|
873
|
+
);
|
|
605
874
|
program.command("presets").description("List scan presets the project can launch").action(() => run((c) => c.listScanPresets()));
|
|
606
|
-
program.command("flows").description("List opt-in flows available at launch").action(() => run((c) => c.listOptionalFlows()));
|
|
607
875
|
function errorMessage(err) {
|
|
608
876
|
if (err instanceof AggregateError && err.errors.length > 0) {
|
|
609
877
|
return err.errors.map(errorMessage).join("; ");
|
|
@@ -615,7 +883,7 @@ function errorMessage(err) {
|
|
|
615
883
|
return err.cause ? `${message}: ${errorMessage(err.cause)}` : message;
|
|
616
884
|
}
|
|
617
885
|
printUpdateNotice(version);
|
|
618
|
-
program.parseAsync().then(() => refreshUpdateCache()).catch((err) => {
|
|
886
|
+
program.parseAsync().then(() => refreshUpdateCache(version)).catch((err) => {
|
|
619
887
|
console.error(errorMessage(err));
|
|
620
888
|
process.exit(1);
|
|
621
889
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zksecurity/zkao-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0-next.3",
|
|
4
4
|
"description": "Command-line interface for the zkao Project API",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "zkSecurity",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"prepublishOnly": "node -e \"if (!(process.env.npm_config_user_agent || '').includes('bun/')) { console.error('Publish with bun publish: npm ships the literal workspace:* dependency and breaks installs.'); process.exit(1); }\""
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@zksecurity/zkao-sdk": "0.
|
|
42
|
+
"@zksecurity/zkao-sdk": "0.11.0-next.3",
|
|
43
43
|
"commander": "^12.1.0"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|