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 CHANGED
@@ -1,22 +1,22 @@
1
- # @aisec-foundation/cli
1
+ # aisec-cli
2
2
 
3
- CLI for **aisec** — AI-powered web security scanner.
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 @aisec-foundation/cli scan https://target.com --token YOUR_TOKEN
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 @aisec-foundation/cli
14
+ npm i -g aisec-cli
15
15
  ```
16
16
 
17
17
  ## Authentication
18
18
 
19
- Get your token at [app.aisec.tools/developer](https://app.aisec.tools/developer).
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}?token=${token}`;
218
+ const wsUrlFull = `${wsBase}/ws/scans/${scanId}`;
177
219
 
178
- const ws = new WebSocket(wsUrlFull);
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
- const cr = msg.data?.credits_used ?? msg.data?.cost;
242
- if (cr != null) {
243
- process.stdout.write(chalk.dim(` [${cr.toFixed(1)} credits]\r`));
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 duration = d.duration ? formatDuration(d.duration) : "?";
257
- const creditsUsed = (d.credits_used ?? d.cost ?? 0).toFixed(1);
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(` Credits: `) + chalk.white(creditsUsed) + chalk.dim(" used · ") + chalk.yellow.bold(remaining) + chalk.dim(" remaining") + "\n" +
272
- chalk.dim(` Duration: `) + chalk.white(duration) + "\n" +
273
- chalk.dim(` Report: `) + chalk.underline.cyan(reportUrl) + "\n" +
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 (!cancelled) process.exit(exitCode);
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(81)));
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.0",
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
  }