@kinosec/cli 0.8.0 → 0.10.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/dist/cli.js CHANGED
@@ -6389,6 +6389,9 @@ var ScansResource = class {
6389
6389
  async cancel(id) {
6390
6390
  return this.api.post(`v1/scans/${id}/cancel`, { json: {} }).json();
6391
6391
  }
6392
+ async scoutStatus(profileId) {
6393
+ return this.api.get(`v1/scans/${profileId}/scout-status`).json();
6394
+ }
6392
6395
  };
6393
6396
  var SchedulesResource = class {
6394
6397
  constructor(api) {
@@ -7772,7 +7775,7 @@ Examples:
7772
7775
  }
7773
7776
 
7774
7777
  // src/commands/pentest.ts
7775
- var import_picocolors6 = __toESM(require_picocolors(), 1);
7778
+ var import_picocolors8 = __toESM(require_picocolors(), 1);
7776
7779
 
7777
7780
  // src/lib/confirm.ts
7778
7781
  var import_picocolors3 = __toESM(require_picocolors(), 1);
@@ -7832,8 +7835,21 @@ ${import_picocolors3.default.dim("use of its services.")}
7832
7835
 
7833
7836
  // src/commands/schedule.ts
7834
7837
  var import_picocolors4 = __toESM(require_picocolors(), 1);
7838
+
7839
+ // src/lib/validate-mode.ts
7840
+ function validateScanMode(mode) {
7841
+ if (mode === "quick") {
7842
+ console.error("Quick mode is temporarily unavailable. Please use --mode full (the default).");
7843
+ process.exit(2);
7844
+ }
7845
+ if (mode !== "full") {
7846
+ console.error(`Invalid mode "${mode}". Must be: full`);
7847
+ process.exit(2);
7848
+ }
7849
+ }
7850
+
7851
+ // src/commands/schedule.ts
7835
7852
  var VALID_PATTERNS = ["once", "daily", "weekly", "biweekly", "monthly"];
7836
- var VALID_MODES = ["quick", "full"];
7837
7853
  var DAY_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
7838
7854
  function extractTime(scheduledAt) {
7839
7855
  if (!scheduledAt) return "\u2014";
@@ -7931,19 +7947,16 @@ Examples:
7931
7947
  handleError(error2);
7932
7948
  }
7933
7949
  });
7934
- schedule.command("create").description("Create a scheduled pentest").requiredOption("--asset <id>", "Asset ID to scan").requiredOption("--pattern <pattern>", "Recurrence: once, daily, weekly, biweekly, monthly").option("--name <name>", "Schedule name").option("--mode <mode>", "Scan mode: quick or full", "full").option("--time <HH:MM>", "Time of day (24h format)", "09:00").option("--day-of-week <0-6>", "Day of week (0=Sun..6=Sat) for weekly/biweekly").option("--day-of-month <1-31>", "Day of month for monthly").option("--timezone <tz>", "Timezone (e.g. America/New_York)", Intl.DateTimeFormat().resolvedOptions().timeZone).addHelpText("after", `
7950
+ schedule.command("create").description("Create a scheduled pentest").requiredOption("--asset <id>", "Asset ID to scan").requiredOption("--pattern <pattern>", "Recurrence: once, daily, weekly, biweekly, monthly").option("--name <name>", "Schedule name").option("--mode <mode>", "Scan mode (default: full)", "full").option("--time <HH:MM>", "Time of day (24h format)", "09:00").option("--day-of-week <0-6>", "Day of week (0=Sun..6=Sat) for weekly/biweekly").option("--day-of-month <1-31>", "Day of month for monthly").option("--timezone <tz>", "Timezone (e.g. America/New_York)", Intl.DateTimeFormat().resolvedOptions().timeZone).addHelpText("after", `
7935
7951
  Examples:
7936
7952
  $ kinosec pentest schedule create --asset abc123 --pattern weekly --day-of-week 1 --time 09:00
7937
- $ kinosec pentest schedule create --asset abc123 --pattern daily --mode quick --time 14:00
7953
+ $ kinosec pentest schedule create --asset abc123 --pattern daily --time 14:00
7938
7954
  $ kinosec pentest schedule create --asset abc123 --pattern monthly --day-of-month 15 --time 03:00 --timezone UTC`).action(async (options) => {
7939
7955
  if (!VALID_PATTERNS.includes(options.pattern)) {
7940
7956
  console.error(`Invalid pattern "${options.pattern}". Must be: ${VALID_PATTERNS.join(", ")}`);
7941
7957
  process.exit(2);
7942
7958
  }
7943
- if (!VALID_MODES.includes(options.mode)) {
7944
- console.error(`Invalid mode "${options.mode}". Must be: ${VALID_MODES.join(", ")}`);
7945
- process.exit(2);
7946
- }
7959
+ validateScanMode(options.mode);
7947
7960
  if ((options.pattern === "weekly" || options.pattern === "biweekly") && options.dayOfWeek === void 0) {
7948
7961
  console.error(`--day-of-week is required for ${options.pattern} schedules.`);
7949
7962
  process.exit(2);
@@ -7981,14 +7994,17 @@ Manage: kinosec pentest schedule get ${s.id}`);
7981
7994
  handleError(error2);
7982
7995
  }
7983
7996
  });
