@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 +441 -74
- package/dist/vendor/README.md +134 -0
- package/dist/vendor/ctxpack.py +701 -0
- package/package.json +2 -1
- package/vendor/README.md +134 -0
- package/vendor/ctxpack.py +701 -0
- package/dist/cli.js.map +0 -1
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
|
|
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-
|
|
8438
|
-
var
|
|
8439
|
-
|
|
8440
|
-
|
|
8441
|
-
|
|
8442
|
-
|
|
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
|
|
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
|
|
8550
|
-
|
|
8551
|
-
|
|
8552
|
-
|
|
8553
|
-
|
|
8554
|
-
|
|
8555
|
-
|
|
8556
|
-
|
|
8557
|
-
|
|
8558
|
-
|
|
8559
|
-
|
|
8560
|
-
|
|
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.
|
|
8569
|
-
name: options.name ?? `${options.pattern} ${
|
|
8570
|
-
scan_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
|
|
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 (
|
|
8600
|
-
|
|
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
|
|
12107
|
-
|
|
12108
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
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("--
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|