@kinosec/cli 0.11.1 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +711 -148
- package/package.json +1 -1
- package/dist/cli.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -6944,6 +6944,10 @@ var ReportsResource = class {
|
|
|
6944
6944
|
async export(params) {
|
|
6945
6945
|
return this.api.post("v1/reports", { json: params }).json();
|
|
6946
6946
|
}
|
|
6947
|
+
/** The API's own markdown rendering of a report (`text/markdown`), returned as is. */
|
|
6948
|
+
async exportMarkdown(scanReportId) {
|
|
6949
|
+
return this.api.post("v1/reports", { json: { scan_report_id: scanReportId, format: "markdown" } }).text();
|
|
6950
|
+
}
|
|
6947
6951
|
};
|
|
6948
6952
|
var TokensResource = class {
|
|
6949
6953
|
constructor(api) {
|
|
@@ -8435,28 +8439,111 @@ ${import_picocolors3.default.dim("use of its services.")}
|
|
|
8435
8439
|
// src/commands/schedule.ts
|
|
8436
8440
|
var import_picocolors4 = __toESM(require_picocolors(), 1);
|
|
8437
8441
|
|
|
8438
|
-
// src/lib/validate-
|
|
8439
|
-
var
|
|
8440
|
-
|
|
8441
|
-
|
|
8442
|
-
|
|
8443
|
-
|
|
8442
|
+
// src/lib/validate-schedule.ts
|
|
8443
|
+
var SCHEDULE_PATTERNS = ["once", "daily", "weekly", "biweekly", "monthly"];
|
|
8444
|
+
var SCHEDULE_MODES = ["quick", "full"];
|
|
8445
|
+
var TIME_OF_DAY = /^([01]?\d|2[0-3]):[0-5]\d$/;
|
|
8446
|
+
var DIGITS = /^\d+$/;
|
|
8447
|
+
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)$/;
|
|
8448
|
+
function scheduleInputError(flags, now) {
|
|
8449
|
+
const { pattern, mode, at, time, timezone, dayOfWeek, dayOfMonth } = flags;
|
|
8450
|
+
if (pattern !== void 0 && !SCHEDULE_PATTERNS.includes(pattern)) {
|
|
8451
|
+
return `Invalid --pattern "${pattern}". Must be one of: ${SCHEDULE_PATTERNS.join(", ")}.`;
|
|
8452
|
+
}
|
|
8453
|
+
if (mode !== void 0 && !SCHEDULE_MODES.includes(mode)) {
|
|
8454
|
+
return `Invalid --mode "${mode}". Schedules support: ${SCHEDULE_MODES.join(", ")}.`;
|
|
8455
|
+
}
|
|
8456
|
+
if (at !== void 0) {
|
|
8457
|
+
const atError = onceAtError(at, now);
|
|
8458
|
+
if (atError) return atError;
|
|
8459
|
+
}
|
|
8460
|
+
if (time !== void 0 && !TIME_OF_DAY.test(time)) {
|
|
8461
|
+
return `Invalid --time "${time}". Use 24-hour HH:MM, e.g. 09:00.`;
|
|
8462
|
+
}
|
|
8463
|
+
if (timezone !== void 0 && !isKnownTimezone(timezone)) {
|
|
8464
|
+
return `Invalid --timezone "${timezone}". Use an IANA name, e.g. America/New_York.`;
|
|
8465
|
+
}
|
|
8466
|
+
if (dayOfWeek !== void 0 && !isWholeNumberIn(dayOfWeek, 0, 6)) {
|
|
8467
|
+
return `Invalid --day-of-week "${dayOfWeek}". Use a whole number 0-6 (0=Sunday).`;
|
|
8444
8468
|
}
|
|
8469
|
+
if (dayOfMonth !== void 0 && !isWholeNumberIn(dayOfMonth, 1, 31)) {
|
|
8470
|
+
return `Invalid --day-of-month "${dayOfMonth}". Use a whole number 1-31.`;
|
|
8471
|
+
}
|
|
8472
|
+
if (pattern === void 0 && at !== void 0) return "--at requires --pattern once.";
|
|
8473
|
+
if (pattern === void 0) return null;
|
|
8474
|
+
return patternFitError(pattern, flags);
|
|
8475
|
+
}
|
|
8476
|
+
function patternFitError(pattern, flags) {
|
|
8477
|
+
if (pattern === "once" && flags.at === void 0) {
|
|
8478
|
+
return "--at is required for once schedules: YYYY-MM-DDTHH:MM followed by Z or \xB1HH:MM.";
|
|
8479
|
+
}
|
|
8480
|
+
if (pattern === "once" && flags.time !== void 0) {
|
|
8481
|
+
return "--time does not apply to once schedules; put the time in --at.";
|
|
8482
|
+
}
|
|
8483
|
+
if (pattern !== "once" && flags.at !== void 0) {
|
|
8484
|
+
return "--at only applies to once schedules.";
|
|
8485
|
+
}
|
|
8486
|
+
const isWeekly = pattern === "weekly" || pattern === "biweekly";
|
|
8487
|
+
if (isWeekly && flags.dayOfWeek === void 0) {
|
|
8488
|
+
return `--day-of-week is required for ${pattern} schedules.`;
|
|
8489
|
+
}
|
|
8490
|
+
if (!isWeekly && flags.dayOfWeek !== void 0) {
|
|
8491
|
+
return "--day-of-week only applies to weekly and biweekly schedules.";
|
|
8492
|
+
}
|
|
8493
|
+
if (pattern === "monthly" && flags.dayOfMonth === void 0) {
|
|
8494
|
+
return "--day-of-month is required for monthly schedules.";
|
|
8495
|
+
}
|
|
8496
|
+
if (pattern !== "monthly" && flags.dayOfMonth !== void 0) {
|
|
8497
|
+
return "--day-of-month only applies to monthly schedules.";
|
|
8498
|
+
}
|
|
8499
|
+
return null;
|
|
8500
|
+
}
|
|
8501
|
+
function onceAtError(at, now) {
|
|
8502
|
+
const match = ONCE_AT.exec(at);
|
|
8503
|
+
const ms = Date.parse(at);
|
|
8504
|
+
if (!match || Number.isNaN(ms) || !isCalendarDate(Number(match[1]), Number(match[2]), Number(match[3]))) {
|
|
8505
|
+
return `Invalid --at "${at}". Use YYYY-MM-DDTHH:MM followed by Z or \xB1HH:MM, on a real date.`;
|
|
8506
|
+
}
|
|
8507
|
+
if (ms <= now.getTime()) return `--at "${at}" is in the past. Pick a future time.`;
|
|
8508
|
+
return null;
|
|
8509
|
+
}
|
|
8510
|
+
function isCalendarDate(year, month, day) {
|
|
8511
|
+
const date = new Date(Date.UTC(year, month - 1, day));
|
|
8512
|
+
return date.getUTCMonth() === month - 1 && date.getUTCDate() === day;
|
|
8513
|
+
}
|
|
8514
|
+
function isKnownTimezone(timezone) {
|
|
8515
|
+
try {
|
|
8516
|
+
new Intl.DateTimeFormat("en-US", { timeZone: timezone });
|
|
8517
|
+
return true;
|
|
8518
|
+
} catch (error2) {
|
|
8519
|
+
if (error2 instanceof RangeError) return false;
|
|
8520
|
+
throw error2;
|
|
8521
|
+
}
|
|
8522
|
+
}
|
|
8523
|
+
function isWholeNumberIn(value, min, max) {
|
|
8524
|
+
if (!DIGITS.test(value)) return false;
|
|
8525
|
+
const n = Number(value);
|
|
8526
|
+
return n >= min && n <= max;
|
|
8445
8527
|
}
|
|
8446
8528
|
|
|
8447
8529
|
// src/commands/schedule.ts
|
|
8448
|
-
var VALID_PATTERNS = ["once", "daily", "weekly", "biweekly", "monthly"];
|
|
8449
8530
|
var DAY_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
|
8450
8531
|
function extractTime(scheduledAt) {
|
|
8451
8532
|
if (!scheduledAt) return "\u2014";
|
|
8452
8533
|
const d = new Date(scheduledAt);
|
|
8453
8534
|
return d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", hour12: false });
|
|
8454
8535
|
}
|
|
8536
|
+
function extractDate(scheduledAt) {
|
|
8537
|
+
if (!scheduledAt) return "\u2014";
|
|
8538
|
+
const d = new Date(scheduledAt);
|
|
8539
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
8540
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
8541
|
+
}
|
|
8455
8542
|
function formatPattern(s) {
|
|
8456
8543
|
const time = extractTime(s.scheduled_at);
|
|
8457
8544
|
switch (s.recurrence_pattern) {
|
|
8458
8545
|
case "once":
|
|
8459
|
-
return `Once at ${time}`;
|
|
8546
|
+
return `Once on ${extractDate(s.scheduled_at)} at ${time}`;
|
|
8460
8547
|
case "daily":
|
|
8461
8548
|
return `Daily at ${time}`;
|
|
8462
8549
|
case "weekly":
|
|
@@ -8469,6 +8556,22 @@ function formatPattern(s) {
|
|
|
8469
8556
|
return s.recurrence_pattern;
|
|
8470
8557
|
}
|
|
8471
8558
|
}
|
|
8559
|
+
function modeTypedOnPentest(pentestCmd) {
|
|
8560
|
+
return pentestCmd.getOptionValueSource("mode") === "cli" ? String(pentestCmd.getOptionValue("mode")) : void 0;
|
|
8561
|
+
}
|
|
8562
|
+
function yesTypedOnPentest(pentestCmd) {
|
|
8563
|
+
return pentestCmd.getOptionValueSource("yes") === "cli";
|
|
8564
|
+
}
|
|
8565
|
+
function exitOnInputError(flags) {
|
|
8566
|
+
const error2 = scheduleInputError(flags, /* @__PURE__ */ new Date());
|
|
8567
|
+
if (!error2) return;
|
|
8568
|
+
console.error(error2);
|
|
8569
|
+
process.exit(2);
|
|
8570
|
+
}
|
|
8571
|
+
function scheduleMode(mode) {
|
|
8572
|
+
if (mode === "quick" || mode === "full") return mode;
|
|
8573
|
+
throw new Error(`Unchecked schedule mode "${mode}".`);
|
|
8574
|
+
}
|
|
8472
8575
|
function registerScheduleCommand(pentestCmd) {
|
|
8473
8576
|
const schedule = pentestCmd.command("schedule").description("Manage scheduled pentests");
|
|
8474
8577
|
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", `
|
|
@@ -8543,36 +8646,35 @@ Examples:
|
|
|
8543
8646
|
handleError(error2);
|
|
8544
8647
|
}
|
|
8545
8648
|
});
|
|
8546
|
-
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
|
|
8649
|
+
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", `
|
|
8547
8650
|
Examples:
|
|
8548
8651
|
$ kinosec pentest schedule create --asset abc123 --pattern weekly --day-of-week 1 --time 09:00
|
|
8549
8652
|
$ kinosec pentest schedule create --asset abc123 --pattern daily --time 14:00
|
|
8550
|
-
$ kinosec pentest schedule create --asset abc123 --pattern monthly --day-of-month 15 --time 03:00 --timezone UTC
|
|
8551
|
-
|
|
8552
|
-
|
|
8553
|
-
|
|
8554
|
-
|
|
8555
|
-
|
|
8556
|
-
|
|
8557
|
-
|
|
8558
|
-
|
|
8559
|
-
|
|
8560
|
-
|
|
8561
|
-
|
|
8562
|
-
process.exit(2);
|
|
8563
|
-
}
|
|
8653
|
+
$ kinosec pentest schedule create --asset abc123 --pattern monthly --day-of-month 15 --time 03:00 --timezone UTC
|
|
8654
|
+
$ kinosec pentest schedule create --asset abc123 --pattern once --at 2099-12-01T09:00+01:00`).action(async (options) => {
|
|
8655
|
+
const mode = modeTypedOnPentest(pentestCmd) ?? options.mode;
|
|
8656
|
+
exitOnInputError({
|
|
8657
|
+
pattern: options.pattern,
|
|
8658
|
+
mode,
|
|
8659
|
+
at: options.at,
|
|
8660
|
+
time: options.time,
|
|
8661
|
+
timezone: options.timezone,
|
|
8662
|
+
dayOfWeek: options.dayOfWeek,
|
|
8663
|
+
dayOfMonth: options.dayOfMonth
|
|
8664
|
+
});
|
|
8564
8665
|
try {
|
|
8565
8666
|
const opts = schedule.parent.parent.opts();
|
|
8566
8667
|
const { token, apiUrl } = resolveToken(opts);
|
|
8567
8668
|
const client = new KinoSecClient({ apiUrl, token });
|
|
8568
8669
|
const params = {
|
|
8569
|
-
asset_id: options.
|
|
8570
|
-
name: options.name ?? `${options.pattern} ${
|
|
8571
|
-
scan_mode:
|
|
8670
|
+
asset_id: options.asset,
|
|
8671
|
+
name: options.name ?? `${options.pattern} ${mode} pentest`,
|
|
8672
|
+
scan_mode: scheduleMode(mode),
|
|
8572
8673
|
recurrence_pattern: options.pattern,
|
|
8573
|
-
time_of_day: options.time,
|
|
8574
8674
|
timezone: options.timezone
|
|
8575
8675
|
};
|
|
8676
|
+
if (options.pattern === "once") params.once_at = new Date(options.at).toISOString();
|
|
8677
|
+
else params.time_of_day = options.time ?? "09:00";
|
|
8576
8678
|
if (options.dayOfWeek !== void 0) params.day_of_week = parseInt(options.dayOfWeek, 10);
|
|
8577
8679
|
if (options.dayOfMonth !== void 0) params.day_of_month = parseInt(options.dayOfMonth, 10);
|
|
8578
8680
|
const s = await client.schedules.create(params);
|
|
@@ -8590,17 +8692,26 @@ Manage: kinosec pentest schedule get ${s.id}`);
|
|
|
8590
8692
|
handleError(error2);
|
|
8591
8693
|
}
|
|
8592
8694
|
});
|
|
8593
|
-
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", `
|
|
8695
|
+
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", `
|
|
8594
8696
|
Examples:
|
|
8595
8697
|
$ kinosec pentest schedule update abc123 --pattern daily --time 14:00
|
|
8596
|
-
$ kinosec pentest schedule update abc123 --time 14:00
|
|
8698
|
+
$ kinosec pentest schedule update abc123 --time 14:00
|
|
8699
|
+
$ kinosec pentest schedule update abc123 --pattern once --at 2099-12-01T09:00Z`).action(async (id, options) => {
|
|
8700
|
+
const mode = modeTypedOnPentest(pentestCmd) ?? options.mode;
|
|
8701
|
+
exitOnInputError({
|
|
8702
|
+
pattern: options.pattern,
|
|
8703
|
+
mode,
|
|
8704
|
+
at: options.at,
|
|
8705
|
+
time: options.time,
|
|
8706
|
+
timezone: options.timezone,
|
|
8707
|
+
dayOfWeek: options.dayOfWeek,
|
|
8708
|
+
dayOfMonth: options.dayOfMonth
|
|
8709
|
+
});
|
|
8597
8710
|
const params = {};
|
|
8598
8711
|
if (options.pattern) params.recurrence_pattern = options.pattern;
|
|
8599
8712
|
if (options.name) params.name = options.name;
|
|
8600
|
-
if (
|
|
8601
|
-
|
|
8602
|
-
params.scan_mode = options.mode;
|
|
8603
|
-
}
|
|
8713
|
+
if (mode) params.scan_mode = scheduleMode(mode);
|
|
8714
|
+
if (options.at !== void 0) params.once_at = new Date(options.at).toISOString();
|
|
8604
8715
|
if (options.time) params.time_of_day = options.time;
|
|
8605
8716
|
if (options.dayOfWeek !== void 0) params.day_of_week = parseInt(options.dayOfWeek, 10);
|
|
8606
8717
|
if (options.dayOfMonth !== void 0) params.day_of_month = parseInt(options.dayOfMonth, 10);
|
|
@@ -8630,7 +8741,7 @@ Examples:
|
|
|
8630
8741
|
$ kinosec pentest schedule delete abc123
|
|
8631
8742
|
$ kinosec pentest schedule delete abc123 --yes`).action(async (id, options) => {
|
|
8632
8743
|
try {
|
|
8633
|
-
await confirmDestructive(`Delete schedule ${id}?`, Boolean(options.yes));
|
|
8744
|
+
await confirmDestructive(`Delete schedule ${id}?`, Boolean(options.yes) || yesTypedOnPentest(pentestCmd));
|
|
8634
8745
|
const opts = schedule.parent.parent.opts();
|
|
8635
8746
|
const { token, apiUrl } = resolveToken(opts);
|
|
8636
8747
|
const client = new KinoSecClient({ apiUrl, token });
|
|
@@ -11904,17 +12015,32 @@ function normalizeActivityHistory(raw) {
|
|
|
11904
12015
|
return JSON.stringify(entry);
|
|
11905
12016
|
});
|
|
11906
12017
|
}
|
|
11907
|
-
|
|
11908
|
-
|
|
11909
|
-
|
|
11910
|
-
|
|
11911
|
-
|
|
11912
|
-
|
|
11913
|
-
|
|
11914
|
-
|
|
11915
|
-
|
|
11916
|
-
|
|
11917
|
-
|
|
12018
|
+
|
|
12019
|
+
// src/lib/failure-reason.ts
|
|
12020
|
+
var REASON_SENTENCES = {
|
|
12021
|
+
timeout: "The pentest stopped reporting progress for longer than we allow, so we shut it down to be safe. We could not see why it went quiet.",
|
|
12022
|
+
no_results: "The pentest ran but produced no results, so there was nothing to report.",
|
|
12023
|
+
worker_image_broken: "The pentest machine started with a broken image on our side. This is our fault, not yours.",
|
|
12024
|
+
worker_startup_failed: "The pentest worker could not start up, so testing never began.",
|
|
12025
|
+
scan_error: "The pentest engine hit an error while testing the target and could not continue.",
|
|
12026
|
+
ad_credentials_invalid: "The Active Directory credentials you supplied were rejected by the domain controller. Update them and try again.",
|
|
12027
|
+
no_dc_reachable: "No domain controller could be reached inside the scope you gave us. Check that the scope covers the domain controller's subnet and that the scout can route to it, then try again.",
|
|
12028
|
+
target_not_authorized: "We could not confirm you are authorized to test this target. Confirm authorization in your dashboard and try again.",
|
|
12029
|
+
mfa_required: "Starting this pentest requires two-factor authentication. Sign in with your second factor and try again.",
|
|
12030
|
+
egress_not_ready: "The network route we use to reach your target was not ready. Please try again in a few minutes.",
|
|
12031
|
+
launch_conflict: "Another launch of this same pentest was already in progress. Refresh your dashboard and try again.",
|
|
12032
|
+
launch_error: "The pentest could not be started. This is usually a temporary problem on our side.",
|
|
12033
|
+
scheduled_launch_error: "A scheduled pentest could not be started at its scheduled time.",
|
|
12034
|
+
schedule_cancelled: "The pentest was stopped because its schedule was cancelled while it was running."
|
|
12035
|
+
};
|
|
12036
|
+
var UNKNOWN_SENTENCE = "We could not determine the exact cause.";
|
|
12037
|
+
function isKnownReason(code) {
|
|
12038
|
+
return Object.hasOwn(REASON_SENTENCES, code);
|
|
12039
|
+
}
|
|
12040
|
+
function failureSentence(rawReason) {
|
|
12041
|
+
if (typeof rawReason !== "string") return UNKNOWN_SENTENCE;
|
|
12042
|
+
const code = rawReason.split(":")[0].trim().toLowerCase();
|
|
12043
|
+
return isKnownReason(code) ? REASON_SENTENCES[code] : UNKNOWN_SENTENCE;
|
|
11918
12044
|
}
|
|
11919
12045
|
|
|
11920
12046
|
// src/lib/buzzwords.ts
|
|
@@ -12019,15 +12145,6 @@ function printActivityLines(entries, tty3) {
|
|
|
12019
12145
|
console.log(`${prompt} ${text}`);
|
|
12020
12146
|
}
|
|
12021
12147
|
}
|
|
12022
|
-
async function resolveShowFullDetails(client) {
|
|
12023
|
-
try {
|
|
12024
|
-
const me = await client.me.get();
|
|
12025
|
-
const slug = me.plan.slug;
|
|
12026
|
-
return slug === "security_pro" || slug === "enterprise";
|
|
12027
|
-
} catch {
|
|
12028
|
-
return false;
|
|
12029
|
-
}
|
|
12030
|
-
}
|
|
12031
12148
|
async function watchPentest(client, profileId, options) {
|
|
12032
12149
|
const { timeoutMs, intervalMs, useJson, showFindingsSummary } = options;
|
|
12033
12150
|
const tty3 = isTTY();
|
|
@@ -12056,7 +12173,6 @@ async function watchPentest(client, profileId, options) {
|
|
|
12056
12173
|
console.log("\nWatch cancelled. Pentest is still running.");
|
|
12057
12174
|
process.exit(0);
|
|
12058
12175
|
});
|
|
12059
|
-
const showFullDetails = await resolveShowFullDetails(client);
|
|
12060
12176
|
let consecutiveErrors = 0;
|
|
12061
12177
|
const MAX_CONSECUTIVE_ERRORS = 2;
|
|
12062
12178
|
let deferredExitCode = null;
|
|
@@ -12104,17 +12220,16 @@ ${icon2} Lost connection to pentest.`);
|
|
|
12104
12220
|
JSON.stringify({ ...scanDetail, elapsed_seconds: elapsed }) + "\n"
|
|
12105
12221
|
);
|
|
12106
12222
|
} else {
|
|
12107
|
-
const
|
|
12108
|
-
|
|
12109
|
-
|
|
12110
|
-
const newEntries = filtered.slice(printedCount);
|
|
12223
|
+
const entries = normalizeActivityHistory(scanDetail.activity_history);
|
|
12224
|
+
if (printedCount > entries.length) printedCount = 0;
|
|
12225
|
+
const newEntries = entries.slice(printedCount);
|
|
12111
12226
|
if (newEntries.length > 0) {
|
|
12112
12227
|
if (spinner) spinner.stop();
|
|
12113
12228
|
printActivityLines(newEntries, tty3);
|
|
12114
12229
|
printedCount += newEntries.length;
|
|
12115
12230
|
if (spinner) spinner = ora({ color: "green", isEnabled: true, discardStdin: false }).start();
|
|
12116
12231
|
}
|
|
12117
|
-
currentPhase = extractPhase(
|
|
12232
|
+
currentPhase = extractPhase(entries);
|
|
12118
12233
|
updateSpinnerText();
|
|
12119
12234
|
}
|
|
12120
12235
|
const status = scanDetail.scan_status;
|
|
@@ -12151,7 +12266,7 @@ ${icon2} Pentest completed: ${domain} ${dur}`);
|
|
|
12151
12266
|
}
|
|
12152
12267
|
if (status === "Failed") {
|
|
12153
12268
|
const icon2 = tty3 ? import_picocolors5.default.red(sym.fail) : sym.fail;
|
|
12154
|
-
const reason = scanDetail.failure_reason
|
|
12269
|
+
const reason = failureSentence(scanDetail.failure_reason);
|
|
12155
12270
|
console.log(`
|
|
12156
12271
|
${icon2} Pentest failed: ${reason}`);
|
|
12157
12272
|
deferredExitCode = 1;
|
|
@@ -12189,8 +12304,8 @@ ${icon} Unexpected status: ${status}`);
|
|
|
12189
12304
|
}
|
|
12190
12305
|
async function printFindingsSummary(client, profileId, tty3) {
|
|
12191
12306
|
try {
|
|
12192
|
-
const reports = await client.reports.list({ limit:
|
|
12193
|
-
const report = reports.items
|
|
12307
|
+
const reports = await client.reports.list({ scan_profile_id: profileId, limit: 1 });
|
|
12308
|
+
const report = reports.items[0];
|
|
12194
12309
|
if (report?.severity_counts) {
|
|
12195
12310
|
const label = tty3 ? import_picocolors5.default.dim("Findings:") : "Findings:";
|
|
12196
12311
|
const summary = renderFindingsSummary(report.severity_counts);
|
|
@@ -12317,6 +12432,15 @@ Timeout: Scout did not connect within ${secs}s.`);
|
|
|
12317
12432
|
}
|
|
12318
12433
|
}
|
|
12319
12434
|
|
|
12435
|
+
// src/lib/validate-mode.ts
|
|
12436
|
+
var VALID_SCAN_MODES = ["full", "quick", "htb", "defcon", "exploit-only"];
|
|
12437
|
+
function validateScanMode(mode) {
|
|
12438
|
+
if (!VALID_SCAN_MODES.includes(mode)) {
|
|
12439
|
+
console.error(`Invalid mode "${mode}". Must be one of: ${VALID_SCAN_MODES.join(", ")}.`);
|
|
12440
|
+
process.exit(2);
|
|
12441
|
+
}
|
|
12442
|
+
}
|
|
12443
|
+
|
|
12320
12444
|
// src/lib/scope.ts
|
|
12321
12445
|
var import_picocolors7 = __toESM(require_picocolors(), 1);
|
|
12322
12446
|
|
|
@@ -12762,14 +12886,26 @@ function ensurePasteBundle(result) {
|
|
|
12762
12886
|
process.exit(1);
|
|
12763
12887
|
}
|
|
12764
12888
|
}
|
|
12889
|
+
var STATUS_FILTERS = {
|
|
12890
|
+
"not-started": "Not Yet Started",
|
|
12891
|
+
running: "Running...",
|
|
12892
|
+
completed: "Completed",
|
|
12893
|
+
failed: "Failed",
|
|
12894
|
+
stopping: "Stopping...",
|
|
12895
|
+
scheduled: "Scheduled"
|
|
12896
|
+
};
|
|
12897
|
+
function isStatusFilter(value) {
|
|
12898
|
+
return Object.hasOwn(STATUS_FILTERS, value);
|
|
12899
|
+
}
|
|
12765
12900
|
function isUserDataTooLarge(err) {
|
|
12766
12901
|
return err.status === 413 || /user-?data too large/i.test(err.detail);
|
|
12767
12902
|
}
|
|
12768
12903
|
function registerPentestCommand(program3) {
|
|
12769
|
-
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
|
|
12904
|
+
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 (default: full; a --profile rerun keeps its saved mode)").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", `
|
|
12770
12905
|
Examples:
|
|
12771
12906
|
$ kinosec pentest -t delikado.org
|
|
12772
12907
|
$ kinosec pentest --profile abc123
|
|
12908
|
+
$ kinosec pentest --profile abc123 --mode quick
|
|
12773
12909
|
$ kinosec pentest -t internal.corp --deployment on-prem
|
|
12774
12910
|
$ kinosec pentest -t example.com --include api.example.com,app.example.com
|
|
12775
12911
|
$ kinosec pentest -t example.com --exclude staging.example.com
|
|
@@ -12778,12 +12914,10 @@ Examples:
|
|
|
12778
12914
|
$ kinosec pentest -t target.htb --mode htb --source ./challenge.zip --zip-password hackthebox
|
|
12779
12915
|
|
|
12780
12916
|
Graybox (--source):
|
|
12781
|
-
Packs the target's source
|
|
12782
|
-
|
|
12783
|
-
|
|
12784
|
-
|
|
12785
|
-
The source is sent over HTTPS, used only on the ephemeral scan droplet, and is NOT stored
|
|
12786
|
-
server-side (the training log records just a "[GRAYBOX] N bytes injected" marker, never the source).`).action(async (options) => {
|
|
12917
|
+
Packs the target's source and sends it with the pentest as extra context.
|
|
12918
|
+
Works with --mode full and quick on your own assets. --budget controls how much
|
|
12919
|
+
source is packed (bigger bundles add context and cost).
|
|
12920
|
+
The source is sent over HTTPS, used only while the pentest runs, and is NOT stored.`).action(async (options) => {
|
|
12787
12921
|
if (!options.target && !options.profile) {
|
|
12788
12922
|
console.error("Provide --target <domain> or --profile <id>.");
|
|
12789
12923
|
process.exit(2);
|
|
@@ -12795,7 +12929,7 @@ Graybox (--source):
|
|
|
12795
12929
|
console.error("Invalid deployment type. Must be: cloud or on-prem.");
|
|
12796
12930
|
process.exit(2);
|
|
12797
12931
|
}
|
|
12798
|
-
validateScanMode(options.mode);
|
|
12932
|
+
if (options.mode !== void 0) validateScanMode(options.mode);
|
|
12799
12933
|
let sourceBudget = DEFAULT_SOURCE_BUDGET;
|
|
12800
12934
|
if (options.source) {
|
|
12801
12935
|
sourceBudget = parseInt(options.budget, 10);
|
|
@@ -12838,7 +12972,11 @@ Graybox (--source):
|
|
|
12838
12972
|
if (options.profile) {
|
|
12839
12973
|
const detail = await client.scans.get(options.profile);
|
|
12840
12974
|
targetDisplay = detail.domain_or_ip;
|
|
12841
|
-
|
|
12975
|
+
const profileParams = { scan_profile_id: options.profile };
|
|
12976
|
+
if (options.mode !== void 0) {
|
|
12977
|
+
profileParams.scan_mode = options.mode;
|
|
12978
|
+
}
|
|
12979
|
+
params = profileParams;
|
|
12842
12980
|
} else {
|
|
12843
12981
|
const parsed = parseTarget(options.target);
|
|
12844
12982
|
for (const warning2 of parsed.warnings) {
|
|
@@ -12869,7 +13007,7 @@ Graybox (--source):
|
|
|
12869
13007
|
targetDisplay = parsed.hostname;
|
|
12870
13008
|
const assetParams = {
|
|
12871
13009
|
asset_id: asset.id,
|
|
12872
|
-
scan_mode: options.mode
|
|
13010
|
+
scan_mode: options.mode ?? "full"
|
|
12873
13011
|
};
|
|
12874
13012
|
if (options.mode === "defcon") {
|
|
12875
13013
|
assetParams.flag_format = options.flag ?? "HTB";
|
|
@@ -13013,17 +13151,36 @@ ${divider}
|
|
|
13013
13151
|
}
|
|
13014
13152
|
});
|
|
13015
13153
|
registerScheduleCommand(pentest);
|
|
13016
|
-
pentest.command("list").description("List pentests").option("--limit <n>", "Max results per page", "20").option("--cursor <cursor>", "Pagination cursor").addHelpText("after", `
|
|
13154
|
+
pentest.command("list").description("List pentests").option("--limit <n>", "Max results per page", "20").option("--cursor <cursor>", "Pagination cursor").option("--status <status>", `Only pentests with this status: ${Object.keys(STATUS_FILTERS).join(", ")}`).option("-t, --target <host>", "Only pentests of this exact host").addHelpText("after", `
|
|
13017
13155
|
Examples:
|
|
13018
13156
|
$ kinosec pentest list
|
|
13019
|
-
$ kinosec pentest list --limit 5 --json
|
|
13157
|
+
$ kinosec pentest list --limit 5 --json
|
|
13158
|
+
$ kinosec pentest list --status failed
|
|
13159
|
+
$ kinosec pentest list -t example.com`).action(async (options) => {
|
|
13160
|
+
const status = options.status;
|
|
13161
|
+
if (status !== void 0 && !isStatusFilter(status)) {
|
|
13162
|
+
console.error(`Invalid --status "${status}". Must be one of: ${Object.keys(STATUS_FILTERS).join(", ")}.`);
|
|
13163
|
+
process.exit(2);
|
|
13164
|
+
}
|
|
13165
|
+
const typedTarget = pentest.opts().target ?? options.target;
|
|
13166
|
+
let target;
|
|
13167
|
+
if (typedTarget !== void 0) {
|
|
13168
|
+
try {
|
|
13169
|
+
target = parseTarget(typedTarget).hostname;
|
|
13170
|
+
} catch (error2) {
|
|
13171
|
+
console.error(error2 instanceof Error ? error2.message : String(error2));
|
|
13172
|
+
process.exit(2);
|
|
13173
|
+
}
|
|
13174
|
+
}
|
|
13020
13175
|
try {
|
|
13021
13176
|
const opts = program3.opts();
|
|
13022
13177
|
const { token, apiUrl } = resolveToken(opts);
|
|
13023
13178
|
const client = new KinoSecClient({ apiUrl, token });
|
|
13024
13179
|
const result = await client.scans.list({
|
|
13025
13180
|
limit: parseInt(options.limit, 10),
|
|
13026
|
-
cursor: options.cursor
|
|
13181
|
+
cursor: options.cursor,
|
|
13182
|
+
status: status === void 0 ? void 0 : STATUS_FILTERS[status],
|
|
13183
|
+
domain_or_ip: target
|
|
13027
13184
|
});
|
|
13028
13185
|
if (opts.json || opts.output === "json") {
|
|
13029
13186
|
printJson(result);
|
|
@@ -13044,8 +13201,9 @@ Examples:
|
|
|
13044
13201
|
])
|
|
13045
13202
|
);
|
|
13046
13203
|
if (result.next_cursor) {
|
|
13204
|
+
const filters = `${status ? ` --status ${status}` : ""}${target ? ` --target ${target}` : ""}`;
|
|
13047
13205
|
console.log(import_picocolors8.default.dim(`
|
|
13048
|
-
More results available
|
|
13206
|
+
More results available: kinosec pentest list${filters} --cursor ${result.next_cursor}`));
|
|
13049
13207
|
}
|
|
13050
13208
|
} catch (error2) {
|
|
13051
13209
|
handleError(error2);
|
|
@@ -13078,6 +13236,10 @@ Examples:
|
|
|
13078
13236
|
["Updated", detail.updated_at]
|
|
13079
13237
|
]
|
|
13080
13238
|
);
|
|
13239
|
+
if (detail.scan_status === "Failed") {
|
|
13240
|
+
console.log(`
|
|
13241
|
+
Failure: ${failureSentence(detail.failure_reason)}`);
|
|
13242
|
+
}
|
|
13081
13243
|
} catch (error2) {
|
|
13082
13244
|
handleError(error2);
|
|
13083
13245
|
}
|
|
@@ -13453,13 +13615,206 @@ Examples:
|
|
|
13453
13615
|
});
|
|
13454
13616
|
}
|
|
13455
13617
|
|
|
13618
|
+
// src/lib/asset-import.ts
|
|
13619
|
+
import { createReadStream } from "fs";
|
|
13620
|
+
|
|
13621
|
+
// src/lib/asset-value.ts
|
|
13622
|
+
var REASON_TEXT = {
|
|
13623
|
+
empty: "Empty",
|
|
13624
|
+
too_long: "Too long",
|
|
13625
|
+
non_ascii_host: "Non-ASCII characters are not supported",
|
|
13626
|
+
invalid_port: "Invalid port",
|
|
13627
|
+
invalid_ip: "Invalid IP address",
|
|
13628
|
+
malformed_host: "Not a valid host",
|
|
13629
|
+
apex_public_suffix: "Public suffix, add a full domain",
|
|
13630
|
+
network_range: "Network ranges aren't supported by asset import"
|
|
13631
|
+
};
|
|
13632
|
+
var MAX_HOST_LENGTH = 253;
|
|
13633
|
+
var MAX_LABEL_LENGTH = 63;
|
|
13634
|
+
var INVISIBLE_CHARS_RE = /[\u200B-\u200D\u2060\uFEFF\u00AD\u202A-\u202E\u2066-\u2069\u00A0]/g;
|
|
13635
|
+
var IPV4_STRICT_RE = /^(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)){3}$/;
|
|
13636
|
+
var HOSTNAME_RE2 = /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$/;
|
|
13637
|
+
var RANGE_RE = /^\d{1,3}(?:\.\d{1,3}){3}\/\d/;
|
|
13638
|
+
function splitValues(text) {
|
|
13639
|
+
return text.split(/\r\n|\r|\n/).flatMap(
|
|
13640
|
+
(lineText, index) => lineText.split(/[,;\s]+/).filter((raw) => raw.length > 0).map((raw) => ({ raw, line: index + 1 }))
|
|
13641
|
+
);
|
|
13642
|
+
}
|
|
13643
|
+
function clean(raw) {
|
|
13644
|
+
return raw.replace(INVISIBLE_CHARS_RE, "").normalize("NFC").trim().replace(/^https?:\/\//i, "");
|
|
13645
|
+
}
|
|
13646
|
+
function normalize2(raw) {
|
|
13647
|
+
const withoutScheme = clean(raw);
|
|
13648
|
+
const slash = withoutScheme.indexOf("/");
|
|
13649
|
+
const host = slash === -1 ? withoutScheme : withoutScheme.slice(0, slash);
|
|
13650
|
+
return dropTrailingDots(host.toLowerCase()).trim();
|
|
13651
|
+
}
|
|
13652
|
+
function dropTrailingDots(text) {
|
|
13653
|
+
let end = text.length;
|
|
13654
|
+
while (end > 0 && text[end - 1] === ".") end--;
|
|
13655
|
+
return text.slice(0, end);
|
|
13656
|
+
}
|
|
13657
|
+
function invalid(reason) {
|
|
13658
|
+
return { valid: false, reason };
|
|
13659
|
+
}
|
|
13660
|
+
function isTooLong(value) {
|
|
13661
|
+
const host = value.replace(/:\d+$/, "");
|
|
13662
|
+
return host.length > MAX_HOST_LENGTH || host.split(".").some((label) => label.length > MAX_LABEL_LENGTH);
|
|
13663
|
+
}
|
|
13664
|
+
function checkValue(raw) {
|
|
13665
|
+
if (RANGE_RE.test(clean(raw))) return invalid("network_range");
|
|
13666
|
+
const value = normalize2(raw);
|
|
13667
|
+
if (value.length === 0) return invalid("empty");
|
|
13668
|
+
if (isTooLong(value)) return invalid("too_long");
|
|
13669
|
+
if (/[\u0080-\uFFFF]/.test(value)) return invalid("non_ascii_host");
|
|
13670
|
+
const colons = value.split(":").length - 1;
|
|
13671
|
+
if (colons >= 2) return invalid("invalid_ip");
|
|
13672
|
+
let host = value;
|
|
13673
|
+
if (colons === 1) {
|
|
13674
|
+
const at = value.lastIndexOf(":");
|
|
13675
|
+
const port = value.slice(at + 1);
|
|
13676
|
+
if (!/^\d+$/.test(port) || Number(port) < 1 || Number(port) > 65535) return invalid("invalid_port");
|
|
13677
|
+
host = value.slice(0, at);
|
|
13678
|
+
}
|
|
13679
|
+
if (/^[\d.]+$/.test(host)) {
|
|
13680
|
+
return IPV4_STRICT_RE.test(host) ? { valid: true, value, type: "ip" } : invalid("invalid_ip");
|
|
13681
|
+
}
|
|
13682
|
+
if (!HOSTNAME_RE2.test(host)) return invalid("malformed_host");
|
|
13683
|
+
if (MULTI_LEVEL_TLDS.has(host) || isPublicSuffix(host)) return invalid("apex_public_suffix");
|
|
13684
|
+
return { valid: true, value, type: detectAssetType(value) };
|
|
13685
|
+
}
|
|
13686
|
+
|
|
13687
|
+
// src/lib/asset-import.ts
|
|
13688
|
+
var MAX_BYTES = 1048576;
|
|
13689
|
+
var MAX_VALUES = 1e4;
|
|
13690
|
+
var PAGE_SIZE = 100;
|
|
13691
|
+
function exitWithInputError(message) {
|
|
13692
|
+
console.error(message);
|
|
13693
|
+
process.exit(2);
|
|
13694
|
+
}
|
|
13695
|
+
function readFailure(error2) {
|
|
13696
|
+
if (error2 instanceof Error && "code" in error2) {
|
|
13697
|
+
if (error2.code === "ENOENT") return "no such file";
|
|
13698
|
+
if (error2.code === "EACCES") return "permission denied";
|
|
13699
|
+
if (error2.code === "EISDIR") return "it is a folder";
|
|
13700
|
+
}
|
|
13701
|
+
return error2 instanceof Error ? error2.message : String(error2);
|
|
13702
|
+
}
|
|
13703
|
+
async function readCapped(source) {
|
|
13704
|
+
const chunks = [];
|
|
13705
|
+
let size = 0;
|
|
13706
|
+
for await (const chunk of source) {
|
|
13707
|
+
const part = chunk;
|
|
13708
|
+
const buffer = Buffer.isBuffer(part) ? part : Buffer.from(String(part));
|
|
13709
|
+
chunks.push(buffer);
|
|
13710
|
+
size += buffer.length;
|
|
13711
|
+
if (size > MAX_BYTES) break;
|
|
13712
|
+
}
|
|
13713
|
+
return Buffer.concat(chunks);
|
|
13714
|
+
}
|
|
13715
|
+
function isUtf16(bytes) {
|
|
13716
|
+
return bytes.length >= 2 && (bytes[0] === 255 && bytes[1] === 254 || bytes[0] === 254 && bytes[1] === 255);
|
|
13717
|
+
}
|
|
13718
|
+
async function readImportValues(file) {
|
|
13719
|
+
const name = file === "-" ? "stdin" : file;
|
|
13720
|
+
let bytes;
|
|
13721
|
+
try {
|
|
13722
|
+
bytes = await readCapped(file === "-" ? process.stdin : createReadStream(file, { end: MAX_BYTES }));
|
|
13723
|
+
} catch (error2) {
|
|
13724
|
+
exitWithInputError(`Cannot read ${name}: ${readFailure(error2)}.`);
|
|
13725
|
+
}
|
|
13726
|
+
if (bytes.length > MAX_BYTES) exitWithInputError(`${name} is larger than 1 MB.`);
|
|
13727
|
+
if (isUtf16(bytes)) exitWithInputError(`${name} is UTF-16. Save it as UTF-8 and try again.`);
|
|
13728
|
+
const values = splitValues(new TextDecoder("utf-8").decode(bytes));
|
|
13729
|
+
if (values.length > MAX_VALUES) {
|
|
13730
|
+
exitWithInputError(`${name} has ${values.length.toLocaleString("en-US")} values. The limit is 10,000.`);
|
|
13731
|
+
}
|
|
13732
|
+
if (values.length === 0) exitWithInputError(`${name} has no values to import.`);
|
|
13733
|
+
return values;
|
|
13734
|
+
}
|
|
13735
|
+
async function orgAssetValues(client) {
|
|
13736
|
+
const values = /* @__PURE__ */ new Set();
|
|
13737
|
+
let cursor;
|
|
13738
|
+
do {
|
|
13739
|
+
const page = await client.assets.list({ limit: PAGE_SIZE, cursor });
|
|
13740
|
+
for (const item of page.items) values.add(item.asset_value);
|
|
13741
|
+
cursor = page.next_cursor ?? void 0;
|
|
13742
|
+
} while (cursor);
|
|
13743
|
+
return values;
|
|
13744
|
+
}
|
|
13745
|
+
function asError(error2) {
|
|
13746
|
+
return error2 instanceof Error ? error2 : new Error(String(error2));
|
|
13747
|
+
}
|
|
13748
|
+
function isAuthError(error2) {
|
|
13749
|
+
return error2 instanceof ApiError && (error2.status === 401 || error2.status === 403);
|
|
13750
|
+
}
|
|
13751
|
+
async function importOne(job) {
|
|
13752
|
+
const { client, input: { raw, line }, seen, existing } = job;
|
|
13753
|
+
const check = checkValue(raw);
|
|
13754
|
+
if (!check.valid) return { kind: "invalid", line, value: raw, reason: check.reason };
|
|
13755
|
+
const { value, type } = check;
|
|
13756
|
+
if (seen.has(value)) return { kind: "skipped", line, value, reason: "repeated" };
|
|
13757
|
+
seen.add(value);
|
|
13758
|
+
if (existing.has(value)) return { kind: "skipped", line, value, reason: "exists" };
|
|
13759
|
+
try {
|
|
13760
|
+
const asset = await client.assets.create({
|
|
13761
|
+
asset_type: type,
|
|
13762
|
+
asset_value: value,
|
|
13763
|
+
parent_domain: deriveParentDomain(value),
|
|
13764
|
+
verification_method: "dns"
|
|
13765
|
+
});
|
|
13766
|
+
return { kind: "created", line, value, id: asset.id };
|
|
13767
|
+
} catch (error2) {
|
|
13768
|
+
if (error2 instanceof ApiError && error2.code === "asset_already_exists") {
|
|
13769
|
+
return { kind: "skipped", line, value, reason: "exists" };
|
|
13770
|
+
}
|
|
13771
|
+
if (isAuthError(error2) || !(error2 instanceof ApiError)) throw error2;
|
|
13772
|
+
return { kind: "failed", line, value, error: asError(error2).message };
|
|
13773
|
+
}
|
|
13774
|
+
}
|
|
13775
|
+
function record(result, entry) {
|
|
13776
|
+
if (entry.kind === "created") result.created.push({ line: entry.line, value: entry.value, id: entry.id });
|
|
13777
|
+
if (entry.kind === "skipped") result.skipped.push({ line: entry.line, value: entry.value, reason: entry.reason });
|
|
13778
|
+
if (entry.kind === "invalid") result.invalid.push({ line: entry.line, value: entry.value, reason: entry.reason });
|
|
13779
|
+
if (entry.kind === "failed") result.failed.push({ line: entry.line, value: entry.value, error: entry.error });
|
|
13780
|
+
}
|
|
13781
|
+
async function importAssets(client, options) {
|
|
13782
|
+
const result = { created: [], skipped: [], invalid: [], failed: [] };
|
|
13783
|
+
const seen = /* @__PURE__ */ new Set();
|
|
13784
|
+
for (const input of options.values) {
|
|
13785
|
+
let entry;
|
|
13786
|
+
try {
|
|
13787
|
+
entry = await importOne({ client, input, seen, existing: options.existing });
|
|
13788
|
+
} catch (error2) {
|
|
13789
|
+
return { result, stopError: asError(error2) };
|
|
13790
|
+
}
|
|
13791
|
+
record(result, entry);
|
|
13792
|
+
options.onEntry?.(entry);
|
|
13793
|
+
}
|
|
13794
|
+
return { result, stopError: null };
|
|
13795
|
+
}
|
|
13796
|
+
function printable(text) {
|
|
13797
|
+
return text.replace(/\p{Cc}/gu, "?");
|
|
13798
|
+
}
|
|
13799
|
+
function entryLine(entry) {
|
|
13800
|
+
if (entry.kind === "created") return `Created ${entry.value}`;
|
|
13801
|
+
if (entry.kind === "invalid") {
|
|
13802
|
+
return `Invalid ${printable(entry.value)} (line ${entry.line}): ${REASON_TEXT[entry.reason]}`;
|
|
13803
|
+
}
|
|
13804
|
+
if (entry.kind === "failed") return `Failed ${entry.value}: ${entry.error}`;
|
|
13805
|
+
return entry.reason === "exists" ? `Skipped ${entry.value}: already exists` : `Skipped ${entry.value} (line ${entry.line}): repeated in file`;
|
|
13806
|
+
}
|
|
13807
|
+
function summaryLine(result) {
|
|
13808
|
+
const { created, skipped, invalid: invalid2, failed } = result;
|
|
13809
|
+
return `Created ${created.length}, skipped ${skipped.length}, invalid ${invalid2.length}, failed ${failed.length}.`;
|
|
13810
|
+
}
|
|
13811
|
+
|
|
13456
13812
|
// src/commands/asset.ts
|
|
13457
13813
|
function registerAssetCommand(program3) {
|
|
13458
13814
|
const asset = program3.command("asset").description("Manage assets");
|
|
13459
|
-
asset.command("list").description("List assets").option("--limit <n>", "Max results per page", "20").option("--cursor <cursor>", "Pagination cursor").option("--
|
|
13815
|
+
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", `
|
|
13460
13816
|
Examples:
|
|
13461
13817
|
$ kinosec asset list
|
|
13462
|
-
$ kinosec asset list --search example.com
|
|
13463
13818
|
$ kinosec asset list --type subdomain --status verified
|
|
13464
13819
|
$ kinosec asset list --limit 50 --json`).action(async (options) => {
|
|
13465
13820
|
try {
|
|
@@ -13469,7 +13824,6 @@ Examples:
|
|
|
13469
13824
|
const result = await client.assets.list({
|
|
13470
13825
|
limit: parseInt(options.limit, 10),
|
|
13471
13826
|
cursor: options.cursor,
|
|
13472
|
-
search: options.search,
|
|
13473
13827
|
status: options.status,
|
|
13474
13828
|
type: options.type
|
|
13475
13829
|
});
|
|
@@ -13570,6 +13924,42 @@ Verify: kinosec asset verify ${result.id}`);
|
|
|
13570
13924
|
handleError(error2);
|
|
13571
13925
|
}
|
|
13572
13926
|
});
|
|
13927
|
+
asset.command("import").description("Add many assets from a text or CSV file").argument("<file>", "File to read, or - to read stdin").addHelpText("after", `
|
|
13928
|
+
Each value is a domain, subdomain or IPv4 address; its type is detected. A
|
|
13929
|
+
:port (example.com:8443) is kept as part of the value. Separate values with
|
|
13930
|
+
newlines, commas, semicolons or spaces. Network ranges are not supported.
|
|
13931
|
+
Limits: UTF-8, 1 MB and 10,000 values per file.
|
|
13932
|
+
|
|
13933
|
+
Values already in your organization are skipped. Assets are created one
|
|
13934
|
+
request at a time, so a long file takes a while. Safe to rerun: values added
|
|
13935
|
+
by an earlier run show as "already exists".
|
|
13936
|
+
|
|
13937
|
+
Token permissions needed: assets:read, assets:write.
|
|
13938
|
+
|
|
13939
|
+
Examples:
|
|
13940
|
+
$ kinosec asset import assets.txt
|
|
13941
|
+
$ cat assets.csv | kinosec asset import -`).action(async (file) => {
|
|
13942
|
+
const values = await readImportValues(file);
|
|
13943
|
+
const opts = program3.opts();
|
|
13944
|
+
const useJson = opts.json || opts.output === "json";
|
|
13945
|
+
let outcome;
|
|
13946
|
+
try {
|
|
13947
|
+
const { token, apiUrl } = resolveToken(opts);
|
|
13948
|
+
const client = new KinoSecClient({ apiUrl, token });
|
|
13949
|
+
const existing = await orgAssetValues(client);
|
|
13950
|
+
outcome = await importAssets(client, {
|
|
13951
|
+
values,
|
|
13952
|
+
existing,
|
|
13953
|
+
onEntry: useJson ? void 0 : (entry) => console.log(entryLine(entry))
|
|
13954
|
+
});
|
|
13955
|
+
} catch (error2) {
|
|
13956
|
+
handleError(error2);
|
|
13957
|
+
}
|
|
13958
|
+
if (useJson) printJson(outcome.result);
|
|
13959
|
+
else console.log(summaryLine(outcome.result));
|
|
13960
|
+
if (outcome.stopError) handleError(outcome.stopError);
|
|
13961
|
+
if (outcome.result.invalid.length > 0 || outcome.result.failed.length > 0) process.exit(1);
|
|
13962
|
+
});
|
|
13573
13963
|
asset.command("delete").description("Delete an asset").argument("<id>", "Asset ID").option("-y, --yes", "Skip confirmation prompt").addHelpText("after", `
|
|
13574
13964
|
Examples:
|
|
13575
13965
|
$ kinosec asset delete abc123
|
|
@@ -13700,12 +14090,39 @@ function formatPoc(poc) {
|
|
|
13700
14090
|
return json === "{}" || json === "[]" ? null : json;
|
|
13701
14091
|
}
|
|
13702
14092
|
var VALID_STATUSES = ["open", "fixed", "false_positive", "accepted_risk", "resolved"];
|
|
14093
|
+
var SEVERITY_FILTERS = ["critical", "high", "medium", "low", "info"];
|
|
14094
|
+
var STATUS_FILTERS2 = [
|
|
14095
|
+
"open",
|
|
14096
|
+
"fixed",
|
|
14097
|
+
"false_positive",
|
|
14098
|
+
"accepted_risk",
|
|
14099
|
+
"investigating",
|
|
14100
|
+
"wont_fix",
|
|
14101
|
+
"resolved"
|
|
14102
|
+
];
|
|
14103
|
+
function isSeverityFilter(value) {
|
|
14104
|
+
return SEVERITY_FILTERS.some((level) => level === value);
|
|
14105
|
+
}
|
|
14106
|
+
function isStatusFilter2(value) {
|
|
14107
|
+
return STATUS_FILTERS2.some((status) => status === value);
|
|
14108
|
+
}
|
|
13703
14109
|
function registerFindingCommand(program3) {
|
|
13704
14110
|
const finding = program3.command("finding").description("Manage findings");
|
|
13705
|
-
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", `
|
|
14111
|
+
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").option("--severity <level>", `Only findings of this severity: ${SEVERITY_FILTERS.join(", ")}`).option("--status <status>", `Only findings with this status: ${STATUS_FILTERS2.join(", ")}`).addHelpText("after", `
|
|
13706
14112
|
Examples:
|
|
13707
|
-
$ kinosec finding list
|
|
14113
|
+
$ kinosec finding list a1b2c3d4-0000-0000-0000-000000000001
|
|
14114
|
+
$ kinosec finding list a1b2c3d4-0000-0000-0000-000000000001 --severity critical --status open
|
|
13708
14115
|
$ kinosec finding list --limit 50 --json`).action(async (profileId, options) => {
|
|
14116
|
+
const severity = options.severity;
|
|
14117
|
+
if (severity !== void 0 && !isSeverityFilter(severity)) {
|
|
14118
|
+
console.error(`Invalid --severity "${severity}". Must be one of: ${SEVERITY_FILTERS.join(", ")}.`);
|
|
14119
|
+
process.exit(2);
|
|
14120
|
+
}
|
|
14121
|
+
const status = options.status;
|
|
14122
|
+
if (status !== void 0 && !isStatusFilter2(status)) {
|
|
14123
|
+
console.error(`Invalid --status "${status}". Must be one of: ${STATUS_FILTERS2.join(", ")}.`);
|
|
14124
|
+
process.exit(2);
|
|
14125
|
+
}
|
|
13709
14126
|
try {
|
|
13710
14127
|
const opts = program3.opts();
|
|
13711
14128
|
const { token, apiUrl } = resolveToken(opts);
|
|
@@ -13713,7 +14130,9 @@ Examples:
|
|
|
13713
14130
|
const result = await client.findings.list({
|
|
13714
14131
|
limit: parseInt(options.limit, 10),
|
|
13715
14132
|
cursor: options.cursor,
|
|
13716
|
-
|
|
14133
|
+
scan_profile_id: profileId,
|
|
14134
|
+
impact_level: severity,
|
|
14135
|
+
status
|
|
13717
14136
|
});
|
|
13718
14137
|
if (opts.json || opts.output === "json") {
|
|
13719
14138
|
printJson(result);
|
|
@@ -13733,8 +14152,9 @@ Examples:
|
|
|
13733
14152
|
])
|
|
13734
14153
|
);
|
|
13735
14154
|
if (result.next_cursor) {
|
|
14155
|
+
const scope = `${profileId ? ` ${profileId}` : ""}${severity ? ` --severity ${severity}` : ""}${status ? ` --status ${status}` : ""}`;
|
|
13736
14156
|
console.log(import_picocolors11.default.dim(`
|
|
13737
|
-
More results available
|
|
14157
|
+
More results available: kinosec finding list${scope} --cursor ${result.next_cursor}`));
|
|
13738
14158
|
}
|
|
13739
14159
|
} catch (error2) {
|
|
13740
14160
|
handleError(error2);
|
|
@@ -13822,69 +14242,163 @@ Examples:
|
|
|
13822
14242
|
|
|
13823
14243
|
// src/commands/report.ts
|
|
13824
14244
|
var import_picocolors12 = __toESM(require_picocolors(), 1);
|
|
13825
|
-
|
|
13826
|
-
|
|
13827
|
-
|
|
13828
|
-
|
|
13829
|
-
|
|
13830
|
-
|
|
13831
|
-
|
|
13832
|
-
|
|
13833
|
-
|
|
13834
|
-
|
|
13835
|
-
|
|
13836
|
-
|
|
13837
|
-
|
|
13838
|
-
|
|
13839
|
-
|
|
13840
|
-
|
|
13841
|
-
|
|
13842
|
-
|
|
13843
|
-
}
|
|
13844
|
-
|
|
13845
|
-
|
|
13846
|
-
|
|
13847
|
-
|
|
13848
|
-
|
|
13849
|
-
|
|
13850
|
-
|
|
13851
|
-
|
|
13852
|
-
|
|
13853
|
-
|
|
13854
|
-
|
|
13855
|
-
|
|
13856
|
-
|
|
13857
|
-
|
|
13858
|
-
|
|
13859
|
-
|
|
13860
|
-
|
|
13861
|
-
|
|
13862
|
-
|
|
13863
|
-
|
|
13864
|
-
|
|
14245
|
+
|
|
14246
|
+
// src/lib/report-summary.ts
|
|
14247
|
+
var POSTURES = ["critical", "high", "medium", "low", "secure", "indeterminate"];
|
|
14248
|
+
var NOT_FINAL_FALLBACK = "Some findings from this scan could not be saved, so this report is not a complete assessment. Re-run the scan for accurate results \u2014 a clean result here does not mean the target is secure.";
|
|
14249
|
+
var PARTIAL_FALLBACK = "This scan did not fully complete, so some findings may be missing. The findings in this report are real \u2014 re-run the scan for complete coverage.";
|
|
14250
|
+
function indented(text) {
|
|
14251
|
+
return text.trim().split("\n").map((line) => ` ${line}`);
|
|
14252
|
+
}
|
|
14253
|
+
function postureLine(summary) {
|
|
14254
|
+
const posture = summary.security_posture;
|
|
14255
|
+
if (posture === void 0) return [];
|
|
14256
|
+
if (summary.incomplete && posture === "secure") return ["Security posture: indeterminate"];
|
|
14257
|
+
return [`Security posture: ${POSTURES.includes(posture) ? posture : "unknown"}`];
|
|
14258
|
+
}
|
|
14259
|
+
function incompleteLines(summary) {
|
|
14260
|
+
if (!summary.incomplete) return [];
|
|
14261
|
+
if (summary.security_posture === "indeterminate") {
|
|
14262
|
+
return ["Scan incomplete: results not final.", summary.incomplete_reason || NOT_FINAL_FALLBACK];
|
|
14263
|
+
}
|
|
14264
|
+
return ["Results may be partial.", summary.incomplete_reason || PARTIAL_FALLBACK];
|
|
14265
|
+
}
|
|
14266
|
+
function summaryLines(summary) {
|
|
14267
|
+
if (!summary) return ["No executive summary for this report."];
|
|
14268
|
+
const sections = [
|
|
14269
|
+
[...postureLine(summary), ...incompleteLines(summary)],
|
|
14270
|
+
summary.executive_summary ? ["Executive summary:", ...indented(summary.executive_summary)] : [],
|
|
14271
|
+
summary.key_concerns?.length ? ["Key concerns:", ...summary.key_concerns.map((c) => ` - ${c}`)] : [],
|
|
14272
|
+
summary.immediate_actions ? ["Immediate actions:", ...indented(summary.immediate_actions)] : []
|
|
14273
|
+
].filter((section) => section.length > 0);
|
|
14274
|
+
if (sections.length === 0) return ["No executive summary for this report."];
|
|
14275
|
+
return sections.flatMap((section, i) => i === 0 ? section : ["", ...section]);
|
|
14276
|
+
}
|
|
14277
|
+
|
|
14278
|
+
// src/lib/uuid.ts
|
|
14279
|
+
var UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
14280
|
+
function isUuid(value) {
|
|
14281
|
+
return UUID_RE2.test(value);
|
|
14282
|
+
}
|
|
14283
|
+
|
|
14284
|
+
// src/lib/export-reports.ts
|
|
14285
|
+
import { existsSync as existsSync4 } from "fs";
|
|
14286
|
+
import { mkdir, rename, writeFile } from "fs/promises";
|
|
14287
|
+
import { join as join4 } from "path";
|
|
14288
|
+
var EXPORT_FORMATS = ["json", "markdown"];
|
|
14289
|
+
var EXTENSIONS = { json: "json", markdown: "md" };
|
|
14290
|
+
var PAGE_SIZE2 = 100;
|
|
14291
|
+
function asError2(error2) {
|
|
14292
|
+
return error2 instanceof Error ? error2 : new Error(String(error2));
|
|
14293
|
+
}
|
|
14294
|
+
function isAuthError2(error2) {
|
|
14295
|
+
return error2 instanceof ApiError && (error2.status === 401 || error2.status === 403);
|
|
14296
|
+
}
|
|
14297
|
+
async function saveReport(client, id, format2, path) {
|
|
14298
|
+
const content = format2 === "markdown" ? await client.reports.exportMarkdown(id) : JSON.stringify(await client.reports.export({ scan_report_id: id, format: "json" }), null, 2) + "\n";
|
|
14299
|
+
const partial = `${path}.partial`;
|
|
14300
|
+
await writeFile(partial, content);
|
|
14301
|
+
await rename(partial, path);
|
|
14302
|
+
}
|
|
14303
|
+
async function exportOne(job) {
|
|
14304
|
+
const { client, options, id, isNewest } = job;
|
|
14305
|
+
if (!isUuid(id)) return { kind: "failed", id, error: "not a valid report ID" };
|
|
14306
|
+
const path = join4(options.outDir, `${id}.${EXTENSIONS[options.format]}`);
|
|
14307
|
+
if (options.skipNewest && isNewest) return { kind: "skipped", id, reason: "running" };
|
|
14308
|
+
if (!options.overwrite && existsSync4(path)) return { kind: "skipped", id, reason: "exists" };
|
|
14309
|
+
try {
|
|
14310
|
+
await saveReport(client, id, options.format, path);
|
|
14311
|
+
return { kind: "written", id, path };
|
|
14312
|
+
} catch (error2) {
|
|
14313
|
+
if (isAuthError2(error2)) throw error2;
|
|
14314
|
+
return { kind: "failed", id, error: asError2(error2).message };
|
|
14315
|
+
}
|
|
14316
|
+
}
|
|
14317
|
+
function record2(result, entry) {
|
|
14318
|
+
if (entry.kind === "written") result.written.push({ id: entry.id, path: entry.path });
|
|
14319
|
+
if (entry.kind === "skipped") result.skipped.push({ id: entry.id, reason: entry.reason });
|
|
14320
|
+
if (entry.kind === "failed") result.failed.push({ id: entry.id, error: entry.error });
|
|
14321
|
+
}
|
|
14322
|
+
async function exportReports(client, options) {
|
|
14323
|
+
const result = { written: [], skipped: [], failed: [] };
|
|
14324
|
+
try {
|
|
14325
|
+
await mkdir(options.outDir, { recursive: true });
|
|
14326
|
+
let cursor;
|
|
14327
|
+
let isNewest = true;
|
|
14328
|
+
do {
|
|
14329
|
+
const page = await client.reports.list({ scan_profile_id: options.profileId, limit: PAGE_SIZE2, cursor });
|
|
14330
|
+
for (const { id } of page.items) {
|
|
14331
|
+
const entry = await exportOne({ client, options, id, isNewest });
|
|
14332
|
+
isNewest = false;
|
|
14333
|
+
record2(result, entry);
|
|
14334
|
+
options.onEntry?.(entry);
|
|
14335
|
+
}
|
|
14336
|
+
cursor = page.next_cursor ?? void 0;
|
|
14337
|
+
} while (cursor);
|
|
14338
|
+
} catch (error2) {
|
|
14339
|
+
return { result, stopError: asError2(error2) };
|
|
14340
|
+
}
|
|
14341
|
+
return { result, stopError: null };
|
|
14342
|
+
}
|
|
14343
|
+
function entryLine2(entry) {
|
|
14344
|
+
if (entry.kind === "written") return `Wrote ${entry.path}`;
|
|
14345
|
+
if (entry.kind === "failed") return `Failed ${entry.id}: ${entry.error}`;
|
|
14346
|
+
return entry.reason === "exists" ? `Skipped ${entry.id}: already exists` : `Skipped ${entry.id}: pentest still running`;
|
|
14347
|
+
}
|
|
14348
|
+
function summaryLine2(result) {
|
|
14349
|
+
return `Exported ${result.written.length}, skipped ${result.skipped.length}, failed ${result.failed.length}.`;
|
|
14350
|
+
}
|
|
14351
|
+
|
|
14352
|
+
// src/commands/report.ts
|
|
14353
|
+
var RUNNING_STATUSES = ["Running...", "Stopping..."];
|
|
14354
|
+
var NO_SCANS_SCOPE_NOTE = "Can't tell whether this pentest is running: this token lacks scans:read.";
|
|
14355
|
+
function exitOnBadPentestId(value) {
|
|
14356
|
+
if (isUuid(value)) return;
|
|
14357
|
+
console.error(`Pentest ID must be a full UUID, got "${value}".`);
|
|
14358
|
+
process.exit(2);
|
|
14359
|
+
}
|
|
14360
|
+
function exitOnBadFormat(value) {
|
|
14361
|
+
if (EXPORT_FORMATS.some((f) => f === value)) return;
|
|
14362
|
+
console.error(`Invalid format "${value}". Valid: ${EXPORT_FORMATS.join(", ")}`);
|
|
14363
|
+
process.exit(2);
|
|
14364
|
+
}
|
|
14365
|
+
function isRunning(scanStatus) {
|
|
14366
|
+
return RUNNING_STATUSES.includes(scanStatus);
|
|
14367
|
+
}
|
|
14368
|
+
async function pentestRunState(client, profileId) {
|
|
14369
|
+
try {
|
|
14370
|
+
const scan = await client.scans.get(profileId);
|
|
14371
|
+
return isRunning(scan.scan_status) ? "running" : "idle";
|
|
14372
|
+
} catch (error2) {
|
|
14373
|
+
if (error2 instanceof ApiError && error2.status === 403) return "unknown";
|
|
14374
|
+
throw error2;
|
|
13865
14375
|
}
|
|
13866
|
-
return lines.join("\n");
|
|
13867
14376
|
}
|
|
13868
14377
|
function registerReportCommand(program3) {
|
|
13869
14378
|
const report = program3.command("report").description("Manage reports");
|
|
13870
|
-
report.command("list").description("List reports").option("--limit <n>", "Max results per page", "20").option("--cursor <cursor>", "Pagination cursor").addHelpText("after", `
|
|
14379
|
+
report.command("list").description("List reports").argument("[profile-id]", "Only this pentest's reports (pentest profile ID)").option("--limit <n>", "Max results per page", "20").option("--cursor <cursor>", "Pagination cursor").addHelpText("after", `
|
|
13871
14380
|
Examples:
|
|
13872
14381
|
$ kinosec report list
|
|
13873
|
-
$ kinosec report list
|
|
14382
|
+
$ kinosec report list 550e8400-e29b-41d4-a716-446655440000
|
|
14383
|
+
$ kinosec report list --limit 10 --json`).action(async (profileId, options) => {
|
|
14384
|
+
if (profileId !== void 0) exitOnBadPentestId(profileId);
|
|
13874
14385
|
try {
|
|
13875
14386
|
const opts = program3.opts();
|
|
13876
14387
|
const { token, apiUrl } = resolveToken(opts);
|
|
13877
14388
|
const client = new KinoSecClient({ apiUrl, token });
|
|
13878
14389
|
const result = await client.reports.list({
|
|
13879
14390
|
limit: parseInt(options.limit, 10),
|
|
13880
|
-
cursor: options.cursor
|
|
14391
|
+
cursor: options.cursor,
|
|
14392
|
+
scan_profile_id: profileId
|
|
13881
14393
|
});
|
|
13882
14394
|
if (opts.json || opts.output === "json") {
|
|
13883
14395
|
printJson(result);
|
|
13884
14396
|
return;
|
|
13885
14397
|
}
|
|
14398
|
+
const runState = profileId ? await pentestRunState(client, profileId) : "idle";
|
|
13886
14399
|
if (result.items.length === 0) {
|
|
13887
|
-
console.log("No reports found.");
|
|
14400
|
+
console.log(runState === "running" ? "No reports yet. This pentest is still running." : "No reports found.");
|
|
14401
|
+
if (runState === "unknown") console.log(NO_SCANS_SCOPE_NOTE);
|
|
13888
14402
|
return;
|
|
13889
14403
|
}
|
|
13890
14404
|
printTable(
|
|
@@ -13894,11 +14408,17 @@ Examples:
|
|
|
13894
14408
|
truncate(r.domain_or_ip, 30),
|
|
13895
14409
|
r.scan_mode,
|
|
13896
14410
|
formatFindings(r.severity_counts),
|
|
13897
|
-
r.scan_completed_at ? new Date(r.scan_completed_at).toLocaleDateString() : "
|
|
14411
|
+
r.scan_completed_at ? new Date(r.scan_completed_at).toLocaleDateString() : "-"
|
|
13898
14412
|
])
|
|
13899
14413
|
);
|
|
14414
|
+
if (runState === "running") {
|
|
14415
|
+
console.log("\nThis pentest is running. Its newest report may still be filling in.");
|
|
14416
|
+
}
|
|
14417
|
+
if (runState === "unknown") console.log(`
|
|
14418
|
+
${NO_SCANS_SCOPE_NOTE}`);
|
|
13900
14419
|
if (result.next_cursor) {
|
|
13901
|
-
console.log(import_picocolors12.default.dim(`
|
|
14420
|
+
console.log(import_picocolors12.default.dim(profileId ? `
|
|
14421
|
+
More results available: kinosec report list ${profileId} --cursor ${result.next_cursor}` : `
|
|
13902
14422
|
More results available. Use --cursor ${result.next_cursor}`));
|
|
13903
14423
|
}
|
|
13904
14424
|
} catch (error2) {
|
|
@@ -13926,7 +14446,7 @@ Examples:
|
|
|
13926
14446
|
["Target", detail.domain_or_ip],
|
|
13927
14447
|
["Profile", detail.scan_profile_name],
|
|
13928
14448
|
["Mode", detail.scan_mode],
|
|
13929
|
-
["Completed", detail.scan_completed_at ?? "
|
|
14449
|
+
["Completed", detail.scan_completed_at ?? "-"],
|
|
13930
14450
|
["Created", detail.created_at]
|
|
13931
14451
|
]
|
|
13932
14452
|
);
|
|
@@ -13936,6 +14456,8 @@ Examples:
|
|
|
13936
14456
|
console.log(` ${severity}: ${count}`);
|
|
13937
14457
|
}
|
|
13938
14458
|
}
|
|
14459
|
+
console.log();
|
|
14460
|
+
for (const line of summaryLines(detail.ai_summary)) console.log(line);
|
|
13939
14461
|
} catch (error2) {
|
|
13940
14462
|
handleError(error2);
|
|
13941
14463
|
}
|
|
@@ -13944,32 +14466,67 @@ Examples:
|
|
|
13944
14466
|
Examples:
|
|
13945
14467
|
$ kinosec report export abc123 --format json > report.json
|
|
13946
14468
|
$ kinosec report export abc123 --format markdown > report.md`).action(async (id, options) => {
|
|
13947
|
-
|
|
13948
|
-
if (!validFormats.includes(options.format)) {
|
|
13949
|
-
console.error(`Invalid format "${options.format}". Valid: ${validFormats.join(", ")}`);
|
|
13950
|
-
process.exit(2);
|
|
13951
|
-
}
|
|
14469
|
+
exitOnBadFormat(options.format);
|
|
13952
14470
|
try {
|
|
13953
14471
|
const opts = program3.opts();
|
|
13954
14472
|
const { token, apiUrl } = resolveToken(opts);
|
|
13955
14473
|
const client = new KinoSecClient({ apiUrl, token });
|
|
13956
|
-
const result = await client.reports.export({
|
|
13957
|
-
scan_report_id: id,
|
|
13958
|
-
format: "json"
|
|
13959
|
-
});
|
|
13960
14474
|
if (options.format === "markdown" && !(opts.json || opts.output === "json")) {
|
|
13961
|
-
process.stdout.write(
|
|
13962
|
-
|
|
13963
|
-
printJson(result);
|
|
14475
|
+
process.stdout.write(await client.reports.exportMarkdown(id));
|
|
14476
|
+
return;
|
|
13964
14477
|
}
|
|
14478
|
+
printJson(await client.reports.export({ scan_report_id: id, format: "json" }));
|
|
13965
14479
|
} catch (error2) {
|
|
13966
14480
|
handleError(error2);
|
|
13967
14481
|
}
|
|
13968
14482
|
});
|
|
14483
|
+
report.command("export-all").description("Save every report of a pentest to a folder").argument("<profile-id>", "Pentest profile ID").option("--out <dir>", "Folder to save the reports in (created if missing)").option("--format <format>", "File format: json or markdown", "json").option("--overwrite", "Replace report files already in the folder").addHelpText("after", `
|
|
14484
|
+
Files are named <report-id>.json or <report-id>.md. Reports already saved in
|
|
14485
|
+
the folder are skipped unless --overwrite is given; other files are never
|
|
14486
|
+
touched. While the pentest runs, its newest report is skipped: run again later.
|
|
14487
|
+
Just after a pentest completes, its newest report can take about a minute to
|
|
14488
|
+
finish. If you exported it then, run again with --overwrite.
|
|
14489
|
+
|
|
14490
|
+
Token permissions needed: reports:read, reports:export, scans:read.
|
|
14491
|
+
|
|
14492
|
+
Examples:
|
|
14493
|
+
$ kinosec report export-all 550e8400-e29b-41d4-a716-446655440000 --out ./reports
|
|
14494
|
+
$ kinosec report export-all 550e8400-e29b-41d4-a716-446655440000 --out ./reports --format markdown`).action(async (profileId, options) => {
|
|
14495
|
+
exitOnBadPentestId(profileId);
|
|
14496
|
+
const format2 = options.format;
|
|
14497
|
+
exitOnBadFormat(format2);
|
|
14498
|
+
if (typeof options.out !== "string" || options.out.trim() === "") {
|
|
14499
|
+
console.error("--out <dir> is required.");
|
|
14500
|
+
process.exit(2);
|
|
14501
|
+
}
|
|
14502
|
+
const opts = program3.opts();
|
|
14503
|
+
const useJson = opts.json || opts.output === "json";
|
|
14504
|
+
let outcome;
|
|
14505
|
+
try {
|
|
14506
|
+
const { token, apiUrl } = resolveToken(opts);
|
|
14507
|
+
const client = new KinoSecClient({ apiUrl, token });
|
|
14508
|
+
const scan = await client.scans.get(profileId);
|
|
14509
|
+
outcome = await exportReports(client, {
|
|
14510
|
+
profileId,
|
|
14511
|
+
format: format2,
|
|
14512
|
+
outDir: options.out,
|
|
14513
|
+
overwrite: options.overwrite === true,
|
|
14514
|
+
skipNewest: isRunning(scan.scan_status),
|
|
14515
|
+
onEntry: useJson ? void 0 : (entry) => console.log(entryLine2(entry))
|
|
14516
|
+
});
|
|
14517
|
+
} catch (error2) {
|
|
14518
|
+
handleError(error2);
|
|
14519
|
+
}
|
|
14520
|
+
if (useJson) printJson(outcome.result);
|
|
14521
|
+
else console.log(summaryLine2(outcome.result));
|
|
14522
|
+
if (outcome.stopError) handleError(outcome.stopError);
|
|
14523
|
+
if (outcome.result.failed.length > 0) process.exit(1);
|
|
14524
|
+
});
|
|
13969
14525
|
}
|
|
13970
14526
|
|
|
13971
14527
|
// src/commands/token.ts
|
|
13972
14528
|
var import_picocolors13 = __toESM(require_picocolors(), 1);
|
|
14529
|
+
var WEB_APP_TOKENS_URL = "https://kinosec.ai/settings?tab=api-tokens";
|
|
13973
14530
|
function registerTokenCommand(program3) {
|
|
13974
14531
|
const token = program3.command("token").description("Manage API tokens");
|
|
13975
14532
|
token.command("list").description("List API tokens").option("--limit <n>", "Max results per page", "20").option("--cursor <cursor>", "Pagination cursor").addHelpText("after", `
|
|
@@ -14015,8 +14572,8 @@ More results available. Use --cursor ${result.next_cursor}`));
|
|
|
14015
14572
|
Examples:
|
|
14016
14573
|
$ kinosec token create --name "CI" --scopes assets:read,scans:create
|
|
14017
14574
|
$ kinosec token create --name "Full" --scopes assets:read,scans:read --expires 30`).action(async (options) => {
|
|
14575
|
+
const opts = program3.opts();
|
|
14018
14576
|
try {
|
|
14019
|
-
const opts = program3.opts();
|
|
14020
14577
|
const { token: authToken, apiUrl } = resolveToken(opts);
|
|
14021
14578
|
const client = new KinoSecClient({ apiUrl, token: authToken });
|
|
14022
14579
|
const scopes = options.scopes.split(",").map((s) => s.trim());
|
|
@@ -14038,6 +14595,13 @@ Examples:
|
|
|
14038
14595
|
console.log(` Scopes: ${result.scopes.join(", ")}`);
|
|
14039
14596
|
console.log(` Expires: ${new Date(result.expires_at).toLocaleDateString()}`);
|
|
14040
14597
|
} catch (error2) {
|
|
14598
|
+
const isJson = opts.json || opts.output === "json";
|
|
14599
|
+
if (error2 instanceof ApiError && error2.code === "pat_cannot_create_token" && !isJson) {
|
|
14600
|
+
printWarning("API tokens can't create other API tokens.");
|
|
14601
|
+
console.error("Create a new token in the KinoSec web app:");
|
|
14602
|
+
console.error(` ${WEB_APP_TOKENS_URL}`);
|
|
14603
|
+
console.error("");
|
|
14604
|
+
}
|
|
14041
14605
|
handleError(error2);
|
|
14042
14606
|
}
|
|
14043
14607
|
});
|
|
@@ -14064,7 +14628,7 @@ Examples:
|
|
|
14064
14628
|
|
|
14065
14629
|
// src/cli.ts
|
|
14066
14630
|
var program2 = new Command();
|
|
14067
|
-
program2.name("kinosec").description("KinoSec CLI \u2014 run pentests from your terminal").version("0.
|
|
14631
|
+
program2.name("kinosec").description("KinoSec CLI \u2014 run pentests from your terminal").version("0.12.0", "-v, --version").option("--token <pat>", "API token (overrides KINOSEC_TOKEN)").option("--output <format>", "Output format: text or json", "text").option("--json", "Shorthand for --output json").option("--no-color", "Disable colors");
|
|
14068
14632
|
registerAuthCommand(program2);
|
|
14069
14633
|
registerMeCommand(program2);
|
|
14070
14634
|
registerCreditsCommand(program2);
|
|
@@ -14073,10 +14637,9 @@ registerAssetCommand(program2);
|
|
|
14073
14637
|
registerFindingCommand(program2);
|
|
14074
14638
|
registerReportCommand(program2);
|
|
14075
14639
|
registerTokenCommand(program2);
|
|
14076
|
-
program2.parseAsync(process.argv).then(() => checkForUpdate("0.
|
|
14640
|
+
program2.parseAsync(process.argv).then(() => checkForUpdate("0.12.0")).catch(handleError);
|
|
14077
14641
|
/*! Bundled license information:
|
|
14078
14642
|
|
|
14079
14643
|
ky/distribution/index.js:
|
|
14080
14644
|
(*! MIT License © Sindre Sorhus *)
|
|
14081
14645
|
*/
|
|
14082
|
-
//# sourceMappingURL=cli.js.map
|