7984
- schedule.command("update").description("Update a schedule").argument("<id>", "Schedule ID").option("--pattern <pattern>", "Recurrence: once, daily, weekly, biweekly, monthly").option("--name <name>", "Schedule name").option("--mode <mode>", "Scan mode: quick or full").option("--time <HH:MM>", "Time of day (24h format)").option("--day-of-week <0-6>", "Day of week (0=Sun..6=Sat)").option("--day-of-month <1-31>", "Day of month").option("--timezone <tz>", "Timezone").addHelpText("after", `
7997
+ schedule.command("update").description("Update a schedule").argument("<id>", "Schedule ID").option("--pattern <pattern>", "Recurrence: once, daily, weekly, biweekly, monthly").option("--name <name>", "Schedule name").option("--mode <mode>", "Scan mode").option("--time <HH:MM>", "Time of day (24h format)").option("--day-of-week <0-6>", "Day of week (0=Sun..6=Sat)").option("--day-of-month <1-31>", "Day of month").option("--timezone <tz>", "Timezone").addHelpText("after", `
7985
7998
  Examples:
7986
7999
  $ kinosec pentest schedule update abc123 --pattern daily --time 14:00
7987
- $ kinosec pentest schedule update abc123 --mode quick`).action(async (id, options) => {
8000
+ $ kinosec pentest schedule update abc123 --time 14:00`).action(async (id, options) => {
7988
8001
  const params = {};
7989
8002
  if (options.pattern) params.recurrence_pattern = options.pattern;
7990
8003
  if (options.name) params.name = options.name;
7991
- if (options.mode) params.scan_mode = options.mode;
8004
+ if (options.mode) {
8005
+ validateScanMode(options.mode);
8006
+ params.scan_mode = options.mode;
8007
+ }
7992
8008
  if (options.time) params.time_of_day = options.time;
7993
8009
  if (options.dayOfWeek !== void 0) params.day_of_week = parseInt(options.dayOfWeek, 10);
7994
8010
  if (options.dayOfMonth !== void 0) params.day_of_month = parseInt(options.dayOfMonth, 10);
@@ -11527,6 +11543,320 @@ async function printFindingsSummary(client, profileId, tty3) {
11527
11543
  }
11528
11544
  }
11529
11545
 
11546
+ // src/lib/scout.ts
11547
+ var import_picocolors6 = __toESM(require_picocolors(), 1);
11548
+ var UNICODE2 = isUnicodeSupported();
11549
+ var sym2 = {
11550
+ success: UNICODE2 ? "\u2713" : "v",
11551
+ fail: UNICODE2 ? "\u2717" : "x",
11552
+ warn: UNICODE2 ? "\u26A0" : "!"
11553
+ };
11554
+ function printOnPremInstructions(result) {
11555
+ const tty3 = isTTY();
11556
+ const header = tty3 ? import_picocolors6.default.bold("On-Prem Scout Setup") : "On-Prem Scout Setup";
11557
+ console.log(`
11558
+ ${header}
11559
+ `);
11560
+ console.log(` Session ID: ${result.session_id}`);
11561
+ console.log(` Profile ID: ${result.scan_profile_id}`);
11562
+ const curlCmd = `curl -fsSL https://api.kinosec.ai/functions/v1/scout-bootstrap/launch/${result.bootstrap_token} | bash`;
11563
+ if (tty3) {
11564
+ console.log(`
11565
+ Run on target host:
11566
+ `);
11567
+ console.log(` ${import_picocolors6.default.cyan(curlCmd)}
11568
+ `);
11569
+ console.log(` ${import_picocolors6.default.yellow(sym2.warn)} Token expires in ${import_picocolors6.default.bold("15 minutes")}.`);
11570
+ } else {
11571
+ console.log(`
11572
+ Run on target host:
11573
+ `);
11574
+ console.log(` ${curlCmd}
11575
+ `);
11576
+ console.log(` ${sym2.warn} Token expires in 15 minutes.`);
11577
+ }
11578
+ }
11579
+ async function waitForScoutConnection(client, profileId, options) {
11580
+ const { timeoutMs, intervalMs, useJson } = options;
11581
+ const tty3 = isTTY();
11582
+ const loopStart = Date.now();
11583
+ let spinner = null;
11584
+ if (!useJson && tty3) {
11585
+ spinner = ora({
11586
+ text: "Waiting for Scout connection...",
11587
+ color: "green",
11588
+ isEnabled: true
11589
+ }).start();
11590
+ }
11591
+ const cleanup = () => {
11592
+ if (spinner) spinner.stop();
11593
+ };
11594
+ const onSigint = () => {
11595
+ cleanup();
11596
+ console.log("\nWatch cancelled. Pentest is waiting for Scout connection.");
11597
+ process.exit(0);
11598
+ };
11599
+ process.once("SIGINT", onSigint);
11600
+ while (true) {
11601
+ const status = await client.scans.scoutStatus(profileId);
11602
+ if (useJson) {
11603
+ const elapsed = Math.round((Date.now() - loopStart) / 1e3);
11604
+ process.stdout.write(
11605
+ JSON.stringify({ ...status, elapsed_seconds: elapsed }) + "\n"
11606
+ );
11607
+ }
11608
+ switch (status.status) {
11609
+ case "pending":
11610
+ if (spinner) spinner.text = import_picocolors6.default.green("Waiting for Scout bootstrap...");
11611
+ break;
11612
+ case "bootstrapped":
11613
+ if (spinner) spinner.text = import_picocolors6.default.green("Scout bootstrapped, establishing connection...");
11614
+ break;
11615
+ case "connected": {
11616
+ cleanup();
11617
+ process.removeListener("SIGINT", onSigint);
11618
+ if (!useJson) {
11619
+ const icon = tty3 ? import_picocolors6.default.green(sym2.success) : sym2.success;
11620
+ console.log(`${icon} Scout connected`);
11621
+ if (status.hostname) {
11622
+ console.log(` Hostname: ${status.hostname}`);
11623
+ }
11624
+ if (status.tools.length > 0) {
11625
+ console.log(` Tools: ${status.tools.join(", ")}`);
11626
+ }
11627
+ }
11628
+ return;
11629
+ }
11630
+ case "expired": {
11631
+ cleanup();
11632
+ process.removeListener("SIGINT", onSigint);
11633
+ const icon = tty3 ? import_picocolors6.default.red(sym2.fail) : sym2.fail;
11634
+ console.error(`
11635
+ ${icon} Scout bootstrap token expired.`);
11636
+ console.error("Start a new pentest to get a fresh token.");
11637
+ process.exit(1);
11638
+ }
11639
+ case "disconnected": {
11640
+ cleanup();
11641
+ process.removeListener("SIGINT", onSigint);
11642
+ const icon = tty3 ? import_picocolors6.default.red(sym2.fail) : sym2.fail;
11643
+ console.error(`
11644
+ ${icon} Scout disconnected unexpectedly.`);
11645
+ console.error(`Check status: kinosec pentest get ${profileId}`);
11646
+ process.exit(1);
11647
+ }
11648
+ }
11649
+ if (Date.now() - loopStart > timeoutMs) {
11650
+ cleanup();
11651
+ process.removeListener("SIGINT", onSigint);
11652
+ const secs = Math.round(timeoutMs / 1e3);
11653
+ console.error(`
11654
+ Timeout: Scout did not connect within ${secs}s.`);
11655
+ console.error("The bootstrap token may have expired. Start a new pentest.");
11656
+ process.exit(3);
11657
+ }
11658
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
11659
+ }
11660
+ }
11661
+
11662
+ // src/lib/scope.ts
11663
+ var import_picocolors7 = __toESM(require_picocolors(), 1);
11664
+
11665
+ // src/lib/prompt.ts
11666
+ import { createInterface as createInterface2 } from "readline";
11667
+ async function promptMasked(label) {
11668
+ if (!isTTY() || !process.stdin.isTTY) {
11669
+ console.error(`Cannot prompt for ${label} in non-interactive mode. Use flags instead.`);
11670
+ process.exit(2);
11671
+ }
11672
+ process.stderr.write(`${label}: `);
11673
+ return new Promise((resolve, reject) => {
11674
+ const chars = [];
11675
+ process.stdin.setRawMode(true);
11676
+ process.stdin.resume();
11677
+ process.stdin.setEncoding("utf-8");
11678
+ const onData = (chunk) => {
11679
+ for (const ch of chunk) {
11680
+ if (ch === "\r" || ch === "\n") {
11681
+ process.stdin.setRawMode(false);
11682
+ process.stdin.pause();
11683
+ process.stdin.removeListener("data", onData);
11684
+ process.stderr.write("\n");
11685
+ resolve(chars.join(""));
11686
+ return;
11687
+ } else if (ch === "") {
11688
+ process.stdin.setRawMode(false);
11689
+ process.stdin.pause();
11690
+ process.stdin.removeListener("data", onData);
11691
+ process.stderr.write("\n");
11692
+ reject(new Error("Cancelled"));
11693
+ return;
11694
+ } else if (ch === "\x7F" || ch === "\b") {
11695
+ if (chars.length > 0) {
11696
+ chars.pop();
11697
+ process.stderr.write("\b \b");
11698
+ }
11699
+ } else if (ch >= " ") {
11700
+ chars.push(ch);
11701
+ process.stderr.write("*");
11702
+ }
11703
+ }
11704
+ };
11705
+ process.stdin.on("data", onData);
11706
+ });
11707
+ }
11708
+ async function promptText(label) {
11709
+ if (!isTTY() || !process.stdin.isTTY) {
11710
+ console.error(`Cannot prompt for ${label} in non-interactive mode. Use flags instead.`);
11711
+ process.exit(2);
11712
+ }
11713
+ const rl = createInterface2({ input: process.stdin, output: process.stderr });
11714
+ const answer = await new Promise((resolve) => {
11715
+ rl.question(`${label}: `, resolve);
11716
+ });
11717
+ rl.close();
11718
+ return answer;
11719
+ }
11720
+ async function promptSelect(message, options) {
11721
+ if (!isTTY() || !process.stdin.isTTY) {
11722
+ console.error("Cannot show interactive menu in non-interactive mode. Use flags instead.");
11723
+ process.exit(2);
11724
+ }
11725
+ console.error(`
11726
+ ${message}`);
11727
+ for (let i = 0; i < options.length; i++) {
11728
+ console.error(` ${i + 1}. ${options[i].label} \u2014 ${options[i].description}`);
11729
+ }
11730
+ const rl = createInterface2({ input: process.stdin, output: process.stderr });
11731
+ const answer = await new Promise((resolve) => {
11732
+ rl.question("> ", resolve);
11733
+ });
11734
+ rl.close();
11735
+ const index = parseInt(answer, 10) - 1;
11736
+ if (isNaN(index) || index < 0 || index >= options.length) {
11737
+ console.error("Invalid selection.");
11738
+ process.exit(2);
11739
+ }
11740
+ return options[index].value;
11741
+ }
11742
+
11743
+ // src/lib/scope.ts
11744
+ var HOSTNAME_RE = /^([a-z0-9]([a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}$/i;
11745
+ var MAX_SCOPE_HOSTS = 50;
11746
+ function parseScopeHosts(input) {
11747
+ return input.split(",").map((h) => h.trim()).filter(Boolean);
11748
+ }
11749
+ function validateScopeHosts(hosts) {
11750
+ for (const host of hosts) {
11751
+ if (!HOSTNAME_RE.test(host)) {
11752
+ console.error(`Error: Invalid hostname "${host}". Must be a valid domain (e.g., api.example.com).`);
11753
+ process.exit(2);
11754
+ }
11755
+ }
11756
+ const seen = /* @__PURE__ */ new Set();
11757
+ for (const host of hosts) {
11758
+ const lower = host.toLowerCase();
11759
+ if (seen.has(lower)) {
11760
+ console.error(`Error: Duplicate hostname "${host}" in list.`);
11761
+ process.exit(2);
11762
+ }
11763
+ seen.add(lower);
11764
+ }
11765
+ }
11766
+ function validateScopeFlags(options) {
11767
+ if (options.include && options.exclude) {
11768
+ console.error("Error: --include and --exclude cannot be used together. Use one or the other.");
11769
+ process.exit(2);
11770
+ }
11771
+ if (options.scopeClear && (options.include || options.exclude)) {
11772
+ console.error("Error: --clear cannot be combined with --include or --exclude.");
11773
+ process.exit(2);
11774
+ }
11775
+ }
11776
+ function validateTotalHostCount(existingCount, newCount) {
11777
+ const total = existingCount + newCount;
11778
+ if (total > MAX_SCOPE_HOSTS) {
11779
+ console.error(`Error: Maximum ${MAX_SCOPE_HOSTS} hosts allowed. You provided ${total}.`);
11780
+ process.exit(2);
11781
+ }
11782
+ }
11783
+ function displayScope(asset) {
11784
+ if (!asset.scope_filter || !asset.scope_hosts?.length) {
11785
+ console.error(`Scope: ${isTTY() ? import_picocolors7.default.dim("all subdomains") : "all subdomains"}`);
11786
+ return;
11787
+ }
11788
+ const label = asset.scope_filter === "include" ? "include only" : "exclude";
11789
+ const hosts = asset.scope_hosts.join(", ");
11790
+ console.error(`Scope: ${label} \u2014 ${hosts}`);
11791
+ }
11792
+ async function resolveScopeUpdate(asset, mode, newHosts) {
11793
+ const existingMode = asset.scope_filter;
11794
+ const existingHosts = asset.scope_hosts ?? [];
11795
+ if (!existingMode || existingHosts.length === 0) {
11796
+ validateScopeHosts(newHosts);
11797
+ if (newHosts.length > MAX_SCOPE_HOSTS) {
11798
+ console.error(`Error: Maximum ${MAX_SCOPE_HOSTS} hosts allowed. You provided ${newHosts.length}.`);
11799
+ process.exit(2);
11800
+ }
11801
+ return { scope_filter: mode, scope_hosts: newHosts };
11802
+ }
11803
+ if (existingMode !== mode) {
11804
+ console.error(`
11805
+ Existing ${existingMode} scope:`);
11806
+ for (const host of existingHosts) {
11807
+ console.error(` - ${host}`);
11808
+ }
11809
+ const answer2 = await promptSelect(
11810
+ `You have an existing ${existingMode} scope. Using --${mode} will replace it. Continue?`,
11811
+ [
11812
+ { value: "switch", label: `Yes, switch to ${mode}`, description: `Replace ${existingMode} list with new ${mode} list` },
11813
+ { value: "cancel", label: "Cancel", description: "Keep existing scope" }
11814
+ ]
11815
+ );
11816
+ if (answer2 === "cancel") {
11817
+ console.error("Cancelled.");
11818
+ return null;
11819
+ }
11820
+ validateScopeHosts(newHosts);
11821
+ if (newHosts.length > MAX_SCOPE_HOSTS) {
11822
+ console.error(`Error: Maximum ${MAX_SCOPE_HOSTS} hosts allowed. You provided ${newHosts.length}.`);
11823
+ process.exit(2);
11824
+ }
11825
+ return { scope_filter: mode, scope_hosts: newHosts };
11826
+ }
11827
+ console.error(`
11828
+ Existing ${existingMode} scope:`);
11829
+ for (const host of existingHosts) {
11830
+ console.error(` - ${host}`);
11831
+ }
11832
+ const answer = await promptSelect(
11833
+ `You have existing subdomains in your ${mode} list. What would you like to do?`,
11834
+ [
11835
+ { value: "add", label: "Add to existing list", description: "Append new hosts to the current list" },
11836
+ { value: "replace", label: "Replace with new list", description: "Remove existing hosts and use only the new ones" }
11837
+ ]
11838
+ );
11839
+ if (answer === "add") {
11840
+ validateScopeHosts(newHosts);
11841
+ const existingLower = new Set(existingHosts.map((h) => h.toLowerCase()));
11842
+ const deduped = newHosts.filter((h) => !existingLower.has(h.toLowerCase()));
11843
+ if (deduped.length === 0) {
11844
+ console.error("All provided hosts are already in the list.");
11845
+ return { scope_filter: mode, scope_hosts: existingHosts };
11846
+ }
11847
+ validateScopeHosts(deduped);
11848
+ validateTotalHostCount(existingHosts.length, deduped.length);
11849
+ const merged = [...existingHosts, ...deduped];
11850
+ return { scope_filter: mode, scope_hosts: merged };
11851
+ }
11852
+ validateScopeHosts(newHosts);
11853
+ if (newHosts.length > MAX_SCOPE_HOSTS) {
11854
+ console.error(`Error: Maximum ${MAX_SCOPE_HOSTS} hosts allowed. You provided ${newHosts.length}.`);
11855
+ process.exit(2);
11856
+ }
11857
+ return { scope_filter: mode, scope_hosts: newHosts };
11858
+ }
11859
+
11530
11860
  // src/commands/pentest.ts
11531
11861
  function stripProtocol(input) {
11532
11862
  const match = input.match(/^https?:\/\/(.+)/i);
@@ -11537,15 +11867,40 @@ function isValidDomain(value) {
11537
11867
  return value.includes(".") && !/\s/.test(value);
11538
11868
  }
11539
11869
  function registerPentestCommand(program3) {
11540
- const pentest = program3.command("pentest").description("Run and manage pentests").option("-t, --target <domain>", "Target domain to pentest").option("--profile <id>", "Pentest profile ID (alternative to --target)").option("--mode <mode>", "Pentest mode: quick or full", "full").option("-y, --yes", "Accept authorization terms without prompting").option("--no-watch", "Skip auto-watch after starting the pentest").addHelpText("after", `
11870
+ const pentest = program3.command("pentest").description("Run and manage pentests").option("-t, --target <domain>", "Target domain to pentest").option("--profile <id>", "Pentest profile ID (alternative to --target)").option("--mode <mode>", "Pentest mode", "full").option("-y, --yes", "Accept authorization terms without prompting").option("--no-watch", "Skip auto-watch after starting the pentest").option("--deployment <type>", "Deployment type: cloud or on-prem", "cloud").option("--include <hosts>", "Include only these subdomains (comma-separated)").option("--exclude <hosts>", "Exclude these subdomains (comma-separated)").option("--scope <action>", "Scope action: clear").addHelpText("after", `
11541
11871
  Examples:
11542
11872
  $ kinosec pentest -t delikado.org
11543
- $ kinosec pentest --target delikado.org --mode quick
11544
- $ kinosec pentest --profile abc123`).action(async (options) => {
11873
+ $ kinosec pentest --profile abc123
11874
+ $ kinosec pentest -t internal.corp --deployment on-prem
11875
+ $ kinosec pentest -t example.com --include api.example.com,app.example.com
11876
+ $ kinosec pentest -t example.com --exclude staging.example.com
11877
+ $ kinosec pentest -t example.com --scope clear`).action(async (options) => {
11545
11878
  if (!options.target && !options.profile) {
11546
11879
  console.error("Provide --target <domain> or --profile <id>.");
11547
11880
  process.exit(2);
11548
11881
  }
11882
+ if (options.deployment) {
11883
+ options.deployment = options.deployment.replace(/-/g, "_");
11884
+ }
11885
+ if (options.deployment !== "cloud" && options.deployment !== "on_prem") {
11886
+ console.error("Invalid deployment type. Must be: cloud or on-prem.");
11887
+ process.exit(2);
11888
+ }
11889
+ validateScanMode(options.mode);
11890
+ const scopeClear = options.scope === "clear";
11891
+ if (options.scope && options.scope !== "clear") {
11892
+ console.error("Invalid --scope value. Must be: clear");
11893
+ process.exit(2);
11894
+ }
11895
+ validateScopeFlags({
11896
+ include: options.include,
11897
+ exclude: options.exclude,
11898
+ scopeClear
11899
+ });
11900
+ if (options.profile && (options.include || options.exclude || scopeClear)) {
11901
+ console.error("Error: Scope flags are only supported with --target, not --profile.");
11902
+ process.exit(2);
11903
+ }
11549
11904
  try {
11550
11905
  const opts = program3.opts();
11551
11906
  const { token, apiUrl } = resolveToken(opts);
@@ -11582,10 +11937,48 @@ Examples:
11582
11937
  }
11583
11938
  }
11584
11939
  targetDisplay = domain;
11585
- params = { asset_id: asset.id, scan_mode: options.mode };
11940
+ const assetParams = {
11941
+ asset_id: asset.id,
11942
+ scan_mode: options.mode
11943
+ };
11944
+ if (options.deployment !== "cloud") {
11945
+ assetParams.deployment_type = options.deployment;
11946
+ }
11947
+ params = assetParams;
11948
+ if (scopeClear) {
11949
+ if (asset.asset_type !== "domain") {
11950
+ console.error("Error: Scope filtering is only available for domain assets.");
11951
+ process.exit(2);
11952
+ }
11953
+ await client.assets.update(asset.id, {
11954
+ scope_filter: null,
11955
+ scope_hosts: null
11956
+ });
11957
+ console.error("Scope settings cleared. Scans will now cover all subdomains by default.");
11958
+ asset = await client.assets.get(asset.id);
11959
+ } else if (options.include || options.exclude) {
11960
+ if (asset.asset_type !== "domain") {
11961
+ console.error("Error: Scope filtering is only available for domain assets.");
11962
+ process.exit(2);
11963
+ }
11964
+ const mode = options.include ? "include" : "exclude";
11965
+ const rawHosts = parseScopeHosts(options.include ?? options.exclude);
11966
+ validateScopeHosts(rawHosts);
11967
+ const scopeResult = await resolveScopeUpdate(asset, mode, rawHosts);
11968
+ if (!scopeResult) {
11969
+ process.exit(0);
11970
+ }
11971
+ await client.assets.update(asset.id, {
11972
+ scope_filter: scopeResult.scope_filter,
11973
+ scope_hosts: scopeResult.scope_hosts
11974
+ });
11975
+ asset = await client.assets.get(asset.id);
11976
+ }
11977
+ displayScope(asset);
11586
11978
  }
11587
11979
  await confirmAuthorization(targetDisplay, Boolean(options.yes));
11588
11980
  const result = await client.scans.create(params);
11981
+ const isOnPrem = result.deployment_type === "on_prem";
11589
11982
  if (opts.json || opts.output === "json") {
11590
11983
  printJson(result);
11591
11984
  return;
@@ -11593,13 +11986,37 @@ Examples:
11593
11986
  printSuccess(`Pentest started: ${result.domain_or_ip}`);
11594
11987
  console.log(` Profile ID: ${result.scan_profile_id}`);
11595
11988
  console.log(` Mode: ${result.scan_mode}`);
11989
+ if (isOnPrem) {
11990
+ printOnPremInstructions(result);
11991
+ if (options.watch && isTTY()) {
11992
+ const divider2 = import_picocolors8.default.dim("\u2500".repeat(37));
11993
+ console.log(`
11994
+ ${divider2}
11995
+ `);
11996
+ await waitForScoutConnection(client, result.scan_profile_id, {
11997
+ timeoutMs: 15 * 60 * 1e3,
11998
+ intervalMs: 5 * 1e3,
11999
+ useJson: false
12000
+ });
12001
+ await watchPentest(client, result.scan_profile_id, {
12002
+ timeoutMs: 1800 * 1e3,
12003
+ intervalMs: 5 * 1e3,
12004
+ useJson: false,
12005
+ showFindingsSummary: true
12006
+ });
12007
+ } else {
12008
+ console.log(`
12009
+ Watch connection: kinosec pentest watch ${result.scan_profile_id}`);
12010
+ }
12011
+ return;
12012
+ }
11596
12013
  if (!options.watch || !isTTY()) {
11597
12014
  console.log(` Status: ${result.scan_status}`);
11598
12015
  console.log(`
11599
12016
  Watch progress: kinosec pentest watch ${result.scan_profile_id}`);
11600
12017
  return;
11601
12018
  }
11602
- const divider = isTTY() ? import_picocolors6.default.dim("\u2500".repeat(37)) : "-".repeat(37);
12019
+ const divider = isTTY() ? import_picocolors8.default.dim("\u2500".repeat(37)) : "-".repeat(37);
11603
12020
  console.log(`
11604
12021
  ${divider}
11605
12022
  `);
@@ -11645,7 +12062,7 @@ Examples:
11645
12062
  ])
11646
12063
  );
11647
12064
  if (result.next_cursor) {
11648
- console.log(import_picocolors6.default.dim(`
12065
+ console.log(import_picocolors8.default.dim(`
11649
12066
  More results available. Use --cursor ${result.next_cursor}`));
11650
12067
  }
11651
12068
  } catch (error2) {
@@ -11672,6 +12089,7 @@ Examples:
11672
12089
  ["Target", detail.domain_or_ip],
11673
12090
  ["Mode", detail.scan_mode],
11674
12091
  ["Type", detail.scan_type],
12092
+ ...detail.deployment_type ? [["Deployment", detail.deployment_type === "on_prem" ? "On-Prem" : "Cloud"]] : [],
11675
12093
  ["Status", detail.scan_status],
11676
12094
  ["Activity", detail.current_activity || "(none)"],
11677
12095
  ["Created", detail.created_at],
@@ -11708,10 +12126,25 @@ Examples:
11708
12126
  const opts = program3.opts();
11709
12127
  const { token, apiUrl } = resolveToken(opts);
11710
12128
  const client = new KinoSecClient({ apiUrl, token });
12129
+ const useJson = Boolean(opts.json || opts.output === "json");
12130
+ const detail = await client.scans.get(id);
12131
+ if (detail.deployment_type === "on_prem") {
12132
+ try {
12133
+ const scout = await client.scans.scoutStatus(id);
12134
+ if (scout.status !== "connected") {
12135
+ await waitForScoutConnection(client, id, {
12136
+ timeoutMs: 15 * 60 * 1e3,
12137
+ intervalMs: parseInt(options.interval, 10) * 1e3,
12138
+ useJson
12139
+ });
12140
+ }
12141
+ } catch {
12142
+ }
12143
+ }
11711
12144
  await watchPentest(client, id, {
11712
12145
  timeoutMs: parseInt(options.timeout, 10) * 1e3,
11713
12146
  intervalMs: parseInt(options.interval, 10) * 1e3,
11714
- useJson: Boolean(opts.json || opts.output === "json"),
12147
+ useJson,
11715
12148
  showFindingsSummary: true
11716
12149
  });
11717
12150
  } catch (error2) {
@@ -11721,10 +12154,10 @@ Examples:
11721
12154
  }
11722
12155
 
11723
12156
  // src/commands/asset.ts
11724
- var import_picocolors8 = __toESM(require_picocolors(), 1);
12157
+ var import_picocolors10 = __toESM(require_picocolors(), 1);
11725
12158
 
11726
12159
  // src/commands/asset-auth.ts
11727
- var import_picocolors7 = __toESM(require_picocolors(), 1);
12160
+ var import_picocolors9 = __toESM(require_picocolors(), 1);
11728
12161
 
11729
12162
  // src/lib/resolve-asset.ts
11730
12163
  var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-/i;
@@ -11750,84 +12183,6 @@ or use pentest which auto-creates the asset:
11750
12183
  }
11751
12184
  }
11752
12185
 
11753
- // src/lib/prompt.ts
11754
- import { createInterface as createInterface2 } from "readline";
11755
- async function promptMasked(label) {
11756
- if (!isTTY() || !process.stdin.isTTY) {
11757
- console.error(`Cannot prompt for ${label} in non-interactive mode. Use flags instead.`);
11758
- process.exit(2);
11759
- }
11760
- process.stderr.write(`${label}: `);
11761
- return new Promise((resolve, reject) => {
11762
- const chars = [];
11763
- process.stdin.setRawMode(true);
11764
- process.stdin.resume();
11765
- process.stdin.setEncoding("utf-8");
11766
- const onData = (chunk) => {
11767
- for (const ch of chunk) {
11768
- if (ch === "\r" || ch === "\n") {
11769
- process.stdin.setRawMode(false);
11770
- process.stdin.pause();
11771
- process.stdin.removeListener("data", onData);
11772
- process.stderr.write("\n");
11773
- resolve(chars.join(""));
11774
- return;
11775
- } else if (ch === "") {
11776
- process.stdin.setRawMode(false);
11777
- process.stdin.pause();
11778
- process.stdin.removeListener("data", onData);
11779
- process.stderr.write("\n");
11780
- reject(new Error("Cancelled"));
11781
- return;
11782
- } else if (ch === "\x7F" || ch === "\b") {
11783
- if (chars.length > 0) {
11784
- chars.pop();
11785
- process.stderr.write("\b \b");
11786
- }
11787
- } else if (ch >= " ") {
11788
- chars.push(ch);
11789
- process.stderr.write("*");
11790
- }
11791
- }
11792
- };
11793
- process.stdin.on("data", onData);
11794
- });
11795
- }
11796
- async function promptText(label) {
11797
- if (!isTTY() || !process.stdin.isTTY) {
11798
- console.error(`Cannot prompt for ${label} in non-interactive mode. Use flags instead.`);
11799
- process.exit(2);
11800
- }
11801
- const rl = createInterface2({ input: process.stdin, output: process.stderr });
11802
- const answer = await new Promise((resolve) => {
11803
- rl.question(`${label}: `, resolve);
11804
- });
11805
- rl.close();
11806
- return answer;
11807
- }
11808
- async function promptSelect(message, options) {
11809
- if (!isTTY() || !process.stdin.isTTY) {
11810
- console.error("Cannot show interactive menu in non-interactive mode. Use flags instead.");
11811
- process.exit(2);
11812
- }
11813
- console.error(`
11814
- ${message}`);
11815
- for (let i = 0; i < options.length; i++) {
11816
- console.error(` ${i + 1}. ${options[i].label} \u2014 ${options[i].description}`);
11817
- }
11818
- const rl = createInterface2({ input: process.stdin, output: process.stderr });
11819
- const answer = await new Promise((resolve) => {
11820
- rl.question("> ", resolve);
11821
- });
11822
- rl.close();
11823
- const index = parseInt(answer, 10) - 1;
11824
- if (isNaN(index) || index < 0 || index >= options.length) {
11825
- console.error("Invalid selection.");
11826
- process.exit(2);
11827
- }
11828
- return options[index].value;
11829
- }
11830
-
11831
12186
  // src/commands/asset-auth.ts
11832
12187
  var VALID_AUTH_TYPES = ["bearer", "basic", "apikey", "session", "cookie"];
11833
12188
  var AUTH_TYPE_OPTIONS = [
@@ -11981,7 +12336,7 @@ Examples:
11981
12336
  }
11982
12337
  if (!creds.has_credentials) {
11983
12338
  console.log(`Asset: ${asset.asset_value}`);
11984
- console.log(`Auth Type: ${import_picocolors7.default.dim("(none)")}`);
12339
+ console.log(`Auth Type: ${import_picocolors9.default.dim("(none)")}`);
11985
12340
  console.log(`Has Credentials: no`);
11986
12341
  return;
11987
12342
  }
@@ -12002,9 +12357,9 @@ Examples:
12002
12357
  const hasAny = ta.has_username || ta.has_cookie || ta.has_bearer_token || ta.has_apikey;
12003
12358
  if (hasAny) {
12004
12359
  const detail = ta.has_username ? "username" : ta.has_bearer_token ? "bearer token" : ta.has_cookie ? "cookie" : "API key";
12005
- console.log(`Target Account: ${import_picocolors7.default.green("configured")} (${detail})`);
12360
+ console.log(`Target Account: ${import_picocolors9.default.green("configured")} (${detail})`);
12006
12361
  } else {
12007
- console.log(`Target Account: ${import_picocolors7.default.dim("not configured")}`);
12362
+ console.log(`Target Account: ${import_picocolors9.default.dim("not configured")}`);
12008
12363
  }
12009
12364
  }
12010
12365
  } catch (error2) {
@@ -12145,17 +12500,18 @@ Examples:
12145
12500
  return;
12146
12501
  }
12147
12502
  printTable(
12148
- ["ID", "Value", "Type", "Status", "Created"],
12503
+ ["ID", "Value", "Type", "Status", "Scope", "Created"],
12149
12504
  result.items.map((a) => [
12150
12505
  a.id,
12151
12506
  truncate(a.asset_value, 40),
12152
12507
  a.asset_type,
12153
12508
  a.verification_status,
12509
+ a.scope_filter ?? "all",
12154
12510
  new Date(a.created_at).toLocaleDateString()
12155
12511
  ])
12156
12512
  );
12157
12513
  if (result.next_cursor) {
12158
- console.log(import_picocolors8.default.dim(`
12514
+ console.log(import_picocolors10.default.dim(`
12159
12515
  More results available. Use --cursor ${result.next_cursor}`));
12160
12516
  }
12161
12517
  } catch (error2) {
@@ -12263,11 +12619,84 @@ Examples:
12263
12619
  handleError(error2);
12264
12620
  }
12265
12621
  });
12622
+ asset.command("scope").description("View or configure subdomain scope for an asset").argument("<id>", "Asset ID or domain").option("--include <hosts>", "Include only these subdomains (comma-separated)").option("--exclude <hosts>", "Exclude these subdomains (comma-separated)").option("--clear", "Clear scope settings").addHelpText("after", `
12623
+ Examples:
12624
+ $ kinosec asset scope abc123
12625
+ $ kinosec asset scope example.com --include api.example.com,app.example.com
12626
+ $ kinosec asset scope abc123 --exclude staging.example.com
12627
+ $ kinosec asset scope abc123 --clear`).action(async (id, options) => {
12628
+ try {
12629
+ validateScopeFlags({
12630
+ include: options.include,
12631
+ exclude: options.exclude,
12632
+ scopeClear: options.clear
12633
+ });
12634
+ const opts = program3.opts();
12635
+ const { token, apiUrl } = resolveToken(opts);
12636
+ const client = new KinoSecClient({ apiUrl, token });
12637
+ const asset2 = await resolveAsset(client, id);
12638
+ if (asset2.asset_type !== "domain") {
12639
+ console.error("Error: Scope filtering is only available for domain assets.");
12640
+ process.exit(2);
12641
+ }
12642
+ if (!options.include && !options.exclude && !options.clear) {
12643
+ if (opts.json || opts.output === "json") {
12644
+ printJson({
12645
+ scope_filter: asset2.scope_filter,
12646
+ scope_hosts: asset2.scope_hosts
12647
+ });
12648
+ return;
12649
+ }
12650
+ if (!asset2.scope_filter || !asset2.scope_hosts?.length) {
12651
+ console.log("Scope: all subdomains");
12652
+ } else {
12653
+ console.log(`Scope: ${asset2.scope_filter}`);
12654
+ console.log("Hosts:");
12655
+ for (const host of asset2.scope_hosts) {
12656
+ console.log(` - ${host}`);
12657
+ }
12658
+ }
12659
+ return;
12660
+ }
12661
+ if (options.clear) {
12662
+ await client.assets.update(asset2.id, {
12663
+ scope_filter: null,
12664
+ scope_hosts: null
12665
+ });
12666
+ if (opts.json || opts.output === "json") {
12667
+ printJson({ scope_filter: null, scope_hosts: null });
12668
+ return;
12669
+ }
12670
+ printSuccess("Scope settings cleared. Scans will now cover all subdomains by default.");
12671
+ return;
12672
+ }
12673
+ const mode = options.include ? "include" : "exclude";
12674
+ const rawHosts = parseScopeHosts(options.include ?? options.exclude);
12675
+ validateScopeHosts(rawHosts);
12676
+ const result = await resolveScopeUpdate(asset2, mode, rawHosts);
12677
+ if (!result) return;
12678
+ await client.assets.update(asset2.id, {
12679
+ scope_filter: result.scope_filter,
12680
+ scope_hosts: result.scope_hosts
12681
+ });
12682
+ if (opts.json || opts.output === "json") {
12683
+ printJson(result);
12684
+ return;
12685
+ }
12686
+ printSuccess(`Scope updated: ${result.scope_filter}`);
12687
+ console.log("Hosts:");
12688
+ for (const host of result.scope_hosts) {
12689
+ console.log(` - ${host}`);
12690
+ }
12691
+ } catch (error2) {
12692
+ handleError(error2);
12693
+ }
12694
+ });
12266
12695
  registerAssetAuthCommand(asset);
12267
12696
  }
12268
12697
 
12269
12698
  // src/commands/finding.ts
12270
- var import_picocolors9 = __toESM(require_picocolors(), 1);
12699
+ var import_picocolors11 = __toESM(require_picocolors(), 1);
12271
12700
  function formatPoc(poc) {
12272
12701
  if (poc == null) return null;
12273
12702
  if (typeof poc === "string") return poc.trim() || null;
@@ -12308,7 +12737,7 @@ Examples:
12308
12737
  ])
