@kinosec/cli 0.10.3 → 0.11.2

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
@@ -6732,10 +6732,11 @@ function parseApiError(body, status) {
6732
6732
  });
6733
6733
  }
6734
6734
  const b = body;
6735
+ const detail = typeof b.detail === "string" ? b.detail : typeof b.error === "string" ? b.error : "An unexpected error occurred";
6735
6736
  return new ApiError({
6736
6737
  status: typeof b.status === "number" ? b.status : status,
6737
6738
  code: typeof b.code === "string" ? b.code : "unknown_error",
6738
- detail: typeof b.detail === "string" ? b.detail : "An unexpected error occurred",
6739
+ detail,
6739
6740
  requestId: typeof b.request_id === "string" ? b.request_id : "unknown"
6740
6741
  });
6741
6742
  }
@@ -8434,28 +8435,111 @@ ${import_picocolors3.default.dim("use of its services.")}
8434
8435
  // src/commands/schedule.ts
8435
8436
  var import_picocolors4 = __toESM(require_picocolors(), 1);
8436
8437
 
8437
- // src/lib/validate-mode.ts
8438
- var VALID_SCAN_MODES = ["full", "quick", "htb", "defcon"];
8439
- function validateScanMode(mode) {
8440
- if (!VALID_SCAN_MODES.includes(mode)) {
8441
- console.error(`Invalid mode "${mode}". Must be one of: ${VALID_SCAN_MODES.join(", ")}.`);
8442
- process.exit(2);
8438
+ // src/lib/validate-schedule.ts
8439
+ var SCHEDULE_PATTERNS = ["once", "daily", "weekly", "biweekly", "monthly"];
8440
+ var SCHEDULE_MODES = ["quick", "full"];
8441
+ var TIME_OF_DAY = /^([01]?\d|2[0-3]):[0-5]\d$/;
8442
+ var DIGITS = /^\d+$/;
8443
+ var ONCE_AT = /^(\d{4})-(\d{2})-(\d{2})T([01]\d|2[0-3]):[0-5]\d(:[0-5]\d(\.\d{3})?)?(Z|[+-]([01]\d|2[0-3]):[0-5]\d)$/;
8444
+ function scheduleInputError(flags, now) {
8445
+ const { pattern, mode, at, time, timezone, dayOfWeek, dayOfMonth } = flags;
8446
+ if (pattern !== void 0 && !SCHEDULE_PATTERNS.includes(pattern)) {
8447
+ return `Invalid --pattern "${pattern}". Must be one of: ${SCHEDULE_PATTERNS.join(", ")}.`;
8448
+ }
8449
+ if (mode !== void 0 && !SCHEDULE_MODES.includes(mode)) {
8450
+ return `Invalid --mode "${mode}". Schedules support: ${SCHEDULE_MODES.join(", ")}.`;
8451
+ }
8452
+ if (at !== void 0) {
8453
+ const atError = onceAtError(at, now);
8454
+ if (atError) return atError;
8455
+ }
8456
+ if (time !== void 0 && !TIME_OF_DAY.test(time)) {
8457
+ return `Invalid --time "${time}". Use 24-hour HH:MM, e.g. 09:00.`;
8458
+ }
8459
+ if (timezone !== void 0 && !isKnownTimezone(timezone)) {
8460
+ return `Invalid --timezone "${timezone}". Use an IANA name, e.g. America/New_York.`;
8461
+ }
8462
+ if (dayOfWeek !== void 0 && !isWholeNumberIn(dayOfWeek, 0, 6)) {
8463
+ return `Invalid --day-of-week "${dayOfWeek}". Use a whole number 0-6 (0=Sunday).`;
8464
+ }
8465
+ if (dayOfMonth !== void 0 && !isWholeNumberIn(dayOfMonth, 1, 31)) {
8466
+ return `Invalid --day-of-month "${dayOfMonth}". Use a whole number 1-31.`;
8467
+ }
8468
+ if (pattern === void 0 && at !== void 0) return "--at requires --pattern once.";
8469
+ if (pattern === void 0) return null;
8470
+ return patternFitError(pattern, flags);
8471
+ }
8472
+ function patternFitError(pattern, flags) {
8473
+ if (pattern === "once" && flags.at === void 0) {
8474
+ return "--at is required for once schedules: YYYY-MM-DDTHH:MM followed by Z or \xB1HH:MM.";
8475
+ }
8476
+ if (pattern === "once" && flags.time !== void 0) {
8477
+ return "--time does not apply to once schedules; put the time in --at.";
8478
+ }
8479
+ if (pattern !== "once" && flags.at !== void 0) {
8480
+ return "--at only applies to once schedules.";
8481
+ }
8482
+ const isWeekly = pattern === "weekly" || pattern === "biweekly";
8483
+ if (isWeekly && flags.dayOfWeek === void 0) {
8484
+ return `--day-of-week is required for ${pattern} schedules.`;
8485
+ }
8486
+ if (!isWeekly && flags.dayOfWeek !== void 0) {
8487
+ return "--day-of-week only applies to weekly and biweekly schedules.";
8443
8488
  }
8489
+ if (pattern === "monthly" && flags.dayOfMonth === void 0) {
8490
+ return "--day-of-month is required for monthly schedules.";
8491
+ }
8492
+ if (pattern !== "monthly" && flags.dayOfMonth !== void 0) {
8493
+ return "--day-of-month only applies to monthly schedules.";
8494
+ }
8495
+ return null;
8496
+ }
8497
+ function onceAtError(at, now) {
8498
+ const match = ONCE_AT.exec(at);
8499
+ const ms = Date.parse(at);
8500
+ if (!match || Number.isNaN(ms) || !isCalendarDate(Number(match[1]), Number(match[2]), Number(match[3]))) {
8501
+ return `Invalid --at "${at}". Use YYYY-MM-DDTHH:MM followed by Z or \xB1HH:MM, on a real date.`;
8502
+ }
8503
+ if (ms <= now.getTime()) return `--at "${at}" is in the past. Pick a future time.`;
8504
+ return null;
8505
+ }
8506
+ function isCalendarDate(year, month, day) {
8507
+ const date = new Date(Date.UTC(year, month - 1, day));
8508
+ return date.getUTCMonth() === month - 1 && date.getUTCDate() === day;
8509
+ }
8510
+ function isKnownTimezone(timezone) {
8511
+ try {
8512
+ new Intl.DateTimeFormat("en-US", { timeZone: timezone });
8513
+ return true;
8514
+ } catch (error2) {
8515
+ if (error2 instanceof RangeError) return false;
8516
+ throw error2;
8517
+ }
8518
+ }
8519
+ function isWholeNumberIn(value, min, max) {
8520
+ if (!DIGITS.test(value)) return false;
8521
+ const n = Number(value);
8522
+ return n >= min && n <= max;
8444
8523
  }
8445
8524
 
8446
8525
  // src/commands/schedule.ts
8447
- var VALID_PATTERNS = ["once", "daily", "weekly", "biweekly", "monthly"];
8448
8526
  var DAY_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
8449
8527
  function extractTime(scheduledAt) {
8450
8528
  if (!scheduledAt) return "\u2014";
8451
8529
  const d = new Date(scheduledAt);
8452
8530
  return d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", hour12: false });
8453
8531
  }
8532
+ function extractDate(scheduledAt) {
8533
+ if (!scheduledAt) return "\u2014";
8534
+ const d = new Date(scheduledAt);
8535
+ const pad = (n) => String(n).padStart(2, "0");
8536
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
8537
+ }
8454
8538
  function formatPattern(s) {
8455
8539
  const time = extractTime(s.scheduled_at);
8456
8540
  switch (s.recurrence_pattern) {
8457
8541
  case "once":
8458
- return `Once at ${time}`;
8542
+ return `Once on ${extractDate(s.scheduled_at)} at ${time}`;
8459
8543
  case "daily":
8460
8544
  return `Daily at ${time}`;
8461
8545
  case "weekly":
@@ -8468,6 +8552,22 @@ function formatPattern(s) {
8468
8552
  return s.recurrence_pattern;
8469
8553
  }
8470
8554
  }
8555
+ function modeTypedOnPentest(pentestCmd) {
8556
+ return pentestCmd.getOptionValueSource("mode") === "cli" ? String(pentestCmd.getOptionValue("mode")) : void 0;
8557
+ }
8558
+ function yesTypedOnPentest(pentestCmd) {
8559
+ return pentestCmd.getOptionValueSource("yes") === "cli";
8560
+ }
8561
+ function exitOnInputError(flags) {
8562
+ const error2 = scheduleInputError(flags, /* @__PURE__ */ new Date());
8563
+ if (!error2) return;
8564
+ console.error(error2);
8565
+ process.exit(2);
8566
+ }
8567
+ function scheduleMode(mode) {
8568
+ if (mode === "quick" || mode === "full") return mode;
8569
+ throw new Error(`Unchecked schedule mode "${mode}".`);
8570
+ }
8471
8571
  function registerScheduleCommand(pentestCmd) {
8472
8572
  const schedule = pentestCmd.command("schedule").description("Manage scheduled pentests");
8473
8573
  schedule.command("list").description("List scheduled pentests").option("--limit <n>", "Max results per page", "20").option("--cursor <cursor>", "Pagination cursor").option("--status <status>", "Filter: active, paused, or all", "all").addHelpText("after", `
@@ -8542,36 +8642,35 @@ Examples:
8542
8642
  handleError(error2);
8543
8643
  }
8544
8644
  });
8545
- 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", `
8645
+ 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 (default: full)", "full").option("--at <datetime>", "Run time for --pattern once: YYYY-MM-DDTHH:MM followed by Z or \xB1HH:MM").option("--time <HH:MM>", "Time of day, 24h, for recurring patterns (default: 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", `
8546
8646
  Examples:
8547
8647
  $ kinosec pentest schedule create --asset abc123 --pattern weekly --day-of-week 1 --time 09:00
8548
8648
  $ kinosec pentest schedule create --asset abc123 --pattern daily --time 14:00
8549
- $ kinosec pentest schedule create --asset abc123 --pattern monthly --day-of-month 15 --time 03:00 --timezone UTC`).action(async (options) => {
8550
- if (!VALID_PATTERNS.includes(options.pattern)) {
8551
- console.error(`Invalid pattern "${options.pattern}". Must be: ${VALID_PATTERNS.join(", ")}`);
8552
- process.exit(2);
8553
- }
8554
- validateScanMode(options.mode);
8555
- if ((options.pattern === "weekly" || options.pattern === "biweekly") && options.dayOfWeek === void 0) {
8556
- console.error(`--day-of-week is required for ${options.pattern} schedules.`);
8557
- process.exit(2);
8558
- }
8559
- if (options.pattern === "monthly" && options.dayOfMonth === void 0) {
8560
- console.error("--day-of-month is required for monthly schedules.");
8561
- process.exit(2);
8562
- }
8649
+ $ kinosec pentest schedule create --asset abc123 --pattern monthly --day-of-month 15 --time 03:00 --timezone UTC
8650
+ $ kinosec pentest schedule create --asset abc123 --pattern once --at 2099-12-01T09:00+01:00`).action(async (options) => {
8651
+ const mode = modeTypedOnPentest(pentestCmd) ?? options.mode;
8652
+ exitOnInputError({
8653
+ pattern: options.pattern,
8654
+ mode,
8655
+ at: options.at,
8656
+ time: options.time,
8657
+ timezone: options.timezone,
8658
+ dayOfWeek: options.dayOfWeek,
8659
+ dayOfMonth: options.dayOfMonth
8660
+ });
8563
8661
  try {
8564
8662
  const opts = schedule.parent.parent.opts();
8565
8663
  const { token, apiUrl } = resolveToken(opts);
8566
8664
  const client = new KinoSecClient({ apiUrl, token });
8567
8665
  const params = {
8568
- asset_id: options.domain_or_ip,
8569
- name: options.name ?? `${options.pattern} ${options.mode} pentest`,
8570
- scan_mode: options.mode,
8666
+ asset_id: options.asset,
8667
+ name: options.name ?? `${options.pattern} ${mode} pentest`,
8668
+ scan_mode: scheduleMode(mode),
8571
8669
  recurrence_pattern: options.pattern,
8572
- time_of_day: options.time,
8573
8670
  timezone: options.timezone
8574
8671
  };
8672
+ if (options.pattern === "once") params.once_at = new Date(options.at).toISOString();
8673
+ else params.time_of_day = options.time ?? "09:00";
8575
8674
  if (options.dayOfWeek !== void 0) params.day_of_week = parseInt(options.dayOfWeek, 10);
8576
8675
  if (options.dayOfMonth !== void 0) params.day_of_month = parseInt(options.dayOfMonth, 10);
8577
8676
  const s = await client.schedules.create(params);
@@ -8589,17 +8688,26 @@ Manage: kinosec pentest schedule get ${s.id}`);
8589
8688
  handleError(error2);
8590
8689
  }
8591
8690
  });
8592
- 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", `
8691
+ 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("--at <datetime>", "Run time with --pattern once: ISO date and time with an offset").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", `
8593
8692
  Examples:
8594
8693
  $ kinosec pentest schedule update abc123 --pattern daily --time 14:00
8595
- $ kinosec pentest schedule update abc123 --time 14:00`).action(async (id, options) => {
8694
+ $ kinosec pentest schedule update abc123 --time 14:00
8695
+ $ kinosec pentest schedule update abc123 --pattern once --at 2099-12-01T09:00Z`).action(async (id, options) => {
8696
+ const mode = modeTypedOnPentest(pentestCmd) ?? options.mode;
8697
+ exitOnInputError({
8698
+ pattern: options.pattern,
8699
+ mode,
8700
+ at: options.at,
8701
+ time: options.time,
8702
+ timezone: options.timezone,
8703
+ dayOfWeek: options.dayOfWeek,
8704
+ dayOfMonth: options.dayOfMonth
8705
+ });
8596
8706
  const params = {};
8597
8707
  if (options.pattern) params.recurrence_pattern = options.pattern;
8598
8708
  if (options.name) params.name = options.name;
8599
- if (options.mode) {
8600
- validateScanMode(options.mode);
8601
- params.scan_mode = options.mode;
8602
- }
8709
+ if (mode) params.scan_mode = scheduleMode(mode);
8710
+ if (options.at !== void 0) params.once_at = new Date(options.at).toISOString();
8603
8711
  if (options.time) params.time_of_day = options.time;
8604
8712
  if (options.dayOfWeek !== void 0) params.day_of_week = parseInt(options.dayOfWeek, 10);
8605
8713
  if (options.dayOfMonth !== void 0) params.day_of_month = parseInt(options.dayOfMonth, 10);
@@ -8629,7 +8737,7 @@ Examples:
8629
8737
  $ kinosec pentest schedule delete abc123
8630
8738
  $ kinosec pentest schedule delete abc123 --yes`).action(async (id, options) => {
8631
8739
  try {
8632
- await confirmDestructive(`Delete schedule ${id}?`, Boolean(options.yes));
8740
+ await confirmDestructive(`Delete schedule ${id}?`, Boolean(options.yes) || yesTypedOnPentest(pentestCmd));
8633
8741
  const opts = schedule.parent.parent.opts();
8634
8742
  const { token, apiUrl } = resolveToken(opts);
8635
8743
  const client = new KinoSecClient({ apiUrl, token });
@@ -11903,18 +12011,6 @@ function normalizeActivityHistory(raw) {
11903
12011
  return JSON.stringify(entry);
11904
12012
  });
11905
12013
  }
11906
- function filterActivityForTier(entries, showFullDetails) {
11907
- if (showFullDetails) return entries;
11908
- return entries.filter((line) => !/^\s{4,}/.test(line)).map((line) => {
11909
- if (/\] Running:/.test(line)) {
11910
- return line.replace(
11911
- /Running:.*/,
11912
- "Running: [upgrade to Security Pro for full details]"
11913
- );
11914
- }
11915
- return line;
11916
- });
11917
- }
11918
12014
 
11919
12015
  // src/lib/buzzwords.ts
11920
12016
  var SCAN_EXECUTION_PHRASES = [
@@ -12018,15 +12114,6 @@ function printActivityLines(entries, tty3) {
12018
12114
  console.log(`${prompt} ${text}`);
12019
12115
  }
12020
12116
  }
12021
- async function resolveShowFullDetails(client) {
12022
- try {
12023
- const me = await client.me.get();
12024
- const slug = me.plan.slug;
12025
- return slug === "security_pro" || slug === "enterprise";
12026
- } catch {
12027
- return false;
12028
- }
12029
- }
12030
12117
  async function watchPentest(client, profileId, options) {
12031
12118
  const { timeoutMs, intervalMs, useJson, showFindingsSummary } = options;
12032
12119
  const tty3 = isTTY();
@@ -12055,7 +12142,6 @@ async function watchPentest(client, profileId, options) {
12055
12142
  console.log("\nWatch cancelled. Pentest is still running.");
12056
12143
  process.exit(0);
12057
12144
  });
12058
- const showFullDetails = await resolveShowFullDetails(client);
12059
12145
  let consecutiveErrors = 0;
12060
12146
  const MAX_CONSECUTIVE_ERRORS = 2;
12061
12147
  let deferredExitCode = null;
@@ -12103,17 +12189,16 @@ ${icon2} Lost connection to pentest.`);
12103
12189
  JSON.stringify({ ...scanDetail, elapsed_seconds: elapsed }) + "\n"
12104
12190
  );
12105
12191
  } 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);
12192
+ const entries = normalizeActivityHistory(scanDetail.activity_history);
12193
+ if (printedCount > entries.length) printedCount = 0;
12194
+ const newEntries = entries.slice(printedCount);
12110
12195
  if (newEntries.length > 0) {
12111
12196
  if (spinner) spinner.stop();
12112
12197
  printActivityLines(newEntries, tty3);
12113
12198
  printedCount += newEntries.length;
12114
12199
  if (spinner) spinner = ora({ color: "green", isEnabled: true, discardStdin: false }).start();
12115
12200
  }
12116
- currentPhase = extractPhase(filtered);
12201
+ currentPhase = extractPhase(entries);
12117
12202
  updateSpinnerText();
12118
12203
  }
12119
12204
  const status = scanDetail.scan_status;
@@ -12316,6 +12401,15 @@ Timeout: Scout did not connect within ${secs}s.`);
12316
12401
  }
12317
12402
  }
12318
12403
 
12404
+ // src/lib/validate-mode.ts
12405
+ var VALID_SCAN_MODES = ["full", "quick", "htb", "defcon", "exploit-only"];
12406
+ function validateScanMode(mode) {
12407
+ if (!VALID_SCAN_MODES.includes(mode)) {
12408
+ console.error(`Invalid mode "${mode}". Must be one of: ${VALID_SCAN_MODES.join(", ")}.`);
12409
+ process.exit(2);
12410
+ }
12411
+ }
12412
+
12319
12413
  // src/lib/scope.ts
12320
12414
  var import_picocolors7 = __toESM(require_picocolors(), 1);
12321
12415
 
@@ -12561,16 +12655,226 @@ function parseTarget(input) {
12561
12655
  };
12562
12656
  }
12563
12657
 
12658
+ // src/lib/graybox.ts
12659
+ import { spawnSync } from "child_process";
12660
+ import { existsSync as existsSync3, mkdtempSync, readFileSync as readFileSync3, rmSync, statSync } from "fs";
12661
+ import { tmpdir } from "os";
12662
+ import { dirname as dirname3, join as join3 } from "path";
12663
+ import { fileURLToPath } from "url";
12664
+ var SOURCE_BYTES_WARN = 2e5;
12665
+ function findPython() {
12666
+ const candidates = [
12667
+ { cmd: "python3", args: [] },
12668
+ { cmd: "py", args: ["-3"] },
12669
+ { cmd: "python", args: [] }
12670
+ ];
12671
+ for (const interp of candidates) {
12672
+ try {
12673
+ const result = spawnSync(interp.cmd, [...interp.args, "--version"], { stdio: "ignore" });
12674
+ if (!result.error && result.status === 0) return interp;
12675
+ } catch {
12676
+ }
12677
+ }
12678
+ return null;
12679
+ }
12680
+ function resolveCtxpackScript() {
12681
+ const here = dirname3(fileURLToPath(import.meta.url));
12682
+ const candidates = [
12683
+ join3(here, "vendor", "ctxpack.py"),
12684
+ // bundled: dist/cli.js -> dist/vendor/
12685
+ join3(here, "..", "vendor", "ctxpack.py"),
12686
+ // published package root -> vendor/
12687
+ join3(here, "..", "..", "vendor", "ctxpack.py")
12688
+ // dev: src/lib -> packages/cli/vendor/
12689
+ ];
12690
+ for (const candidate of candidates) {
12691
+ if (existsSync3(candidate)) return candidate;
12692
+ }
12693
+ throw new Error(
12694
+ `Could not locate the vendored ctxpack packer (looked in: ${candidates.join(", ")}). This is a packaging bug \u2014 reinstall @kinosec/cli.`
12695
+ );
12696
+ }
12697
+ var PYTHON_MISSING_MESSAGE = 'Packing --source requires Python 3, but none of "python3", "py -3", or "python" was found on PATH.\nInstall Python 3 (https://www.python.org/downloads/) and re-run, or pass an already-packed bundle: --source <bundle.txt>.';
12698
+ async function packSource(source, options) {
12699
+ if (!existsSync3(source)) {
12700
+ throw new Error(`--source path not found: ${source}`);
12701
+ }
12702
+ const stats = statSync(source);
12703
+ if (stats.isFile() && source.toLowerCase().endsWith(".txt")) {
12704
+ const bundleText = readFileSync3(source, "utf8");
12705
+ return {
12706
+ bundleText,
12707
+ bytes: Buffer.byteLength(bundleText, "utf8"),
12708
+ mode: null,
12709
+ stack: sniffBundleStack(bundleText),
12710
+ manifest: null,
12711
+ stderr: "",
12712
+ premade: true
12713
+ };
12714
+ }
12715
+ const python = findPython();
12716
+ if (!python) {
12717
+ throw new Error(PYTHON_MISSING_MESSAGE);
12718
+ }
12719
+ const script = resolveCtxpackScript();
12720
+ const outDir = mkdtempSync(join3(tmpdir(), "kinosec-ctxpack-"));
12721
+ try {
12722
+ const args = [...python.args, script, source, "--budget", String(options.budget), "-o", outDir];
12723
+ if (options.zipPassword) {
12724
+ args.push("--zip-password", options.zipPassword);
12725
+ }
12726
+ const proc = spawnSync(python.cmd, args, { encoding: "utf8" });
12727
+ if (proc.error) {
12728
+ throw new Error(`Failed to run ctxpack via ${python.cmd}: ${proc.error.message}`);
12729
+ }
12730
+ if (proc.status !== 0) {
12731
+ const detail = (proc.stderr || "").trim();
12732
+ throw new Error(
12733
+ `ctxpack failed to pack --source (exit ${proc.status ?? "unknown"}).` + (detail ? `
12734
+ ${detail}` : "") + (looksLikeAesZip(detail) ? '\nHint: only classic ZipCrypto zips are supported (HTB handouts use "hackthebox"), not WinZip/AES-256. Re-create the archive with classic encryption, or extract it and pass the directory.' : "")
12735
+ );
12736
+ }
12737
+ const bundlePath = join3(outDir, "bundle.txt");
12738
+ if (!existsSync3(bundlePath)) {
12739
+ throw new Error(`ctxpack produced no bundle (${bundlePath} missing).`);
12740
+ }
12741
+ const bundleText = readFileSync3(bundlePath, "utf8");
12742
+ let manifest = null;
12743
+ try {
12744
+ manifest = JSON.parse(readFileSync3(join3(outDir, "manifest.json"), "utf8"));
12745
+ } catch {
12746
+ manifest = null;
12747
+ }
12748
+ const stack = manifest?.signals?.stack ? String(manifest.signals.stack) : null;
12749
+ const mode = manifest?.mode ? String(manifest.mode) : null;
12750
+ return {
12751
+ bundleText,
12752
+ bytes: Buffer.byteLength(bundleText, "utf8"),
12753
+ mode,
12754
+ stack,
12755
+ manifest,
12756
+ stderr: proc.stderr || "",
12757
+ premade: false
12758
+ };
12759
+ } finally {
12760
+ try {
12761
+ rmSync(outDir, { recursive: true, force: true });
12762
+ } catch {
12763
+ }
12764
+ }
12765
+ }
12766
+ function looksLikeAesZip(stderr) {
12767
+ const lc = stderr.toLowerCase();
12768
+ return lc.includes("compression type 99") || lc.includes("aes") || lc.includes("winzip");
12769
+ }
12770
+ function sniffBundleStack(text) {
12771
+ const lc = text.toLowerCase();
12772
+ if (/"next"|next\.config|\/_next\//.test(lc)) return "nextjs";
12773
+ if (/"express"/.test(lc)) return "express";
12774
+ if (/from flask|import flask/.test(lc)) return "flask";
12775
+ if (/<\?php/.test(lc)) return "php";
12776
+ return null;
12777
+ }
12778
+ function matchLiveStack(lowercased) {
12779
+ if (lowercased.includes("x-powered-by: next.js") || lowercased.includes("/_next/") || lowercased.includes("__next_data__")) {
12780
+ return "nextjs";
12781
+ }
12782
+ if (lowercased.includes("x-powered-by: express")) return "express";
12783
+ if (lowercased.includes("x-powered-by: php") || lowercased.includes("phpsessid")) return "php";
12784
+ if (lowercased.includes("server: werkzeug")) return "flask";
12785
+ if (lowercased.includes("csrftoken")) return "django";
12786
+ return null;
12787
+ }
12788
+ var COMPATIBLE_STACKS = /* @__PURE__ */ new Set([
12789
+ "nextjs/react",
12790
+ "react/nextjs",
12791
+ "nextjs/node",
12792
+ "node/nextjs",
12793
+ "express/node",
12794
+ "node/express"
12795
+ ]);
12796
+ function stacksConflict(bundleStack, liveStack) {
12797
+ if (!bundleStack || !liveStack) return false;
12798
+ if (bundleStack === liveStack) return false;
12799
+ if (COMPATIBLE_STACKS.has(`${bundleStack}/${liveStack}`)) return false;
12800
+ return true;
12801
+ }
12802
+ async function probeLiveStack(host, timeoutMs = 8e3) {
12803
+ for (const scheme of ["https", "http"]) {
12804
+ let res;
12805
+ const controller = new AbortController();
12806
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
12807
+ try {
12808
+ res = await fetch(`${scheme}://${host}/`, { signal: controller.signal, redirect: "follow" });
12809
+ } catch {
12810
+ continue;
12811
+ } finally {
12812
+ clearTimeout(timer);
12813
+ }
12814
+ const headerBlob = [...res.headers].map(([key, value]) => `${key}: ${value}`).join("\n");
12815
+ let body = "";
12816
+ try {
12817
+ body = (await res.text()).slice(0, 4096);
12818
+ } catch {
12819
+ }
12820
+ return matchLiveStack(`${headerBlob}
12821
+ ${body}`.toLowerCase());
12822
+ }
12823
+ return null;
12824
+ }
12825
+
12564
12826
  // src/commands/pentest.ts
12827
+ var DEFAULT_SOURCE_BUDGET = 4e4;
12828
+ function reportPackResult(result, budget) {
12829
+ const size = `${result.bytes.toLocaleString()} bytes`;
12830
+ if (result.premade) {
12831
+ console.error(`Graybox: using pre-made source bundle (${size}).`);
12832
+ } else {
12833
+ for (const line of result.stderr.split("\n")) {
12834
+ const trimmed = line.trim();
12835
+ if (trimmed.startsWith("ctxpack: mode=")) {
12836
+ console.error(`Graybox: ${trimmed.slice("ctxpack: ".length)} [${size}]`);
12837
+ }
12838
+ }
12839
+ }
12840
+ if (result.bytes > SOURCE_BYTES_WARN) {
12841
+ printWarning(
12842
+ `Source bundle is ${size} \u2014 that's a lot of prompt context. It will still be delivered (the server ships oversize source out-of-band), but large bundles raise per-scan cost and can dilute the agent's focus. Lower --budget (currently ${budget}) or trim --source to tighten it.`
12843
+ );
12844
+ }
12845
+ }
12846
+ function ensurePasteBundle(result) {
12847
+ if (result.premade || result.mode === "paste") return;
12848
+ if (result.mode === "retrieve") {
12849
+ printWarning(
12850
+ "ctxpack fell back to retrieve mode: the source is too large to paste within --budget, so the bundle is a path outline with no code \u2014 useless as graybox context."
12851
+ );
12852
+ console.error(
12853
+ "Not sending it. Raise --budget to re-pack the source as a full paste bundle, or trim --source to the exploit-relevant files (or run a normal black-box scan without --source)."
12854
+ );
12855
+ process.exit(1);
12856
+ }
12857
+ }
12858
+ function isUserDataTooLarge(err) {
12859
+ return err.status === 413 || /user-?data too large/i.test(err.detail);
12860
+ }
12565
12861
  function registerPentestCommand(program3) {
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", `
12862
+ 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, defcon, or exploit-only", "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").option("--source <path>", "Graybox source handout: a directory, an archive (.zip/.tar/.tgz), or a pre-made bundle .txt. Its presence enables graybox").option("--budget <tokens>", "Token budget for packing --source (how much source to pack; larger = more context and cost)", String(DEFAULT_SOURCE_BUDGET)).option("--zip-password <pw>", "Password for an encrypted --source .zip (classic ZipCrypto only; HTB handouts use: hackthebox)").option("--force", "Proceed even when the --source stack looks unlike the live target").addHelpText("after", `
12567
12863
  Examples:
12568
12864
  $ kinosec pentest -t delikado.org
12569
12865
  $ kinosec pentest --profile abc123
12570
12866
  $ kinosec pentest -t internal.corp --deployment on-prem
12571
12867
  $ kinosec pentest -t example.com --include api.example.com,app.example.com
12572
12868
  $ kinosec pentest -t example.com --exclude staging.example.com
12573
- $ kinosec pentest -t example.com --scope clear`).action(async (options) => {
12869
+ $ kinosec pentest -t example.com --scope clear
12870
+ $ kinosec pentest -t juice.example.com --mode exploit-only
12871
+ $ kinosec pentest -t target.htb --mode htb --source ./challenge.zip --zip-password hackthebox
12872
+
12873
+ Graybox (--source):
12874
+ Packs the target's source and sends it with the pentest as extra context.
12875
+ Works with --mode full and quick on your own assets. --budget controls how much
12876
+ source is packed (bigger bundles add context and cost).
12877
+ The source is sent over HTTPS, used only while the pentest runs, and is NOT stored.`).action(async (options) => {
12574
12878
  if (!options.target && !options.profile) {
12575
12879
  console.error("Provide --target <domain> or --profile <id>.");
12576
12880
  process.exit(2);
@@ -12583,6 +12887,16 @@ Examples:
12583
12887
  process.exit(2);
12584
12888
  }
12585
12889
  validateScanMode(options.mode);
12890
+ let sourceBudget = DEFAULT_SOURCE_BUDGET;
12891
+ if (options.source) {
12892
+ sourceBudget = parseInt(options.budget, 10);
12893
+ if (!Number.isInteger(sourceBudget) || sourceBudget <= 0) {
12894
+ console.error("Invalid --budget. Must be a positive integer (tokens).");
12895
+ process.exit(2);
12896
+ }
12897
+ } else if (options.zipPassword || options.force) {
12898
+ console.error("Warning: --zip-password/--force apply only with --source; ignoring.");
12899
+ }
12586
12900
  const scopeClear = options.scope === "clear";
12587
12901
  if (options.scope && options.scope !== "clear") {
12588
12902
  console.error("Invalid --scope value. Must be: clear");
@@ -12601,6 +12915,15 @@ Examples:
12601
12915
  const opts = program3.opts();
12602
12916
  const { token, apiUrl } = resolveToken(opts);
12603
12917
  const client = new KinoSecClient({ apiUrl, token });
12918
+ let packResult = null;
12919
+ if (options.source) {
12920
+ packResult = await packSource(options.source, {
12921
+ budget: sourceBudget,
12922
+ zipPassword: options.zipPassword
12923
+ });
12924
+ reportPackResult(packResult, sourceBudget);
12925
+ ensurePasteBundle(packResult);
12926
+ }
12604
12927
  let targetDisplay;
12605
12928
  let params;
12606
12929
  if (options.profile) {
@@ -12684,8 +13007,47 @@ Examples:
12684
13007
  }
12685
13008
  displayScope(asset);
12686
13009
  }
13010
+ if (packResult?.stack && !options.force) {
13011
+ const liveStack = await probeLiveStack(targetDisplay);
13012
+ if (liveStack && stacksConflict(packResult.stack, liveStack)) {
13013
+ printWarning(
13014
+ `Source handout looks like "${packResult.stack}", but ${targetDisplay} responds like "${liveStack}".`
13015
+ );
13016
+ printWarning("The source may belong to a different app; graybox context could mislead the pentest.");
13017
+ if (options.yes) {
13018
+ console.error("Proceeding anyway (--yes).");
13019
+ } else {
13020
+ console.error("Aborting. Re-run with --force to override, or fix --source.");
13021
+ process.exit(2);
13022
+ }
13023
+ }
13024
+ }
12687
13025
  await confirmAuthorization(targetDisplay, Boolean(options.yes));
12688
- const result = await client.scans.create(params);
13026
+ if (packResult) {
13027
+ params.source_context = packResult.bundleText;
13028
+ }
13029
+ let result;
13030
+ try {
13031
+ result = await client.scans.create(params);
13032
+ } catch (err) {
13033
+ if (err instanceof ApiError) {
13034
+ if (packResult && isUserDataTooLarge(err)) {
13035
+ printWarning(`Server rejected the scan: ${err.detail}`);
13036
+ console.error(
13037
+ `The graybox source made the provisioning payload too large. Re-pack with a smaller --budget (currently ${sourceBudget}) or trim --source, then retry.`
13038
+ );
13039
+ process.exit(1);
13040
+ }
13041
+ if (err.status === 403 && (options.mode === "htb" || options.mode === "defcon")) {
13042
+ printWarning(`The "${options.mode}" scan mode requires admin access (server returned 403).`);
13043
+ console.error(
13044
+ "Run with --mode full or --mode quick (which support --source for your own assets), or ask your KinoSec admin to enable CTF modes for your account."
13045
+ );
13046
+ process.exit(77);
13047
+ }
13048
+ }
13049
+ throw err;
13050
+ }
12689
13051
  const isOnPrem = result.deployment_type === "on_prem";
12690
13052
  if (opts.json || opts.output === "json") {
12691
13053
  printJson(result);
@@ -13185,10 +13547,9 @@ Examples:
13185
13547
  // src/commands/asset.ts
13186
13548
  function registerAssetCommand(program3) {
13187
13549
  const asset = program3.command("asset").description("Manage assets");
13188
- asset.command("list").description("List assets").option("--limit <n>", "Max results per page", "20").option("--cursor <cursor>", "Pagination cursor").option("--search <term>", "Filter by asset value (substring match)").option("--status <status>", "Filter by status (verified, unverified)").option("--type <type>", "Filter by type (domain, subdomain, ip)").addHelpText("after", `
13550
+ asset.command("list").description("List assets").option("--limit <n>", "Max results per page", "20").option("--cursor <cursor>", "Pagination cursor").option("--status <status>", "Filter by status (verified, unverified)").option("--type <type>", "Filter by type (domain, subdomain, ip)").addHelpText("after", `
13189
13551
  Examples:
13190
13552
  $ kinosec asset list
13191
- $ kinosec asset list --search example.com
13192
13553
  $ kinosec asset list --type subdomain --status verified
13193
13554
  $ kinosec asset list --limit 50 --json`).action(async (options) => {
13194
13555
  try {
@@ -13198,7 +13559,6 @@ Examples:
13198
13559
  const result = await client.assets.list({
13199
13560
  limit: parseInt(options.limit, 10),
13200
13561
  cursor: options.cursor,
13201
- search: options.search,
13202
13562
  status: options.status,
13203
13563
  type: options.type
13204
13564
  });
@@ -13433,7 +13793,7 @@ function registerFindingCommand(program3) {
13433
13793
  const finding = program3.command("finding").description("Manage findings");
13434
13794
  finding.command("list").description("List findings").argument("[profile-id]", "Pentest profile ID").option("--limit <n>", "Max results per page", "20").option("--cursor <cursor>", "Pagination cursor").addHelpText("after", `
13435
13795
  Examples:
13436
- $ kinosec finding list abc123
13796
+ $ kinosec finding list a1b2c3d4-0000-0000-0000-000000000001
13437
13797
  $ kinosec finding list --limit 50 --json`).action(async (profileId, options) => {
13438
13798
  try {
13439
13799
  const opts = program3.opts();
@@ -13442,7 +13802,7 @@ Examples:
13442
13802
  const result = await client.findings.list({
13443
13803
  limit: parseInt(options.limit, 10),
13444
13804
  cursor: options.cursor,
13445
- scan: profileId
13805
+ scan_profile_id: profileId
13446
13806
  });
13447
13807
  if (opts.json || opts.output === "json") {
13448
13808
  printJson(result);
@@ -13699,6 +14059,7 @@ Examples:
13699
14059
 
13700
14060
  // src/commands/token.ts
13701
14061
  var import_picocolors13 = __toESM(require_picocolors(), 1);
14062
+ var WEB_APP_TOKENS_URL = "https://kinosec.ai/settings?tab=api-tokens";
13702
14063
  function registerTokenCommand(program3) {
13703
14064
  const token = program3.command("token").description("Manage API tokens");
13704
14065
  token.command("list").description("List API tokens").option("--limit <n>", "Max results per page", "20").option("--cursor <cursor>", "Pagination cursor").addHelpText("after", `
@@ -13744,8 +14105,8 @@ More results available. Use --cursor ${result.next_cursor}`));
13744
14105
  Examples:
13745
14106
  $ kinosec token create --name "CI" --scopes assets:read,scans:create
13746
14107
  $ kinosec token create --name "Full" --scopes assets:read,scans:read --expires 30`).action(async (options) => {
14108
+ const opts = program3.opts();
13747
14109
  try {
13748
- const opts = program3.opts();
13749
14110
  const { token: authToken, apiUrl } = resolveToken(opts);
13750
14111
  const client = new KinoSecClient({ apiUrl, token: authToken });
13751
14112
  const scopes = options.scopes.split(",").map((s) => s.trim());
@@ -13767,6 +14128,13 @@ Examples:
13767
14128
  console.log(` Scopes: ${result.scopes.join(", ")}`);
13768
14129
  console.log(` Expires: ${new Date(result.expires_at).toLocaleDateString()}`);
13769
14130
  } catch (error2) {
14131
+ const isJson = opts.json || opts.output === "json";
14132
+ if (error2 instanceof ApiError && error2.code === "pat_cannot_create_token" && !isJson) {
14133
+ printWarning("API tokens can't create other API tokens.");
14134
+ console.error("Create a new token in the KinoSec web app:");
14135
+ console.error(` ${WEB_APP_TOKENS_URL}`);
14136
+ console.error("");
14137
+ }
13770
14138
  handleError(error2);
13771
14139
  }
13772
14140
  });
@@ -13793,7 +14161,7 @@ Examples:
13793
14161
 
13794
14162
  // src/cli.ts
13795
14163
  var program2 = new Command();
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");
14164
+ program2.name("kinosec").description("KinoSec CLI \u2014 run pentests from your terminal").version("0.11.2", "-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");
13797
14165
  registerAuthCommand(program2);
13798
14166
  registerMeCommand(program2);
13799
14167
  registerCreditsCommand(program2);
@@ -13802,10 +14170,9 @@ registerAssetCommand(program2);
13802
14170
  registerFindingCommand(program2);
13803
14171
  registerReportCommand(program2);
13804
14172
  registerTokenCommand(program2);
13805
- program2.parseAsync(process.argv).then(() => checkForUpdate("0.10.3")).catch(handleError);
14173
+ program2.parseAsync(process.argv).then(() => checkForUpdate("0.11.2")).catch(handleError);
13806
14174
  /*! Bundled license information:
13807
14175
 
13808
14176
  ky/distribution/index.js:
13809
14177
  (*! MIT License © Sindre Sorhus *)
13810
14178
  */
13811
- //# sourceMappingURL=cli.js.map