aisec-cli 0.1.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/bin/aisec.mjs +12 -0
- package/lib/api.mjs +20 -0
- package/lib/scan.mjs +76 -23
- package/lib/scans.mjs +1 -5
- package/package.json +5 -2
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/bin/aisec.mjs
CHANGED
|
@@ -16,12 +16,16 @@ program
|
|
|
16
16
|
.option("--stealth", "Stealth profile — slower, WAF evasion")
|
|
17
17
|
.option("--aggressive", "Aggressive — full port scan, brute force, sqlmap")
|
|
18
18
|
.option("--full", "Full — aggressive + subdomain scope + 50 iterations")
|
|
19
|
+
.option("--bounty", "Bug bounty — high-impact vulns, skip noise, PoC-ready output")
|
|
20
|
+
.option("--scan-type <type>", "Scan type: web, network, crypto", "web")
|
|
19
21
|
.option("-e, --engine <engine>", "AI engine: claude or ollama", "claude")
|
|
20
22
|
.option("-m, --model <model>", "Model name")
|
|
23
|
+
.option("--review-model <model>", "Review model (default: claude-sonnet-4-6)")
|
|
21
24
|
.option("--temperature <temp>", "AI temperature 0.0-1.0", parseFloat)
|
|
22
25
|
.option("-n, --max-iterations <n>", "Max AI iterations", parseInt)
|
|
23
26
|
.option("--scope <scope>", "Scan scope: target, domain, subdomain")
|
|
24
27
|
.option("-t, --timeout <minutes>", "Timeout in minutes, 0=unlimited", parseInt)
|
|
28
|
+
.option("--cost-cap <credits>", "Max credits to spend (0=no limit)", parseFloat)
|
|
25
29
|
.option("--skip-recon", "Skip infrastructure recon")
|
|
26
30
|
.option("--skip-browser", "Skip browser-based recon")
|
|
27
31
|
.option("-u, --username <user>", "Username for auth scanning")
|
|
@@ -29,6 +33,14 @@ program
|
|
|
29
33
|
.option("--cookies <json>", "Session cookies as JSON or @file")
|
|
30
34
|
.option("--proxy <url>", "Proxy URL")
|
|
31
35
|
.option("--headers <headers>", "Custom headers: 'Key:Val,Key2:Val2' or @file")
|
|
36
|
+
.option("--localstorage <json>", "Browser localStorage as JSON or @file")
|
|
37
|
+
.option("--custom-instructions <text>", "Free-text guidance for the AI agent (max 500 chars)")
|
|
38
|
+
.option("--disable-tools <tools>", "Comma-separated tools to disable (e.g. sqlmap,hydra,nikto)")
|
|
39
|
+
.option("--disable-enrichments <list>", "Comma-separated enrichments to disable (e.g. leak_check,shodan)")
|
|
40
|
+
.option("--out-of-scope <list>", "Comma-separated domains/paths to exclude")
|
|
41
|
+
.option("--wordlist <name>", "Wordlist: common, big, api-endpoints")
|
|
42
|
+
.option("--auto-compact", "Auto-compact context for long scans (saves credits)")
|
|
43
|
+
.option("--project-id <id>", "Assign scan to a project")
|
|
32
44
|
.option("--fail-on <severity>", "Exit 1 if findings at this severity or above (critical, high, medium, low)")
|
|
33
45
|
.option("--source <source>", "Scan source identifier (cli, ci, api)", "cli")
|
|
34
46
|
.option("--token <token>", "API token (or AISEC_TOKEN env)")
|
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,8 +4,22 @@ 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";
|
|
22
|
+
if (opts.bounty) return "bounty";
|
|
9
23
|
if (opts.aggressive) return "aggressive";
|
|
10
24
|
if (opts.stealth) return "stealth";
|
|
11
25
|
return undefined;
|
|
@@ -32,6 +46,19 @@ function parseCookies(raw) {
|
|
|
32
46
|
return raw;
|
|
33
47
|
}
|
|
34
48
|
|
|
49
|
+
function parseFileOrString(raw) {
|
|
50
|
+
if (!raw) return undefined;
|
|
51
|
+
if (raw.startsWith("@")) {
|
|
52
|
+
return readFileSync(raw.slice(1), "utf-8").trim();
|
|
53
|
+
}
|
|
54
|
+
return raw;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function parseCommaSeparated(raw) {
|
|
58
|
+
if (!raw) return undefined;
|
|
59
|
+
return raw.split(",").map(s => s.trim()).filter(Boolean);
|
|
60
|
+
}
|
|
61
|
+
|
|
35
62
|
function buildBody(target, opts) {
|
|
36
63
|
const body = { target, source: opts.source || "cli" };
|
|
37
64
|
const profile = resolveProfile(opts);
|
|
@@ -47,10 +74,25 @@ function buildBody(target, opts) {
|
|
|
47
74
|
if (opts.username) body.username = opts.username;
|
|
48
75
|
if (opts.password) body.password = opts.password;
|
|
49
76
|
if (opts.proxy) body.proxy = opts.proxy;
|
|
77
|
+
if (opts.costCap != null) body.cost_cap = opts.costCap;
|
|
78
|
+
if (opts.reviewModel) body.review_model = opts.reviewModel;
|
|
79
|
+
if (opts.scanType && opts.scanType !== "web") body.scan_type = opts.scanType;
|
|
50
80
|
const cookies = parseCookies(opts.cookies);
|
|
51
81
|
if (cookies) body.cookies_json = cookies;
|
|
52
82
|
const headers = parseHeaders(opts.headers);
|
|
53
83
|
if (headers) body.custom_headers = headers;
|
|
84
|
+
const ls = parseFileOrString(opts.localstorage);
|
|
85
|
+
if (ls) body.localstorage_json = ls;
|
|
86
|
+
if (opts.customInstructions) body.custom_instructions = opts.customInstructions;
|
|
87
|
+
const disabledTools = parseCommaSeparated(opts.disableTools);
|
|
88
|
+
if (disabledTools) body.disabled_tools = disabledTools;
|
|
89
|
+
const disabledEnrichments = parseCommaSeparated(opts.disableEnrichments);
|
|
90
|
+
if (disabledEnrichments) body.disabled_enrichments = disabledEnrichments;
|
|
91
|
+
const outOfScope = parseCommaSeparated(opts.outOfScope);
|
|
92
|
+
if (outOfScope) body.out_of_scope = outOfScope;
|
|
93
|
+
if (opts.wordlist) body.wordlist = opts.wordlist;
|
|
94
|
+
if (opts.autoCompact) body.auto_compact = true;
|
|
95
|
+
if (opts.projectId) body.project_id = opts.projectId;
|
|
54
96
|
return body;
|
|
55
97
|
}
|
|
56
98
|
|
|
@@ -173,10 +215,15 @@ export async function cmdScan(target, opts) {
|
|
|
173
215
|
|
|
174
216
|
// WebSocket streaming
|
|
175
217
|
const wsBase = wsUrl(apiUrl);
|
|
176
|
-
const wsUrlFull = `${wsBase}/ws/scans/${scanId}
|
|
218
|
+
const wsUrlFull = `${wsBase}/ws/scans/${scanId}`;
|
|
177
219
|
|
|
178
|
-
|
|
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]);
|
|
179
225
|
let cancelled = false;
|
|
226
|
+
let completed = false; // set true on scan_complete
|
|
180
227
|
const foundFindings = []; // track severities for --fail-on
|
|
181
228
|
let exitCode = 0;
|
|
182
229
|
|
|
@@ -235,12 +282,13 @@ export async function cmdScan(target, opts) {
|
|
|
235
282
|
}
|
|
236
283
|
break;
|
|
237
284
|
|
|
238
|
-
case "credits_update":
|
|
239
285
|
case "cost_update":
|
|
240
286
|
{
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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`));
|
|
244
292
|
}
|
|
245
293
|
}
|
|
246
294
|
break;
|
|
@@ -252,25 +300,19 @@ export async function cmdScan(target, opts) {
|
|
|
252
300
|
|
|
253
301
|
case "scan_complete": {
|
|
254
302
|
spinner.stop();
|
|
303
|
+
completed = true;
|
|
255
304
|
const d = msg.data || {};
|
|
256
|
-
const
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
// Fetch remaining credits
|
|
260
|
-
let remaining = "?";
|
|
261
|
-
try {
|
|
262
|
-
const me = await request(apiUrl, "/api/v1/auth/me", token);
|
|
263
|
-
remaining = parseFloat(me.credits_balance || 0).toFixed(1);
|
|
264
|
-
} catch {}
|
|
265
|
-
|
|
266
|
-
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);
|
|
267
308
|
console.log(
|
|
268
309
|
"\n" + chalk.green("━".repeat(50)) + "\n" +
|
|
269
310
|
chalk.bold.green(" Scan complete\n") +
|
|
270
311
|
chalk.dim(` Findings: `) + chalk.white(d.findings ?? 0) + "\n" +
|
|
271
|
-
chalk.dim(`
|
|
272
|
-
|
|
273
|
-
|
|
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" +
|
|
274
316
|
chalk.green("━".repeat(50))
|
|
275
317
|
);
|
|
276
318
|
|
|
@@ -278,7 +320,7 @@ export async function cmdScan(target, opts) {
|
|
|
278
320
|
if (process.env.GITHUB_OUTPUT) {
|
|
279
321
|
const { appendFileSync } = await import("fs");
|
|
280
322
|
appendFileSync(process.env.GITHUB_OUTPUT, `findings=${d.findings ?? 0}\n`);
|
|
281
|
-
appendFileSync(process.env.GITHUB_OUTPUT, `report-url=${reportUrl}\n`);
|
|
323
|
+
if (reportUrl) appendFileSync(process.env.GITHUB_OUTPUT, `report-url=${reportUrl}\n`);
|
|
282
324
|
}
|
|
283
325
|
|
|
284
326
|
// --fail-on check
|
|
@@ -306,8 +348,19 @@ export async function cmdScan(target, opts) {
|
|
|
306
348
|
console.error(chalk.red(`WebSocket error: ${err.message}`));
|
|
307
349
|
});
|
|
308
350
|
|
|
309
|
-
ws.on("close", () => {
|
|
351
|
+
ws.on("close", (code) => {
|
|
310
352
|
spinner.stop();
|
|
311
|
-
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);
|
|
312
365
|
});
|
|
313
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.1
|
|
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
|
},
|
|
@@ -26,8 +29,8 @@
|
|
|
26
29
|
"url": "https://github.com/aisec-foundation/cli-node.git"
|
|
27
30
|
},
|
|
28
31
|
"dependencies": {
|
|
29
|
-
"commander": "^12.1.0",
|
|
30
32
|
"chalk": "^5.3.0",
|
|
33
|
+
"commander": "^12.1.0",
|
|
31
34
|
"ws": "^8.18.0"
|
|
32
35
|
}
|
|
33
36
|
}
|