12309
12738
  );
12310
12739
  if (result.next_cursor) {
12311
- console.log(import_picocolors9.default.dim(`
12740
+ console.log(import_picocolors11.default.dim(`
12312
12741
  More results available. Use --cursor ${result.next_cursor}`));
12313
12742
  }
12314
12743
  } catch (error2) {
@@ -12396,7 +12825,7 @@ Examples:
12396
12825
  }
12397
12826
 
12398
12827
  // src/commands/report.ts
12399
- var import_picocolors10 = __toESM(require_picocolors(), 1);
12828
+ var import_picocolors12 = __toESM(require_picocolors(), 1);
12400
12829
  function renderMarkdown(data) {
12401
12830
  const { scan, findings } = data;
12402
12831
  const lines = [];
@@ -12473,7 +12902,7 @@ Examples:
12473
12902
  ])
12474
12903
  );
12475
12904
  if (result.next_cursor) {
12476
- console.log(import_picocolors10.default.dim(`
12905
+ console.log(import_picocolors12.default.dim(`
12477
12906
  More results available. Use --cursor ${result.next_cursor}`));
12478
12907
  }
12479
12908
  } catch (error2) {
@@ -12544,7 +12973,7 @@ Examples:
12544
12973
  }
12545
12974
 
12546
12975
  // src/commands/token.ts
12547
- var import_picocolors11 = __toESM(require_picocolors(), 1);
12976
+ var import_picocolors13 = __toESM(require_picocolors(), 1);
12548
12977
  function registerTokenCommand(program3) {
12549
12978
  const token = program3.command("token").description("Manage API tokens");
12550
12979
  token.command("list").description("List API tokens").option("--limit <n>", "Max results per page", "20").option("--cursor <cursor>", "Pagination cursor").addHelpText("after", `
@@ -12579,7 +13008,7 @@ Examples:
12579
13008
  ])
12580
13009
  );
12581
13010
  if (result.next_cursor) {
12582
- console.log(import_picocolors11.default.dim(`
13011
+ console.log(import_picocolors13.default.dim(`
12583
13012
  More results available. Use --cursor ${result.next_cursor}`));
12584
13013
  }
12585
13014
  } catch (error2) {
@@ -12606,7 +13035,7 @@ Examples:
12606
13035
  }
12607
13036
  printSuccess(`Token created: ${result.name}`);
12608
13037
  console.log("");
12609
- console.log(import_picocolors11.default.bold(" Token: ") + result.token);
13038
+ console.log(import_picocolors13.default.bold(" Token: ") + result.token);
12610
13039
  console.log("");
12611
13040
  printWarning("Save this token \u2014 it will not be shown again.");
12612
13041
  console.log(` ID: ${result.id}`);
@@ -12639,7 +13068,7 @@ Examples:
12639
13068
 
12640
13069
  // src/cli.ts
12641
13070
  var program2 = new Command();
12642
- program2.name("kinosec").description("KinoSec CLI \u2014 run pentests from your terminal").version("0.8.0", "-v, --version").option("--token <pat>", "API token (overrides KINOSEC_TOKEN)").option("--output <format>", "Output format: text or json", "text").option("--json", "Shorthand for --output json").option("--no-color", "Disable colors");
13071
+ program2.name("kinosec").description("KinoSec CLI \u2014 run pentests from your terminal").version("0.10.0", "-v, --version").option("--token <pat>", "API token (overrides KINOSEC_TOKEN)").option("--output <format>", "Output format: text or json", "text").option("--json", "Shorthand for --output json").option("--no-color", "Disable colors");
12643
13072
  registerAuthCommand(program2);
12644
13073
  registerMeCommand(program2);
12645
13074
  registerCreditsCommand(program2);
@@ -12648,7 +13077,7 @@ registerAssetCommand(program2);
12648
13077
  registerFindingCommand(program2);
12649
13078
  registerReportCommand(program2);
12650
13079
  registerTokenCommand(program2);
12651
- program2.parseAsync(process.argv).then(() => checkForUpdate("0.8.0")).catch(handleError);
13080
+ program2.parseAsync(process.argv).then(() => checkForUpdate("0.10.0")).catch(handleError);
12652
13081
  /*! Bundled license information:
12653
13082
 
12654
13083
  ky/distribution/index.js: