@kinosec/cli 0.11.2 → 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 +543 -76
- package/package.json +1 -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) {
|
|
@@ -12012,6 +12016,33 @@ function normalizeActivityHistory(raw) {
|
|
|
12012
12016
|
});
|
|
12013
12017
|
}
|
|
12014
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;
|
|
12044
|
+
}
|
|
12045
|
+
|
|
12015
12046
|
// src/lib/buzzwords.ts
|
|
12016
12047
|
var SCAN_EXECUTION_PHRASES = [
|
|
12017
12048
|
"DEEP SCAN",
|
|
@@ -12235,7 +12266,7 @@ ${icon2} Pentest completed: ${domain} ${dur}`);
|
|
|
12235
12266
|
}
|
|
12236
12267
|
if (status === "Failed") {
|
|
12237
12268
|
const icon2 = tty3 ? import_picocolors5.default.red(sym.fail) : sym.fail;
|
|
12238
|
-
const reason = scanDetail.failure_reason
|
|
12269
|
+
const reason = failureSentence(scanDetail.failure_reason);
|
|
12239
12270
|
console.log(`
|
|
12240
12271
|
${icon2} Pentest failed: ${reason}`);
|
|
12241
12272
|
deferredExitCode = 1;
|
|
@@ -12273,8 +12304,8 @@ ${icon} Unexpected status: ${status}`);
|
|
|
12273
12304
|
}
|
|
12274
12305
|
async function printFindingsSummary(client, profileId, tty3) {
|
|
12275
12306
|
try {
|
|
12276
|
-
const reports = await client.reports.list({ limit:
|
|
12277
|
-
const report = reports.items
|
|
12307
|
+
const reports = await client.reports.list({ scan_profile_id: profileId, limit: 1 });
|
|
12308
|
+
const report = reports.items[0];
|
|
12278
12309
|
if (report?.severity_counts) {
|
|
12279
12310
|
const label = tty3 ? import_picocolors5.default.dim("Findings:") : "Findings:";
|
|
12280
12311
|
const summary = renderFindingsSummary(report.severity_counts);
|
|
@@ -12855,14 +12886,26 @@ function ensurePasteBundle(result) {
|
|
|
12855
12886
|
process.exit(1);
|
|
12856
12887
|
}
|
|
12857
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
|
+
}
|
|
12858
12900
|
function isUserDataTooLarge(err) {
|
|
12859
12901
|
return err.status === 413 || /user-?data too large/i.test(err.detail);
|
|
12860
12902
|
}
|
|
12861
12903
|
function registerPentestCommand(program3) {
|
|
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
|
|
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", `
|
|
12863
12905
|
Examples:
|
|
12864
12906
|
$ kinosec pentest -t delikado.org
|
|
12865
12907
|
$ kinosec pentest --profile abc123
|
|
12908
|
+
$ kinosec pentest --profile abc123 --mode quick
|
|
12866
12909
|
$ kinosec pentest -t internal.corp --deployment on-prem
|
|
12867
12910
|
$ kinosec pentest -t example.com --include api.example.com,app.example.com
|
|
12868
12911
|
$ kinosec pentest -t example.com --exclude staging.example.com
|
|
@@ -12886,7 +12929,7 @@ Graybox (--source):
|
|
|
12886
12929
|
console.error("Invalid deployment type. Must be: cloud or on-prem.");
|
|
12887
12930
|
process.exit(2);
|
|
12888
12931
|
}
|
|
12889
|
-
validateScanMode(options.mode);
|
|
12932
|
+
if (options.mode !== void 0) validateScanMode(options.mode);
|
|
12890
12933
|
let sourceBudget = DEFAULT_SOURCE_BUDGET;
|
|
12891
12934
|
if (options.source) {
|
|
12892
12935
|
sourceBudget = parseInt(options.budget, 10);
|
|
@@ -12929,7 +12972,11 @@ Graybox (--source):
|
|
|
12929
12972
|
if (options.profile) {
|
|
12930
12973
|
const detail = await client.scans.get(options.profile);
|
|
12931
12974
|
targetDisplay = detail.domain_or_ip;
|
|
12932
|
-
|
|
12975
|
+
const profileParams = { scan_profile_id: options.profile };
|
|
12976
|
+
if (options.mode !== void 0) {
|
|
12977
|
+
profileParams.scan_mode = options.mode;
|
|
12978
|
+
}
|
|
12979
|
+
params = profileParams;
|
|
12933
12980
|
} else {
|
|
12934
12981
|
const parsed = parseTarget(options.target);
|
|
12935
12982
|
for (const warning2 of parsed.warnings) {
|
|
@@ -12960,7 +13007,7 @@ Graybox (--source):
|
|
|
12960
13007
|
targetDisplay = parsed.hostname;
|
|
12961
13008
|
const assetParams = {
|
|
12962
13009
|
asset_id: asset.id,
|
|
12963
|
-
scan_mode: options.mode
|
|
13010
|
+
scan_mode: options.mode ?? "full"
|
|
12964
13011
|
};
|
|
12965
13012
|
if (options.mode === "defcon") {
|
|
12966
13013
|
assetParams.flag_format = options.flag ?? "HTB";
|
|
@@ -13104,17 +13151,36 @@ ${divider}
|
|
|
13104
13151
|
}
|
|
13105
13152
|
});
|
|
13106
13153
|
registerScheduleCommand(pentest);
|
|
13107
|
-
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", `
|
|
13108
13155
|
Examples:
|
|
13109
13156
|
$ kinosec pentest list
|
|
13110
|
-
$ 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
|
+
}
|
|
13111
13175
|
try {
|
|
13112
13176
|
const opts = program3.opts();
|
|
13113
13177
|
const { token, apiUrl } = resolveToken(opts);
|
|
13114
13178
|
const client = new KinoSecClient({ apiUrl, token });
|
|
13115
13179
|
const result = await client.scans.list({
|
|
13116
13180
|
limit: parseInt(options.limit, 10),
|
|
13117
|
-
cursor: options.cursor
|
|
13181
|
+
cursor: options.cursor,
|
|
13182
|
+
status: status === void 0 ? void 0 : STATUS_FILTERS[status],
|
|
13183
|
+
domain_or_ip: target
|
|
13118
13184
|
});
|
|
13119
13185
|
if (opts.json || opts.output === "json") {
|
|
13120
13186
|
printJson(result);
|
|
@@ -13135,8 +13201,9 @@ Examples:
|
|
|
13135
13201
|
])
|
|
13136
13202
|
);
|
|
13137
13203
|
if (result.next_cursor) {
|
|
13204
|
+
const filters = `${status ? ` --status ${status}` : ""}${target ? ` --target ${target}` : ""}`;
|
|
13138
13205
|
console.log(import_picocolors8.default.dim(`
|
|
13139
|
-
More results available
|
|
13206
|
+
More results available: kinosec pentest list${filters} --cursor ${result.next_cursor}`));
|
|
13140
13207
|
}
|
|
13141
13208
|
} catch (error2) {
|
|
13142
13209
|
handleError(error2);
|
|
@@ -13169,6 +13236,10 @@ Examples:
|
|
|
13169
13236
|
["Updated", detail.updated_at]
|
|
13170
13237
|
]
|
|
13171
13238
|
);
|
|
13239
|
+
if (detail.scan_status === "Failed") {
|
|
13240
|
+
console.log(`
|
|
13241
|
+
Failure: ${failureSentence(detail.failure_reason)}`);
|
|
13242
|
+
}
|
|
13172
13243
|
} catch (error2) {
|
|
13173
13244
|
handleError(error2);
|
|
13174
13245
|
}
|
|
@@ -13544,6 +13615,200 @@ Examples:
|
|
|
13544
13615
|
});
|
|
13545
13616
|
}
|
|
13546
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
|
+
|
|
13547
13812
|
// src/commands/asset.ts
|
|
13548
13813
|
function registerAssetCommand(program3) {
|
|
13549
13814
|
const asset = program3.command("asset").description("Manage assets");
|
|
@@ -13659,6 +13924,42 @@ Verify: kinosec asset verify ${result.id}`);
|
|
|
13659
13924
|
handleError(error2);
|
|
13660
13925
|
}
|
|
13661
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
|
+
});
|
|
13662
13963
|
asset.command("delete").description("Delete an asset").argument("<id>", "Asset ID").option("-y, --yes", "Skip confirmation prompt").addHelpText("after", `
|
|
13663
13964
|
Examples:
|
|
13664
13965
|
$ kinosec asset delete abc123
|
|
@@ -13789,12 +14090,39 @@ function formatPoc(poc) {
|
|
|
13789
14090
|
return json === "{}" || json === "[]" ? null : json;
|
|
13790
14091
|
}
|
|
13791
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
|
+
}
|
|
13792
14109
|
function registerFindingCommand(program3) {
|
|
13793
14110
|
const finding = program3.command("finding").description("Manage findings");
|
|
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", `
|
|
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", `
|
|
13795
14112
|
Examples:
|
|
13796
14113
|
$ kinosec finding list a1b2c3d4-0000-0000-0000-000000000001
|
|
14114
|
+
$ kinosec finding list a1b2c3d4-0000-0000-0000-000000000001 --severity critical --status open
|
|
13797
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
|
+
}
|
|
13798
14126
|
try {
|
|
13799
14127
|
const opts = program3.opts();
|
|
13800
14128
|
const { token, apiUrl } = resolveToken(opts);
|
|
@@ -13802,7 +14130,9 @@ Examples:
|
|
|
13802
14130
|
const result = await client.findings.list({
|
|
13803
14131
|
limit: parseInt(options.limit, 10),
|
|
13804
14132
|
cursor: options.cursor,
|
|
13805
|
-
scan_profile_id: profileId
|
|
14133
|
+
scan_profile_id: profileId,
|
|
14134
|
+
impact_level: severity,
|
|
14135
|
+
status
|
|
13806
14136
|
});
|
|
13807
14137
|
if (opts.json || opts.output === "json") {
|
|
13808
14138
|
printJson(result);
|
|
@@ -13822,8 +14152,9 @@ Examples:
|
|
|
13822
14152
|
])
|
|
13823
14153
|
);
|
|
13824
14154
|
if (result.next_cursor) {
|
|
14155
|
+
const scope = `${profileId ? ` ${profileId}` : ""}${severity ? ` --severity ${severity}` : ""}${status ? ` --status ${status}` : ""}`;
|
|
13825
14156
|
console.log(import_picocolors11.default.dim(`
|
|
13826
|
-
More results available
|
|
14157
|
+
More results available: kinosec finding list${scope} --cursor ${result.next_cursor}`));
|
|
13827
14158
|
}
|
|
13828
14159
|
} catch (error2) {
|
|
13829
14160
|
handleError(error2);
|
|
@@ -13911,69 +14242,163 @@ Examples:
|
|
|
13911
14242
|
|
|
13912
14243
|
// src/commands/report.ts
|
|
13913
14244
|
var import_picocolors12 = __toESM(require_picocolors(), 1);
|
|
13914
|
-
|
|
13915
|
-
|
|
13916
|
-
|
|
13917
|
-
|
|
13918
|
-
|
|
13919
|
-
|
|
13920
|
-
|
|
13921
|
-
|
|
13922
|
-
|
|
13923
|
-
|
|
13924
|
-
|
|
13925
|
-
|
|
13926
|
-
|
|
13927
|
-
|
|
13928
|
-
|
|
13929
|
-
|
|
13930
|
-
|
|
13931
|
-
|
|
13932
|
-
}
|
|
13933
|
-
|
|
13934
|
-
|
|
13935
|
-
|
|
13936
|
-
|
|
13937
|
-
|
|
13938
|
-
|
|
13939
|
-
|
|
13940
|
-
|
|
13941
|
-
|
|
13942
|
-
|
|
13943
|
-
|
|
13944
|
-
|
|
13945
|
-
|
|
13946
|
-
|
|
13947
|
-
|
|
13948
|
-
|
|
13949
|
-
|
|
13950
|
-
|
|
13951
|
-
|
|
13952
|
-
|
|
13953
|
-
|
|
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;
|
|
13954
14375
|
}
|
|
13955
|
-
return lines.join("\n");
|
|
13956
14376
|
}
|
|
13957
14377
|
function registerReportCommand(program3) {
|
|
13958
14378
|
const report = program3.command("report").description("Manage reports");
|
|
13959
|
-
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", `
|
|
13960
14380
|
Examples:
|
|
13961
14381
|
$ kinosec report list
|
|
13962
|
-
$ 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);
|
|
13963
14385
|
try {
|
|
13964
14386
|
const opts = program3.opts();
|
|
13965
14387
|
const { token, apiUrl } = resolveToken(opts);
|
|
13966
14388
|
const client = new KinoSecClient({ apiUrl, token });
|
|
13967
14389
|
const result = await client.reports.list({
|
|
13968
14390
|
limit: parseInt(options.limit, 10),
|
|
13969
|
-
cursor: options.cursor
|
|
14391
|
+
cursor: options.cursor,
|
|
14392
|
+
scan_profile_id: profileId
|
|
13970
14393
|
});
|
|
13971
14394
|
if (opts.json || opts.output === "json") {
|
|
13972
14395
|
printJson(result);
|
|
13973
14396
|
return;
|
|
13974
14397
|
}
|
|
14398
|
+
const runState = profileId ? await pentestRunState(client, profileId) : "idle";
|
|
13975
14399
|
if (result.items.length === 0) {
|
|
13976
|
-
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);
|
|
13977
14402
|
return;
|
|
13978
14403
|
}
|
|
13979
14404
|
printTable(
|
|
@@ -13983,11 +14408,17 @@ Examples:
|
|
|
13983
14408
|
truncate(r.domain_or_ip, 30),
|
|
13984
14409
|
r.scan_mode,
|
|
13985
14410
|
formatFindings(r.severity_counts),
|
|
13986
|
-
r.scan_completed_at ? new Date(r.scan_completed_at).toLocaleDateString() : "
|
|
14411
|
+
r.scan_completed_at ? new Date(r.scan_completed_at).toLocaleDateString() : "-"
|
|
13987
14412
|
])
|
|
13988
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}`);
|
|
13989
14419
|
if (result.next_cursor) {
|
|
13990
|
-
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}` : `
|
|
13991
14422
|
More results available. Use --cursor ${result.next_cursor}`));
|
|
13992
14423
|
}
|
|
13993
14424
|
} catch (error2) {
|
|
@@ -14015,7 +14446,7 @@ Examples:
|
|
|
14015
14446
|
["Target", detail.domain_or_ip],
|
|
14016
14447
|
["Profile", detail.scan_profile_name],
|
|
14017
14448
|
["Mode", detail.scan_mode],
|
|
14018
|
-
["Completed", detail.scan_completed_at ?? "
|
|
14449
|
+
["Completed", detail.scan_completed_at ?? "-"],
|
|
14019
14450
|
["Created", detail.created_at]
|
|
14020
14451
|
]
|
|
14021
14452
|
);
|
|
@@ -14025,6 +14456,8 @@ Examples:
|
|
|
14025
14456
|
console.log(` ${severity}: ${count}`);
|
|
14026
14457
|
}
|
|
14027
14458
|
}
|
|
14459
|
+
console.log();
|
|
14460
|
+
for (const line of summaryLines(detail.ai_summary)) console.log(line);
|
|
14028
14461
|
} catch (error2) {
|
|
14029
14462
|
handleError(error2);
|
|
14030
14463
|
}
|
|
@@ -14033,27 +14466,61 @@ Examples:
|
|
|
14033
14466
|
Examples:
|
|
14034
14467
|
$ kinosec report export abc123 --format json > report.json
|
|
14035
14468
|
$ kinosec report export abc123 --format markdown > report.md`).action(async (id, options) => {
|
|
14036
|
-
|
|
14037
|
-
if (!validFormats.includes(options.format)) {
|
|
14038
|
-
console.error(`Invalid format "${options.format}". Valid: ${validFormats.join(", ")}`);
|
|
14039
|
-
process.exit(2);
|
|
14040
|
-
}
|
|
14469
|
+
exitOnBadFormat(options.format);
|
|
14041
14470
|
try {
|
|
14042
14471
|
const opts = program3.opts();
|
|
14043
14472
|
const { token, apiUrl } = resolveToken(opts);
|
|
14044
14473
|
const client = new KinoSecClient({ apiUrl, token });
|
|
14045
|
-
const result = await client.reports.export({
|
|
14046
|
-
scan_report_id: id,
|
|
14047
|
-
format: "json"
|
|
14048
|
-
});
|
|
14049
14474
|
if (options.format === "markdown" && !(opts.json || opts.output === "json")) {
|
|
14050
|
-
process.stdout.write(
|
|
14051
|
-
|
|
14052
|
-
printJson(result);
|
|
14475
|
+
process.stdout.write(await client.reports.exportMarkdown(id));
|
|
14476
|
+
return;
|
|
14053
14477
|
}
|
|
14478
|
+
printJson(await client.reports.export({ scan_report_id: id, format: "json" }));
|
|
14479
|
+
} catch (error2) {
|
|
14480
|
+
handleError(error2);
|
|
14481
|
+
}
|
|
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
|
+
});
|
|
14054
14517
|
} catch (error2) {
|
|
14055
14518
|
handleError(error2);
|
|
14056
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);
|
|
14057
14524
|
});
|
|
14058
14525
|
}
|
|
14059
14526
|
|
|
@@ -14161,7 +14628,7 @@ Examples:
|
|
|
14161
14628
|
|
|
14162
14629
|
// src/cli.ts
|
|
14163
14630
|
var program2 = new Command();
|
|
14164
|
-
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");
|
|
14165
14632
|
registerAuthCommand(program2);
|
|
14166
14633
|
registerMeCommand(program2);
|
|
14167
14634
|
registerCreditsCommand(program2);
|
|
@@ -14170,7 +14637,7 @@ registerAssetCommand(program2);
|
|
|
14170
14637
|
registerFindingCommand(program2);
|
|
14171
14638
|
registerReportCommand(program2);
|
|
14172
14639
|
registerTokenCommand(program2);
|
|
14173
|
-
program2.parseAsync(process.argv).then(() => checkForUpdate("0.
|
|
14640
|
+
program2.parseAsync(process.argv).then(() => checkForUpdate("0.12.0")).catch(handleError);
|
|
14174
14641
|
/*! Bundled license information:
|
|
14175
14642
|
|
|
14176
14643
|
ky/distribution/index.js:
|