@kinosec/cli 0.10.1 → 0.10.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.
Files changed (3) hide show
  1. package/dist/cli.js +132 -71
  2. package/dist/cli.js.map +1 -1
  3. package/package.json +13 -13
package/dist/cli.js CHANGED
@@ -8048,12 +8048,8 @@ import { join as join2, dirname as dirname2 } from "path";
8048
8048
  // src/lib/config.ts
8049
8049
  import { existsSync, mkdirSync, readFileSync, writeFileSync, openSync, writeSync, closeSync, unlinkSync, constants } from "fs";
8050
8050
  import { join, dirname } from "path";
8051
- import { homedir, platform } from "os";
8051
+ import { homedir } from "os";
8052
8052
  function getConfigDir() {
8053
- const p = platform();
8054
- if (p === "win32") {
8055
- return join(process.env.APPDATA ?? join(homedir(), "AppData", "Roaming"), "kinosec");
8056
- }
8057
8053
  const xdg = process.env.XDG_CONFIG_HOME;
8058
8054
  if (xdg) return join(xdg, "kinosec");
8059
8055
  return join(homedir(), ".config", "kinosec");
@@ -8074,7 +8070,7 @@ function readJsonFile(path) {
8074
8070
  function writeJsonFile(path, data, restrictPermissions = false) {
8075
8071
  ensureDir(dirname(path));
8076
8072
  const content = JSON.stringify(data, null, 2) + "\n";
8077
- if (restrictPermissions && platform() !== "win32") {
8073
+ if (restrictPermissions) {
8078
8074
  const fd = openSync(path, constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC, 384);
8079
8075
  writeSync(fd, content);
8080
8076
  closeSync(fd);
@@ -8439,13 +8435,10 @@ ${import_picocolors3.default.dim("use of its services.")}
8439
8435
  var import_picocolors4 = __toESM(require_picocolors(), 1);
8440
8436
 
8441
8437
  // src/lib/validate-mode.ts
8438
+ var VALID_SCAN_MODES = ["full", "quick", "htb", "defcon"];
8442
8439
  function validateScanMode(mode) {
8443
- if (mode === "quick") {
8444
- console.error("Quick mode is temporarily unavailable. Please use --mode full (the default).");
8445
- process.exit(2);
8446
- }
8447
- if (mode !== "full") {
8448
- console.error(`Invalid mode "${mode}". Must be: full`);
8440
+ if (!VALID_SCAN_MODES.includes(mode)) {
8441
+ console.error(`Invalid mode "${mode}". Must be one of: ${VALID_SCAN_MODES.join(", ")}.`);
8449
8442
  process.exit(2);
8450
8443
  }
8451
8444
  }
@@ -12019,10 +12012,9 @@ function extractPhase(entries) {
12019
12012
  return null;
12020
12013
  }
12021
12014
  function printActivityLines(entries, tty3) {
12022
- for (let i = 0; i < entries.length; i++) {
12023
- const isLastEntry = i === entries.length - 1;
12024
- const prompt = tty3 ? import_picocolors5.default.green(">") : ">";
12025
- const text = tty3 ? isLastEntry ? import_picocolors5.default.green(entries[i]) : import_picocolors5.default.dim(import_picocolors5.default.green(entries[i])) : entries[i];
12015
+ for (const entry of entries) {
12016
+ const prompt = tty3 ? import_picocolors5.default.dim(import_picocolors5.default.green(">")) : ">";
12017
+ const text = tty3 ? import_picocolors5.default.dim(import_picocolors5.default.green(entry)) : entry;
12026
12018
  console.log(`${prompt} ${text}`);
12027
12019
  }
12028
12020
  }
@@ -12049,7 +12041,7 @@ async function watchPentest(client, profileId, options) {
12049
12041
  spinner.text = import_picocolors5.default.green(currentPhase ? `[${currentPhase}] ${buzz}...` : `${buzz}...`);
12050
12042
  }
12051
12043
  if (!useJson && tty3) {
12052
- spinner = ora({ text: "Fetching pentest status...", color: "green", isEnabled: true }).start();
12044
+ spinner = ora({ text: "Fetching pentest status...", color: "green", isEnabled: true, discardStdin: false }).start();
12053
12045
  cycler = createBuzzwordCycler(SCAN_EXECUTION_PHRASES, 2500, () => {
12054
12046
  updateSpinnerText();
12055
12047
  });
@@ -12064,71 +12056,134 @@ async function watchPentest(client, profileId, options) {
12064
12056
  process.exit(0);
12065
12057
  });
12066
12058
  const showFullDetails = await resolveShowFullDetails(client);
12067
- while (true) {
12068
- const scanDetail = await client.scans.get(profileId);
12069
- const totalMs = Date.now() - loopStart;
12070
- if (useJson) {
12071
- const elapsed = Math.round(totalMs / 1e3);
12072
- process.stdout.write(
12073
- JSON.stringify({ ...scanDetail, elapsed_seconds: elapsed }) + "\n"
12074
- );
12075
- } else {
12076
- const normalized = normalizeActivityHistory(scanDetail.activity_history);
12077
- const filtered = filterActivityForTier(normalized, showFullDetails);
12078
- if (printedCount > filtered.length) printedCount = 0;
12079
- const newEntries = filtered.slice(printedCount);
12080
- if (newEntries.length > 0) {
12081
- if (spinner) spinner.stop();
12082
- printActivityLines(newEntries, tty3);
12083
- printedCount += newEntries.length;
12084
- if (spinner) spinner = ora({ color: "green", isEnabled: true }).start();
12085
- }
12086
- currentPhase = extractPhase(filtered);
12087
- updateSpinnerText();
12088
- }
12089
- const terminalStatuses = ["Completed", "Failed", "Stopped"];
12090
- if (terminalStatuses.some((s) => scanDetail.scan_status.includes(s))) {
12091
- cleanup();
12059
+ let consecutiveErrors = 0;
12060
+ const MAX_CONSECUTIVE_ERRORS = 2;
12061
+ let deferredExitCode = null;
12062
+ try {
12063
+ while (true) {
12064
+ let scanDetail;
12065
+ try {
12066
+ scanDetail = await client.scans.get(profileId);
12067
+ consecutiveErrors = 0;
12068
+ } catch (error2) {
12069
+ if (error2 instanceof ApiError) {
12070
+ if (error2.status === 401 || error2.status === 403) {
12071
+ throw error2;
12072
+ }
12073
+ if (error2.status === 404) {
12074
+ if (!useJson) {
12075
+ const icon2 = tty3 ? import_picocolors5.default.yellow(sym.warn) : sym.warn;
12076
+ console.log(`
12077
+ ${icon2} Pentest is no longer available.`);
12078
+ console.log(` Check status: kinosec pentest get ${profileId}`);
12079
+ }
12080
+ break;
12081
+ }
12082
+ }
12083
+ consecutiveErrors++;
12084
+ if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
12085
+ if (!useJson) {
12086
+ const icon2 = tty3 ? import_picocolors5.default.red(sym.fail) : sym.fail;
12087
+ console.log(`
12088
+ ${icon2} Lost connection to pentest.`);
12089
+ console.log(` Check status: kinosec pentest get ${profileId}`);
12090
+ }
12091
+ break;
12092
+ }
12093
+ if (spinner) {
12094
+ spinner.text = import_picocolors5.default.yellow("Connection lost, retrying...");
12095
+ }
12096
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
12097
+ continue;
12098
+ }
12099
+ const totalMs = Date.now() - loopStart;
12100
+ if (useJson) {
12101
+ const elapsed = Math.round(totalMs / 1e3);
12102
+ process.stdout.write(
12103
+ JSON.stringify({ ...scanDetail, elapsed_seconds: elapsed }) + "\n"
12104
+ );
12105
+ } else {
12106
+ const normalized = normalizeActivityHistory(scanDetail.activity_history);
12107
+ const filtered = filterActivityForTier(normalized, showFullDetails);
12108
+ if (printedCount > filtered.length) printedCount = 0;
12109
+ const newEntries = filtered.slice(printedCount);
12110
+ if (newEntries.length > 0) {
12111
+ if (spinner) spinner.stop();
12112
+ printActivityLines(newEntries, tty3);
12113
+ printedCount += newEntries.length;
12114
+ if (spinner) spinner = ora({ color: "green", isEnabled: true, discardStdin: false }).start();
12115
+ }
12116
+ currentPhase = extractPhase(filtered);
12117
+ updateSpinnerText();
12118
+ }
12119
+ const status = scanDetail.scan_status;
12120
+ if (status === "Running...") {
12121
+ if (Date.now() - loopStart > timeoutMs) {
12122
+ const secs = Math.round(timeoutMs / 1e3);
12123
+ console.error(`
12124
+ Timeout: pentest still running after ${secs}s.`);
12125
+ console.error(`Check status: kinosec pentest get ${profileId}`);
12126
+ deferredExitCode = 3;
12127
+ break;
12128
+ }
12129
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
12130
+ continue;
12131
+ }
12092
12132
  if (useJson) break;
12093
- if (scanDetail.scan_status.includes("Failed")) {
12133
+ if (status === "Completed") {
12134
+ const totalDur = formatDuration(Date.now() - loopStart);
12135
+ const domain = tty3 ? import_picocolors5.default.bold(scanDetail.domain_or_ip) : scanDetail.domain_or_ip;
12136
+ const icon2 = tty3 ? import_picocolors5.default.green(sym.success) : sym.success;
12137
+ const dur = tty3 ? import_picocolors5.default.dim(totalDur) : totalDur;
12138
+ console.log(`
12139
+ ${icon2} Pentest completed: ${domain} ${dur}`);
12140
+ if (showFindingsSummary) {
12141
+ await printFindingsSummary(client, profileId, tty3);
12142
+ }
12143
+ const cmd = `kinosec finding list ${profileId}`;
12144
+ const viewLabel = tty3 ? import_picocolors5.default.dim("View details:") : "View details:";
12145
+ const viewCmd = tty3 ? import_picocolors5.default.cyan(cmd) : cmd;
12146
+ console.log(`
12147
+ ${viewLabel} ${viewCmd}`);
12148
+ console.log();
12149
+ break;
12150
+ }
12151
+ if (status === "Failed") {
12094
12152
  const icon2 = tty3 ? import_picocolors5.default.red(sym.fail) : sym.fail;
12095
12153
  const reason = scanDetail.failure_reason || "unknown reason";
12096
12154
  console.log(`
12097
12155
  ${icon2} Pentest failed: ${reason}`);
12098
- process.exit(1);
12156
+ deferredExitCode = 1;
12157
+ break;
12099
12158
  }
12100
- if (scanDetail.scan_status.includes("Stopped")) {
12159
+ if (status === "Stopping...") {
12101
12160
  const icon2 = tty3 ? import_picocolors5.default.yellow(sym.warn) : sym.warn;
12102
12161
  console.log(`
12103
- ${icon2} Pentest was cancelled.`);
12162
+ ${icon2} Pentest is being stopped.`);
12104
12163
  break;
12105
12164
  }
12106
- const totalDur = formatDuration(Date.now() - loopStart);
12107
- const domain = tty3 ? import_picocolors5.default.bold(scanDetail.domain_or_ip) : scanDetail.domain_or_ip;
12108
- const icon = tty3 ? import_picocolors5.default.green(sym.success) : sym.success;
12109
- const dur = tty3 ? import_picocolors5.default.dim(totalDur) : totalDur;
12110
- console.log(`
12111
- ${icon} Pentest completed: ${domain} ${dur}`);
12112
- if (showFindingsSummary) {
12113
- await printFindingsSummary(client, profileId, tty3);
12165
+ if (status === "Not Yet Started") {
12166
+ const icon2 = tty3 ? import_picocolors5.default.yellow(sym.warn) : sym.warn;
12167
+ console.log(`
12168
+ ${icon2} Pentest was stopped.`);
12169
+ break;
12114
12170
  }
12115
- const cmd = `kinosec finding list ${profileId}`;
12116
- const viewLabel = tty3 ? import_picocolors5.default.dim("View details:") : "View details:";
12117
- const viewCmd = tty3 ? import_picocolors5.default.cyan(cmd) : cmd;
12171
+ if (status === "Scheduled") {
12172
+ const icon2 = tty3 ? import_picocolors5.default.yellow(sym.warn) : sym.warn;
12173
+ console.log(`
12174
+ ${icon2} Pentest is scheduled, not currently running.`);
12175
+ break;
12176
+ }
12177
+ const icon = tty3 ? import_picocolors5.default.yellow(sym.warn) : sym.warn;
12118
12178
  console.log(`
12119
- ${viewLabel} ${viewCmd}`);
12120
- console.log();
12179
+ ${icon} Unexpected status: ${status}`);
12121
12180
  break;
12122
12181
  }
12123
- if (Date.now() - loopStart > timeoutMs) {
12124
- cleanup();
12125
- const secs = Math.round(timeoutMs / 1e3);
12126
- console.error(`
12127
- Timeout: pentest still running after ${secs}s.`);
12128
- console.error(`Check status: kinosec pentest get ${profileId}`);
12129
- process.exit(3);
12130
- }
12131
- await new Promise((resolve) => setTimeout(resolve, intervalMs));
12182
+ } finally {
12183
+ cleanup();
12184
+ }
12185
+ if (deferredExitCode !== null) {
12186
+ process.exit(deferredExitCode);
12132
12187
  }
12133
12188
  }
12134
12189
  async function printFindingsSummary(client, profileId, tty3) {
@@ -12508,7 +12563,7 @@ function parseTarget(input) {
12508
12563
 
12509
12564
  // src/commands/pentest.ts
12510
12565
  function registerPentestCommand(program3) {
12511
- 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", `
12566
+ 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, quick, htb, or defcon", "full").option("--flag <prefix>", "CTF flag prefix (defcon mode only, default HTB)").option("--context <text>", "CTF focus hint (defcon mode only)").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", `
12512
12567
  Examples:
12513
12568
  $ kinosec pentest -t delikado.org
12514
12569
  $ kinosec pentest --profile abc123
@@ -12584,6 +12639,12 @@ Examples:
12584
12639
  asset_id: asset.id,
12585
12640
  scan_mode: options.mode
12586
12641
  };
12642
+ if (options.mode === "defcon") {
12643
+ assetParams.flag_format = options.flag ?? "HTB";
12644
+ if (options.context) assetParams.ctf_context = options.context;
12645
+ } else if (options.flag || options.context) {
12646
+ console.error("Warning: --flag/--context apply only to --mode defcon; ignoring.");
12647
+ }
12587
12648
  if (options.deployment !== "cloud") {
12588
12649
  assetParams.deployment_type = options.deployment;
12589
12650
  }
@@ -13732,7 +13793,7 @@ Examples:
13732
13793
 
13733
13794
  // src/cli.ts
13734
13795
  var program2 = new Command();
13735
- program2.name("kinosec").description("KinoSec CLI \u2014 run pentests from your terminal").version("0.10.1", "-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");
13796
+ program2.name("kinosec").description("KinoSec CLI \u2014 run pentests from your terminal").version("0.10.3", "-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");
13736
13797
  registerAuthCommand(program2);
13737
13798
  registerMeCommand(program2);
13738
13799
  registerCreditsCommand(program2);
@@ -13741,7 +13802,7 @@ registerAssetCommand(program2);
13741
13802
  registerFindingCommand(program2);
13742
13803
  registerReportCommand(program2);
13743
13804
  registerTokenCommand(program2);
13744
- program2.parseAsync(process.argv).then(() => checkForUpdate("0.10.1")).catch(handleError);
13805
+ program2.parseAsync(process.argv).then(() => checkForUpdate("0.10.3")).catch(handleError);
13745
13806
  /*! Bundled license information:
13746
13807
 
13747
13808
  ky/distribution/index.js: