aisec-cli 0.2.0 → 0.2.1
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 +13 -5
- package/lib/api.mjs +20 -0
- package/lib/scan.mjs +47 -23
- package/lib/scans.mjs +1 -5
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
#
|
|
1
|
+
# aisec-cli
|
|
2
2
|
|
|
3
|
-
CLI for
|
|
3
|
+
CLI for [AISEC](https://aisec.tools) — AI-powered web application security scanner. Autonomous pentesting from your terminal.
|
|
4
4
|
|
|
5
5
|
## Quick Start
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npx
|
|
8
|
+
npx aisec-cli scan https://target.com --token YOUR_TOKEN
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
Or install globally:
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
npm i -g
|
|
14
|
+
npm i -g aisec-cli
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
## Authentication
|
|
18
18
|
|
|
19
|
-
Get your token at [
|
|
19
|
+
Get your token at the [AISEC Dashboard](https://app.aisec.tools/developer).
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
22
|
export AISEC_TOKEN=ask_...
|
|
@@ -49,6 +49,14 @@ aisec scans -l 20 # Last 20 scans
|
|
|
49
49
|
aisec status # Check connection & auth
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
+
## Links
|
|
53
|
+
|
|
54
|
+
- [AISEC Website](https://aisec.tools) — AI-powered penetration testing platform
|
|
55
|
+
- [Dashboard](https://app.aisec.tools) — manage scans and findings
|
|
56
|
+
- [How It Works](https://aisec.tools/how-it-works) — platform documentation
|
|
57
|
+
- [Pricing](https://aisec.tools/pricing) — plans and credits
|
|
58
|
+
- [Python CLI](https://github.com/aisec-foundation/cli-python) — alternative CLI
|
|
59
|
+
|
|
52
60
|
## License
|
|
53
61
|
|
|
54
62
|
MIT
|
package/lib/api.mjs
CHANGED
|
@@ -15,6 +15,26 @@ export async function request(apiUrl, path, token, opts = {}) {
|
|
|
15
15
|
process.exit(1);
|
|
16
16
|
}
|
|
17
17
|
const body = await res.text();
|
|
18
|
+
if (res.status === 403) {
|
|
19
|
+
try {
|
|
20
|
+
const parsed = JSON.parse(body);
|
|
21
|
+
const err = parsed?.detail || parsed;
|
|
22
|
+
if (err?.error === "target_not_verified") {
|
|
23
|
+
const root = err.root_domain || "your target";
|
|
24
|
+
console.error(chalk.bold.red("\nTarget not verified"));
|
|
25
|
+
console.error(`To scan ${chalk.bold(root)}, publish a DNS TXT record:`);
|
|
26
|
+
console.error(` ${chalk.cyan("Host")}: _aisec-verify.${root}`);
|
|
27
|
+
console.error(` ${chalk.cyan("Type")}: TXT`);
|
|
28
|
+
console.error(` ${chalk.cyan("Value")}: use the dashboard to get your per-user challenge token`);
|
|
29
|
+
// Prefer the backend-provided per-project verification path; fall
|
|
30
|
+
// back to /projects (verification is per-project — there is no
|
|
31
|
+
// account-wide /verifications route).
|
|
32
|
+
const vpath = err.verification_url || "/projects";
|
|
33
|
+
console.error(`\nDashboard: ${chalk.cyan(`https://app.aisec.tools${vpath}`)}`);
|
|
34
|
+
process.exit(3);
|
|
35
|
+
}
|
|
36
|
+
} catch {}
|
|
37
|
+
}
|
|
18
38
|
throw new Error(`${res.status} ${res.statusText}: ${body}`);
|
|
19
39
|
}
|
|
20
40
|
|
package/lib/scan.mjs
CHANGED
|
@@ -4,6 +4,19 @@ import { readFileSync } from "fs";
|
|
|
4
4
|
import { resolveAuth, resolveApi, wsUrl } from "./config.mjs";
|
|
5
5
|
import { request, healthCheck } from "./api.mjs";
|
|
6
6
|
|
|
7
|
+
// Map a known API host to its dashboard host. Returns null for unknown
|
|
8
|
+
// hosts (staging/local/other tenant) so we never print a production URL
|
|
9
|
+
// for a scan that didn't run on production.
|
|
10
|
+
function dashboardUrl(apiUrl, scanId) {
|
|
11
|
+
try {
|
|
12
|
+
const host = new URL(apiUrl).hostname;
|
|
13
|
+
if (host === "api.aisec.tools") return `https://app.aisec.tools/scans/${scanId}`;
|
|
14
|
+
return null;
|
|
15
|
+
} catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
7
20
|
function resolveProfile(opts) {
|
|
8
21
|
if (opts.full) return "full";
|
|
9
22
|
if (opts.bounty) return "bounty";
|
|
@@ -202,10 +215,15 @@ export async function cmdScan(target, opts) {
|
|
|
202
215
|
|
|
203
216
|
// WebSocket streaming
|
|
204
217
|
const wsBase = wsUrl(apiUrl);
|
|
205
|
-
const wsUrlFull = `${wsBase}/ws/scans/${scanId}
|
|
218
|
+
const wsUrlFull = `${wsBase}/ws/scans/${scanId}`;
|
|
206
219
|
|
|
207
|
-
|
|
220
|
+
// The API authenticates the socket via the `aisec-token` subprotocol
|
|
221
|
+
// (same as the dashboard + Python CLI). It does NOT read ?token= from the
|
|
222
|
+
// URL — passing it there both fails auth and risks leaking the token into
|
|
223
|
+
// logs. The ws lib sends the second protocol entry as the token value.
|
|
224
|
+
const ws = new WebSocket(wsUrlFull, ["aisec-token", token]);
|
|
208
225
|
let cancelled = false;
|
|
226
|
+
let completed = false; // set true on scan_complete
|
|
209
227
|
const foundFindings = []; // track severities for --fail-on
|
|
210
228
|
let exitCode = 0;
|
|
211
229
|
|
|
@@ -264,12 +282,13 @@ export async function cmdScan(target, opts) {
|
|
|
264
282
|
}
|
|
265
283
|
break;
|
|
266
284
|
|
|
267
|
-
case "credits_update":
|
|
268
285
|
case "cost_update":
|
|
269
286
|
{
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
287
|
+
// Backend emits token counts now (cost/credits were removed).
|
|
288
|
+
const ti = msg.data?.tokens_in;
|
|
289
|
+
const to = msg.data?.tokens_out;
|
|
290
|
+
if (ti != null || to != null) {
|
|
291
|
+
process.stdout.write(chalk.dim(` [tokens ${ti ?? 0} in · ${to ?? 0} out]\r`));
|
|
273
292
|
}
|
|
274
293
|
}
|
|
275
294
|
break;
|
|
@@ -281,25 +300,19 @@ export async function cmdScan(target, opts) {
|
|
|
281
300
|
|
|
282
301
|
case "scan_complete": {
|
|
283
302
|
spinner.stop();
|
|
303
|
+
completed = true;
|
|
284
304
|
const d = msg.data || {};
|
|
285
|
-
const
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
// Fetch remaining credits
|
|
289
|
-
let remaining = "?";
|
|
290
|
-
try {
|
|
291
|
-
const me = await request(apiUrl, "/api/v1/auth/me", token);
|
|
292
|
-
remaining = parseFloat(me.credits_balance || 0).toFixed(1);
|
|
293
|
-
} catch {}
|
|
294
|
-
|
|
295
|
-
const reportUrl = `https://app.aisec.tools/scans/${scanId}`;
|
|
305
|
+
const tokensIn = d.tokens_in ?? 0;
|
|
306
|
+
const tokensOut = d.tokens_out ?? 0;
|
|
307
|
+
const reportUrl = dashboardUrl(apiUrl, scanId);
|
|
296
308
|
console.log(
|
|
297
309
|
"\n" + chalk.green("━".repeat(50)) + "\n" +
|
|
298
310
|
chalk.bold.green(" Scan complete\n") +
|
|
299
311
|
chalk.dim(` Findings: `) + chalk.white(d.findings ?? 0) + "\n" +
|
|
300
|
-
chalk.dim(`
|
|
301
|
-
|
|
302
|
-
|
|
312
|
+
chalk.dim(` Tokens: `) + chalk.white(`${tokensIn} in · ${tokensOut} out`) + "\n" +
|
|
313
|
+
(reportUrl
|
|
314
|
+
? chalk.dim(` Report: `) + chalk.underline.cyan(reportUrl)
|
|
315
|
+
: chalk.dim(` Scan ID: `) + chalk.white(scanId)) + "\n" +
|
|
303
316
|
chalk.green("━".repeat(50))
|
|
304
317
|
);
|
|
305
318
|
|
|
@@ -307,7 +320,7 @@ export async function cmdScan(target, opts) {
|
|
|
307
320
|
if (process.env.GITHUB_OUTPUT) {
|
|
308
321
|
const { appendFileSync } = await import("fs");
|
|
309
322
|
appendFileSync(process.env.GITHUB_OUTPUT, `findings=${d.findings ?? 0}\n`);
|
|
310
|
-
appendFileSync(process.env.GITHUB_OUTPUT, `report-url=${reportUrl}\n`);
|
|
323
|
+
if (reportUrl) appendFileSync(process.env.GITHUB_OUTPUT, `report-url=${reportUrl}\n`);
|
|
311
324
|
}
|
|
312
325
|
|
|
313
326
|
// --fail-on check
|
|
@@ -335,8 +348,19 @@ export async function cmdScan(target, opts) {
|
|
|
335
348
|
console.error(chalk.red(`WebSocket error: ${err.message}`));
|
|
336
349
|
});
|
|
337
350
|
|
|
338
|
-
ws.on("close", () => {
|
|
351
|
+
ws.on("close", (code) => {
|
|
339
352
|
spinner.stop();
|
|
340
|
-
if (
|
|
353
|
+
if (cancelled) return;
|
|
354
|
+
// Closing before scan_complete means we never saw the result (auth
|
|
355
|
+
// failure, network drop, server restart). Don't let CI read that as a
|
|
356
|
+
// pass — exit non-zero so the pipeline surfaces it.
|
|
357
|
+
if (!completed && exitCode === 0) {
|
|
358
|
+
console.error(
|
|
359
|
+
chalk.red(`\n✗ Stream closed before the scan completed (ws code ${code ?? "?"}). `) +
|
|
360
|
+
chalk.dim("Check the dashboard for status; the scan may still be running.")
|
|
361
|
+
);
|
|
362
|
+
exitCode = 1;
|
|
363
|
+
}
|
|
364
|
+
process.exit(exitCode);
|
|
341
365
|
});
|
|
342
366
|
}
|
package/lib/scans.mjs
CHANGED
|
@@ -35,18 +35,15 @@ export async function cmdScans(opts) {
|
|
|
35
35
|
chalk.dim("Status".padEnd(12)) +
|
|
36
36
|
chalk.dim("Domain".padEnd(30)) +
|
|
37
37
|
chalk.dim("Finds".padStart(6)) +
|
|
38
|
-
chalk.dim("Cost".padStart(9)) +
|
|
39
38
|
chalk.dim("Date".padStart(13)) +
|
|
40
39
|
chalk.dim("ID".padStart(11))
|
|
41
40
|
);
|
|
42
|
-
console.log(chalk.dim("─".repeat(
|
|
41
|
+
console.log(chalk.dim("─".repeat(72)));
|
|
43
42
|
|
|
44
43
|
for (const s of scans) {
|
|
45
44
|
const color = STATUS_COLORS[s.status] || chalk.white;
|
|
46
45
|
const domain = (s.target || "").replace(/^https?:\/\//, "").slice(0, 28);
|
|
47
46
|
const findings = String(s.findings_count ?? s.total_findings ?? 0);
|
|
48
|
-
const cr = s.credits_used ?? s.total_cost;
|
|
49
|
-
const cost = cr != null ? `${cr.toFixed(1)}` : "-";
|
|
50
47
|
const date = s.created_at ? s.created_at.slice(0, 10) : "-";
|
|
51
48
|
const id = (s.id || "").slice(0, 8);
|
|
52
49
|
|
|
@@ -54,7 +51,6 @@ export async function cmdScans(opts) {
|
|
|
54
51
|
color(s.status.padEnd(12)) +
|
|
55
52
|
chalk.white(domain.padEnd(30)) +
|
|
56
53
|
chalk.white(findings.padStart(6)) +
|
|
57
|
-
chalk.white(cost.padStart(9)) +
|
|
58
54
|
chalk.dim(date.padStart(13)) +
|
|
59
55
|
chalk.dim(id.padStart(11))
|
|
60
56
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aisec-cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "CLI for aisec — AI-powered web security scanner",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
"bin/",
|
|
11
11
|
"lib/"
|
|
12
12
|
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"test": "node test/unit.mjs"
|
|
15
|
+
},
|
|
13
16
|
"engines": {
|
|
14
17
|
"node": ">=18"
|
|
15
18
|
},
|