securewithtrace 0.38.0 → 0.40.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/README.md +40 -1
- package/dist/index.js +1416 -143
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -25006,34 +25006,6 @@ var PentestDetailResponseSchema = PentestResponseSchema.extend({
|
|
|
25006
25006
|
testingWindowStart: external_exports.coerce.date().nullable().optional(),
|
|
25007
25007
|
testingWindowEnd: external_exports.coerce.date().nullable().optional(),
|
|
25008
25008
|
engagementMetadataOverrides: EngagementMetadataOverridesSchema.optional(),
|
|
25009
|
-
/**
|
|
25010
|
-
* True once the engagement's repository scope has been snapshotted, which
|
|
25011
|
-
* PERMANENTLY refuses bind and unbind (`repositoryScopes > 0` in
|
|
25012
|
-
* `requireBindableInLock`).
|
|
25013
|
-
*
|
|
25014
|
-
* This one refusal only — that guard also refuses on `PENTEST_CLOSED` and on a
|
|
25015
|
-
* non-scoping stage, both of which the client can already read off `status` and
|
|
25016
|
-
* `currentStage` on this same response. So `false` means "not frozen by the
|
|
25017
|
-
* snapshot", NOT "a bind will succeed"; a consumer that gates a control must check
|
|
25018
|
-
* the stage and closed state too.
|
|
25019
|
-
*
|
|
25020
|
-
* Needed as its OWN field because nothing else on the wire answers it: the scope
|
|
25021
|
-
* freezes at the first scope-consuming session create, and only VALIDATION moves
|
|
25022
|
-
* the stage, so RECON / CODE_REVIEW / PRODUCT_RECON leave the engagement in
|
|
25023
|
-
* `DRAFT` with permanently frozen bindings. `getRepositoryScope` reports
|
|
25024
|
-
* `source: 'PENTEST_SCOPE'` only PAST the scoping stages, so a frozen `DRAFT`
|
|
25025
|
-
* engagement still returns live application links there.
|
|
25026
|
-
*
|
|
25027
|
-
* Scoped to BINDINGS, not to the whole target: environment add/remove is gated on
|
|
25028
|
-
* stage alone (`PentestEnvironmentService`, `NON_SCOPING_PENTEST_STAGES`), and
|
|
25029
|
-
* `DRAFT` is a scoping stage — so a consumer must not disable environment editing
|
|
25030
|
-
* on this flag. Doing so would remove the one provisioning action a frozen
|
|
25031
|
-
* multi-application engagement still legitimately needs.
|
|
25032
|
-
*
|
|
25033
|
-
* Detail-only (the list schema deliberately carries no per-pentest aggregates) and
|
|
25034
|
-
* `.optional()` for the same rolling-deploy reason as the fields above.
|
|
25035
|
-
*/
|
|
25036
|
-
applicationBindingFrozen: external_exports.boolean().optional(),
|
|
25037
25009
|
/**
|
|
25038
25010
|
* Optional customer intake questionnaire answers. Staff detail only.
|
|
25039
25011
|
* `.optional()` for rolling-deploy safety (Vercel ahead of ECS).
|
|
@@ -25481,7 +25453,18 @@ var ClaimedDesktopActionSchema = external_exports.object({
|
|
|
25481
25453
|
kind: external_exports.enum(DesktopActionKindValues),
|
|
25482
25454
|
request: external_exports.unknown(),
|
|
25483
25455
|
/** Epoch millis; past this the box abandons the work and reports nothing. */
|
|
25484
|
-
expiresAtMs: external_exports.number().int().positive()
|
|
25456
|
+
expiresAtMs: external_exports.number().int().positive(),
|
|
25457
|
+
/**
|
|
25458
|
+
* A presigned PUT for the ONE artifact this kind can produce, issued with the
|
|
25459
|
+
* claim so an action does not spend a second api round trip asking for it.
|
|
25460
|
+
* The key is target-relative and travels back on `complete` unchanged.
|
|
25461
|
+
*
|
|
25462
|
+
* Null when the api has no artifacts bucket configured, and absent from an
|
|
25463
|
+
* api that predates this. The box presigns for itself in both cases, and also
|
|
25464
|
+
* whenever the PUT is refused, which is what keeps the 15-minute URL safe to
|
|
25465
|
+
* issue at claim time for an action that may run longer than that.
|
|
25466
|
+
*/
|
|
25467
|
+
artifactUpload: external_exports.object({ key: external_exports.string().min(1).max(1024), url: external_exports.url() }).nullable().optional()
|
|
25485
25468
|
});
|
|
25486
25469
|
var DesktopTargetClaimResponseSchema = external_exports.object({
|
|
25487
25470
|
action: ClaimedDesktopActionSchema.nullable(),
|
|
@@ -25501,7 +25484,18 @@ var DesktopTargetCompleteRequestSchema = external_exports.object({
|
|
|
25501
25484
|
*/
|
|
25502
25485
|
resultS3Key: external_exports.string().min(1).max(1024).optional(),
|
|
25503
25486
|
/** Shape-only. Never customer data. */
|
|
25504
|
-
failureDetails: external_exports.string().max(2e3).optional()
|
|
25487
|
+
failureDetails: external_exports.string().max(2e3).optional(),
|
|
25488
|
+
/**
|
|
25489
|
+
* What the box spent, as a SIBLING of `result` rather than a member of it.
|
|
25490
|
+
*
|
|
25491
|
+
* Two reasons it cannot live inside `result`: the api parses that against the
|
|
25492
|
+
* kind's own schema, which strips anything the schema does not declare, and
|
|
25493
|
+
* the box sizes its inline output against the 32 KB cap before this is known.
|
|
25494
|
+
*/
|
|
25495
|
+
rigTimings: external_exports.object({
|
|
25496
|
+
executeMs: external_exports.number().int().nonnegative(),
|
|
25497
|
+
uploadMs: external_exports.number().int().nonnegative()
|
|
25498
|
+
}).optional()
|
|
25505
25499
|
});
|
|
25506
25500
|
var DesktopTargetCompleteResponseSchema = external_exports.object({ ok: external_exports.literal(true) });
|
|
25507
25501
|
var DesktopTargetPresignRequestSchema = external_exports.object({
|
|
@@ -32310,12 +32304,15 @@ var StaffOrganizationSummarySchema = external_exports.object({
|
|
|
32310
32304
|
var ListAllOrganizationsResponseSchema = external_exports.array(StaffOrganizationSummarySchema);
|
|
32311
32305
|
var CreateOrganizationRequestSchema = external_exports.object({
|
|
32312
32306
|
name: external_exports.string().trim().min(2, "Organization name must be at least 2 characters").max(100, "Organization name must be 100 characters or fewer"),
|
|
32313
|
-
referrerId: external_exports.uuid().nullable().optional()
|
|
32307
|
+
referrerId: external_exports.uuid().nullable().optional(),
|
|
32308
|
+
homepageUrl: external_exports.url("Homepage must be a full URL, like https://acme.com").max(2048).nullable().optional()
|
|
32314
32309
|
});
|
|
32310
|
+
var LogoFetchOutcomeValues = ["skipped", "stored", "not_found", "failed"];
|
|
32315
32311
|
var CreateOrganizationResponseSchema = external_exports.object({
|
|
32316
32312
|
organizationId: external_exports.uuid(),
|
|
32317
32313
|
slug: external_exports.string(),
|
|
32318
|
-
name: external_exports.string()
|
|
32314
|
+
name: external_exports.string(),
|
|
32315
|
+
logoFetch: external_exports.enum(LogoFetchOutcomeValues)
|
|
32319
32316
|
});
|
|
32320
32317
|
var OnboardingItemValues = [
|
|
32321
32318
|
"INVITE_MEMBERS",
|
|
@@ -33183,10 +33180,9 @@ var pentestContract = c27.router({
|
|
|
33183
33180
|
401: ErrorResponseSchema,
|
|
33184
33181
|
403: ErrorResponseSchema,
|
|
33185
33182
|
404: ErrorResponseSchema,
|
|
33186
|
-
// 409
|
|
33187
|
-
//
|
|
33188
|
-
//
|
|
33189
|
-
// so a new binding could never enter it).
|
|
33183
|
+
// 409: PENTEST_APPLICATION_ALREADY_BOUND (unique violation), PENTEST_CLOSED,
|
|
33184
|
+
// or PENTEST_APPLICATION_WITHOUT_REPOSITORY (the repository scope is already
|
|
33185
|
+
// snapshotted and this application has no scan-enabled repository to add).
|
|
33190
33186
|
409: ErrorResponseSchema,
|
|
33191
33187
|
500: ErrorResponseSchema
|
|
33192
33188
|
}
|
|
@@ -33202,7 +33198,7 @@ var pentestContract = c27.router({
|
|
|
33202
33198
|
403: ErrorResponseSchema,
|
|
33203
33199
|
404: ErrorResponseSchema,
|
|
33204
33200
|
// 409 — PENTEST_APPLICATION_UNBIND_BLOCKED (this application still has
|
|
33205
|
-
// linked environments)
|
|
33201
|
+
// linked environments) or PENTEST_CLOSED.
|
|
33206
33202
|
409: ErrorResponseSchema,
|
|
33207
33203
|
500: ErrorResponseSchema
|
|
33208
33204
|
}
|
|
@@ -37929,18 +37925,18 @@ var proto = Object.defineProperties(() => {
|
|
|
37929
37925
|
}
|
|
37930
37926
|
}
|
|
37931
37927
|
});
|
|
37932
|
-
var createStyler = (
|
|
37928
|
+
var createStyler = (open2, close, parent) => {
|
|
37933
37929
|
let openAll;
|
|
37934
37930
|
let closeAll;
|
|
37935
37931
|
if (parent === void 0) {
|
|
37936
|
-
openAll =
|
|
37932
|
+
openAll = open2;
|
|
37937
37933
|
closeAll = close;
|
|
37938
37934
|
} else {
|
|
37939
|
-
openAll = parent.openAll +
|
|
37935
|
+
openAll = parent.openAll + open2;
|
|
37940
37936
|
closeAll = close + parent.closeAll;
|
|
37941
37937
|
}
|
|
37942
37938
|
return {
|
|
37943
|
-
open,
|
|
37939
|
+
open: open2,
|
|
37944
37940
|
close,
|
|
37945
37941
|
openAll,
|
|
37946
37942
|
closeAll,
|
|
@@ -39659,6 +39655,7 @@ function sanitizeDisplay(value) {
|
|
|
39659
39655
|
}
|
|
39660
39656
|
var SUCCESS_STATUS = 200;
|
|
39661
39657
|
var CREATED_STATUS = 201;
|
|
39658
|
+
var ACCEPTED_STATUS = 202;
|
|
39662
39659
|
var NO_CONTENT_STATUS = 204;
|
|
39663
39660
|
var UNAUTHORIZED_STATUS2 = 401;
|
|
39664
39661
|
function parseOutput(value) {
|
|
@@ -41588,6 +41585,323 @@ ${b}
|
|
|
41588
41585
|
};
|
|
41589
41586
|
var i = `${styleText2("gray", S_BAR)} `;
|
|
41590
41587
|
|
|
41588
|
+
// src/org/update.ts
|
|
41589
|
+
import { readFileSync as readFileSync2, statSync } from "fs";
|
|
41590
|
+
import { basename, extname } from "path";
|
|
41591
|
+
var LOGO_CONTENT_TYPE_BY_EXTENSION = {
|
|
41592
|
+
".png": "image/png",
|
|
41593
|
+
".jpg": "image/jpeg",
|
|
41594
|
+
".jpeg": "image/jpeg",
|
|
41595
|
+
".gif": "image/gif",
|
|
41596
|
+
".svg": "image/svg+xml",
|
|
41597
|
+
".webp": "image/webp"
|
|
41598
|
+
};
|
|
41599
|
+
var SUPPORTED_LOGO_EXTENSIONS = Object.keys(
|
|
41600
|
+
LOGO_CONTENT_TYPE_BY_EXTENSION
|
|
41601
|
+
);
|
|
41602
|
+
var MAX_LOGO_UPLOAD_BYTES = 2 * 1024 * 1024;
|
|
41603
|
+
function logoContentTypeForFile(path) {
|
|
41604
|
+
return LOGO_CONTENT_TYPE_BY_EXTENSION[extname(path).toLowerCase()] ?? null;
|
|
41605
|
+
}
|
|
41606
|
+
function isHttpUrl(value) {
|
|
41607
|
+
try {
|
|
41608
|
+
const parsed = new URL(value);
|
|
41609
|
+
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
|
41610
|
+
} catch {
|
|
41611
|
+
return false;
|
|
41612
|
+
}
|
|
41613
|
+
}
|
|
41614
|
+
var URL_SCHEME_PATTERN = /^[a-z][a-z0-9+.-]*:\/\//i;
|
|
41615
|
+
function resolveLogoValue(value, flag) {
|
|
41616
|
+
const trimmed = value.trim();
|
|
41617
|
+
if (trimmed.length === 0) {
|
|
41618
|
+
return { error: `--${flag} cannot be empty. Pass an image file path or an http(s) URL.` };
|
|
41619
|
+
}
|
|
41620
|
+
if (isHttpUrl(trimmed)) {
|
|
41621
|
+
return { change: { action: "url", url: trimmed } };
|
|
41622
|
+
}
|
|
41623
|
+
if (URL_SCHEME_PATTERN.test(trimmed)) {
|
|
41624
|
+
return {
|
|
41625
|
+
error: `--${flag} '${sanitizeDisplay(trimmed)}' must be an http(s) URL or a file path.`
|
|
41626
|
+
};
|
|
41627
|
+
}
|
|
41628
|
+
const contentType = logoContentTypeForFile(trimmed);
|
|
41629
|
+
if (!contentType) {
|
|
41630
|
+
return {
|
|
41631
|
+
error: `--${flag} '${sanitizeDisplay(trimmed)}' has an unsupported extension. Use one of: ${SUPPORTED_LOGO_EXTENSIONS.join(", ")}.`
|
|
41632
|
+
};
|
|
41633
|
+
}
|
|
41634
|
+
let sizeBytes;
|
|
41635
|
+
try {
|
|
41636
|
+
const stats = statSync(trimmed);
|
|
41637
|
+
if (!stats.isFile()) {
|
|
41638
|
+
return { error: `--${flag} '${sanitizeDisplay(trimmed)}' is not a regular file.` };
|
|
41639
|
+
}
|
|
41640
|
+
sizeBytes = stats.size;
|
|
41641
|
+
} catch {
|
|
41642
|
+
return { error: `Cannot read --${flag} '${sanitizeDisplay(trimmed)}'.` };
|
|
41643
|
+
}
|
|
41644
|
+
if (sizeBytes === 0) {
|
|
41645
|
+
return { error: `--${flag} '${sanitizeDisplay(trimmed)}' is empty.` };
|
|
41646
|
+
}
|
|
41647
|
+
if (sizeBytes > MAX_LOGO_UPLOAD_BYTES) {
|
|
41648
|
+
return {
|
|
41649
|
+
error: `--${flag} is ${formatBytes(sizeBytes)}; logo files must be ${formatBytes(MAX_LOGO_UPLOAD_BYTES)} or smaller.`
|
|
41650
|
+
};
|
|
41651
|
+
}
|
|
41652
|
+
return { change: { action: "upload", file: { path: trimmed, contentType, sizeBytes } } };
|
|
41653
|
+
}
|
|
41654
|
+
function formatBytes(bytes) {
|
|
41655
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
41656
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
|
|
41657
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
41658
|
+
}
|
|
41659
|
+
var NOTHING_TO_UPDATE = "Nothing to update. Pass at least one of --homepage, --clear-homepage, --logo, --clear-logo, --dark-logo, --clear-dark-logo, --logos-from-homepage.";
|
|
41660
|
+
function resolveLogoSlot(value, clear, flag) {
|
|
41661
|
+
const resolved = resolveClearableOption(value, clear, flag);
|
|
41662
|
+
if ("error" in resolved) {
|
|
41663
|
+
return resolved;
|
|
41664
|
+
}
|
|
41665
|
+
if (resolved.value === null) {
|
|
41666
|
+
return { change: { action: "clear" } };
|
|
41667
|
+
}
|
|
41668
|
+
if (resolved.value === void 0) {
|
|
41669
|
+
return { change: void 0 };
|
|
41670
|
+
}
|
|
41671
|
+
const logo = resolveLogoValue(resolved.value, flag);
|
|
41672
|
+
return "error" in logo ? logo : { change: logo.change };
|
|
41673
|
+
}
|
|
41674
|
+
function planOrgUpdate(options) {
|
|
41675
|
+
const homepage = resolveClearableOption(options.homepage, options.clearHomepage, "homepage");
|
|
41676
|
+
if ("error" in homepage) {
|
|
41677
|
+
return homepage;
|
|
41678
|
+
}
|
|
41679
|
+
if (typeof homepage.value === "string" && !isHttpUrl(homepage.value.trim())) {
|
|
41680
|
+
return {
|
|
41681
|
+
error: `--homepage '${sanitizeDisplay(homepage.value)}' must be an http(s) URL (e.g. https://acme.com).`
|
|
41682
|
+
};
|
|
41683
|
+
}
|
|
41684
|
+
const logo = resolveLogoSlot(options.logo, options.clearLogo, "logo");
|
|
41685
|
+
if ("error" in logo) {
|
|
41686
|
+
return logo;
|
|
41687
|
+
}
|
|
41688
|
+
const darkLogo = resolveLogoSlot(options.darkLogo, options.clearDarkLogo, "dark-logo");
|
|
41689
|
+
if ("error" in darkLogo) {
|
|
41690
|
+
return darkLogo;
|
|
41691
|
+
}
|
|
41692
|
+
const derive = options.logosFromHomepage === true;
|
|
41693
|
+
if (derive && (logo.change !== void 0 || darkLogo.change !== void 0)) {
|
|
41694
|
+
return {
|
|
41695
|
+
error: "Pass either --logos-from-homepage or explicit logo flags (--logo, --dark-logo, --clear-logo, --clear-dark-logo), not both."
|
|
41696
|
+
};
|
|
41697
|
+
}
|
|
41698
|
+
if (derive && homepage.value === null) {
|
|
41699
|
+
return { error: "--logos-from-homepage needs a homepage; drop --clear-homepage." };
|
|
41700
|
+
}
|
|
41701
|
+
const homepageUrl = typeof homepage.value === "string" ? homepage.value.trim() : homepage.value;
|
|
41702
|
+
if (homepageUrl === void 0 && logo.change === void 0 && darkLogo.change === void 0 && !derive) {
|
|
41703
|
+
return { error: NOTHING_TO_UPDATE };
|
|
41704
|
+
}
|
|
41705
|
+
return {
|
|
41706
|
+
plan: {
|
|
41707
|
+
homepageUrl,
|
|
41708
|
+
logo: logo.change,
|
|
41709
|
+
darkLogo: darkLogo.change,
|
|
41710
|
+
deriveLogosFromHomepage: derive
|
|
41711
|
+
}
|
|
41712
|
+
};
|
|
41713
|
+
}
|
|
41714
|
+
function slotFields(change, uploadedKey, variant) {
|
|
41715
|
+
switch (change.action) {
|
|
41716
|
+
case "clear":
|
|
41717
|
+
return { url: null, s3Key: null };
|
|
41718
|
+
case "url":
|
|
41719
|
+
return { url: change.url, s3Key: null };
|
|
41720
|
+
case "upload":
|
|
41721
|
+
if (uploadedKey === void 0) {
|
|
41722
|
+
throw new Error(
|
|
41723
|
+
`Missing uploaded S3 key for the ${variant} logo; expected the upload step to run before building the update body.`
|
|
41724
|
+
);
|
|
41725
|
+
}
|
|
41726
|
+
return { url: null, s3Key: uploadedKey };
|
|
41727
|
+
}
|
|
41728
|
+
}
|
|
41729
|
+
function buildOrgUpdateBody(plan, uploaded) {
|
|
41730
|
+
const body = {};
|
|
41731
|
+
if (plan.homepageUrl !== void 0) {
|
|
41732
|
+
body.homepageUrl = plan.homepageUrl;
|
|
41733
|
+
}
|
|
41734
|
+
if (plan.logo !== void 0) {
|
|
41735
|
+
const fields = slotFields(plan.logo, uploaded.light, "light");
|
|
41736
|
+
body.logoUrl = fields.url;
|
|
41737
|
+
body.logoS3Key = fields.s3Key;
|
|
41738
|
+
}
|
|
41739
|
+
if (plan.darkLogo !== void 0) {
|
|
41740
|
+
const fields = slotFields(plan.darkLogo, uploaded.dark, "dark");
|
|
41741
|
+
body.darkModeLogoUrl = fields.url;
|
|
41742
|
+
body.darkModeLogoS3Key = fields.s3Key;
|
|
41743
|
+
}
|
|
41744
|
+
return body;
|
|
41745
|
+
}
|
|
41746
|
+
function isEmptyOrgUpdate(body) {
|
|
41747
|
+
return Object.keys(body).length === 0;
|
|
41748
|
+
}
|
|
41749
|
+
var MAX_DISPLAY_URL_LENGTH = 120;
|
|
41750
|
+
function formatUrlForDisplay(url2) {
|
|
41751
|
+
if (url2 === null) {
|
|
41752
|
+
return "\u2014";
|
|
41753
|
+
}
|
|
41754
|
+
const clean = sanitizeDisplay(url2);
|
|
41755
|
+
return clean.length > MAX_DISPLAY_URL_LENGTH ? `${clean.slice(0, MAX_DISPLAY_URL_LENGTH - 1)}\u2026` : clean;
|
|
41756
|
+
}
|
|
41757
|
+
function failOrgOp(action, status, body) {
|
|
41758
|
+
const { message } = readErrorFields(body);
|
|
41759
|
+
if (status === UNAUTHORIZED_STATUS2) {
|
|
41760
|
+
exitAuthFailed();
|
|
41761
|
+
}
|
|
41762
|
+
if (status === 403) {
|
|
41763
|
+
console.error(
|
|
41764
|
+
colorError(
|
|
41765
|
+
"Updating organization settings requires an organization admin signed in with a browser session (API keys are rejected)."
|
|
41766
|
+
)
|
|
41767
|
+
);
|
|
41768
|
+
process.exit(1);
|
|
41769
|
+
}
|
|
41770
|
+
if (status === 404) {
|
|
41771
|
+
console.error(colorError(message ?? "Organization not found."));
|
|
41772
|
+
process.exit(1);
|
|
41773
|
+
}
|
|
41774
|
+
if (status === 400 || status === 422) {
|
|
41775
|
+
console.error(colorError(message ?? `Failed to ${action} (status ${status}).`));
|
|
41776
|
+
process.exit(1);
|
|
41777
|
+
}
|
|
41778
|
+
console.error(
|
|
41779
|
+
colorError(`Failed to ${action} (status ${status})${message ? `: ${message}` : "."}`)
|
|
41780
|
+
);
|
|
41781
|
+
process.exit(1);
|
|
41782
|
+
}
|
|
41783
|
+
async function uploadLogo(organizationId, variant, file2) {
|
|
41784
|
+
const slot = await authenticatedRequest(
|
|
41785
|
+
(client) => client.organization.generateLogoUploadUrl({
|
|
41786
|
+
params: { organizationId },
|
|
41787
|
+
body: { variant, contentType: file2.contentType, fileName: basename(file2.path) }
|
|
41788
|
+
})
|
|
41789
|
+
);
|
|
41790
|
+
if (slot.status !== SUCCESS_STATUS) {
|
|
41791
|
+
failOrgOp(`start the ${variant} logo upload`, slot.status, slot.body);
|
|
41792
|
+
}
|
|
41793
|
+
const bytes = readFileSync2(file2.path);
|
|
41794
|
+
const put = await fetch(slot.body.uploadUrl, {
|
|
41795
|
+
method: "PUT",
|
|
41796
|
+
body: new Uint8Array(bytes),
|
|
41797
|
+
headers: { "Content-Type": file2.contentType }
|
|
41798
|
+
});
|
|
41799
|
+
if (!put.ok) {
|
|
41800
|
+
console.error(
|
|
41801
|
+
colorError(
|
|
41802
|
+
`Upload of the ${variant} logo to storage failed (status ${put.status}). The upload link is short-lived; re-run the command.`
|
|
41803
|
+
)
|
|
41804
|
+
);
|
|
41805
|
+
process.exit(1);
|
|
41806
|
+
}
|
|
41807
|
+
return slot.body.s3Key;
|
|
41808
|
+
}
|
|
41809
|
+
function outputOrganization(org, output, derived) {
|
|
41810
|
+
if (output === "json") {
|
|
41811
|
+
console.log(JSON.stringify(org, null, 2));
|
|
41812
|
+
return;
|
|
41813
|
+
}
|
|
41814
|
+
if (output === "tsv") {
|
|
41815
|
+
console.log("name slug homepageUrl logoUrl darkModeLogoUrl");
|
|
41816
|
+
console.log(
|
|
41817
|
+
[
|
|
41818
|
+
tsvSafe(sanitizeDisplay(org.name)),
|
|
41819
|
+
tsvSafe(sanitizeDisplay(org.slug)),
|
|
41820
|
+
tsvSafe(sanitizeDisplay(org.homepageUrl ?? "")),
|
|
41821
|
+
tsvSafe(sanitizeDisplay(org.logoUrl ?? "")),
|
|
41822
|
+
tsvSafe(sanitizeDisplay(org.darkModeLogoUrl ?? ""))
|
|
41823
|
+
].join(" ")
|
|
41824
|
+
);
|
|
41825
|
+
return;
|
|
41826
|
+
}
|
|
41827
|
+
console.log(
|
|
41828
|
+
colorSuccess(`Updated organization ${sanitizeDisplay(org.name)} (${sanitizeDisplay(org.slug)})`)
|
|
41829
|
+
);
|
|
41830
|
+
if (derived) {
|
|
41831
|
+
console.log(" Logos fetched from the homepage.");
|
|
41832
|
+
}
|
|
41833
|
+
console.log(` Homepage: ${formatUrlForDisplay(org.homepageUrl)}`);
|
|
41834
|
+
console.log(` Light logo: ${formatUrlForDisplay(org.logoUrl)}`);
|
|
41835
|
+
console.log(` Dark logo: ${formatUrlForDisplay(org.darkModeLogoUrl)}`);
|
|
41836
|
+
}
|
|
41837
|
+
async function runUpdate2(options) {
|
|
41838
|
+
const output = parseOutput(options.output);
|
|
41839
|
+
const planned = planOrgUpdate(options);
|
|
41840
|
+
if ("error" in planned) {
|
|
41841
|
+
console.error(colorError(planned.error));
|
|
41842
|
+
process.exit(1);
|
|
41843
|
+
}
|
|
41844
|
+
const { plan } = planned;
|
|
41845
|
+
const organizationId = await resolveOrganizationId();
|
|
41846
|
+
const uploaded = {};
|
|
41847
|
+
if (plan.logo?.action === "upload") {
|
|
41848
|
+
uploaded.light = await uploadLogo(organizationId, "light", plan.logo.file);
|
|
41849
|
+
}
|
|
41850
|
+
if (plan.darkLogo?.action === "upload") {
|
|
41851
|
+
uploaded.dark = await uploadLogo(organizationId, "dark", plan.darkLogo.file);
|
|
41852
|
+
}
|
|
41853
|
+
let org = null;
|
|
41854
|
+
const body = buildOrgUpdateBody(plan, uploaded);
|
|
41855
|
+
if (!isEmptyOrgUpdate(body)) {
|
|
41856
|
+
const result = await authenticatedRequest(
|
|
41857
|
+
(client) => client.organization.updateOrganization({ params: { organizationId }, body })
|
|
41858
|
+
);
|
|
41859
|
+
if (result.status !== SUCCESS_STATUS) {
|
|
41860
|
+
failOrgOp("update organization settings", result.status, result.body);
|
|
41861
|
+
}
|
|
41862
|
+
org = result.body;
|
|
41863
|
+
}
|
|
41864
|
+
if (plan.deriveLogosFromHomepage) {
|
|
41865
|
+
const result = await authenticatedRequest(
|
|
41866
|
+
(client) => client.organization.deriveLogosFromHomepage({ params: { organizationId }, body: {} })
|
|
41867
|
+
);
|
|
41868
|
+
if (result.status !== SUCCESS_STATUS) {
|
|
41869
|
+
failOrgOp("fetch logos from the homepage", result.status, result.body);
|
|
41870
|
+
}
|
|
41871
|
+
org = result.body;
|
|
41872
|
+
}
|
|
41873
|
+
if (org === null) {
|
|
41874
|
+
throw new Error("Organization update produced no response; expected at least one API call.");
|
|
41875
|
+
}
|
|
41876
|
+
outputOrganization(org, output, plan.deriveLogosFromHomepage);
|
|
41877
|
+
}
|
|
41878
|
+
function createOrgUpdateCommand() {
|
|
41879
|
+
return new Command("update").description("Update the active organization's homepage URL and logos").option("--homepage <url>", "Homepage URL (https://...)").option("--clear-homepage", "Remove the homepage URL").option("--logo <file-or-url>", "Light-mode logo: an image file to upload, or an http(s) URL").option("--clear-logo", "Remove the light-mode logo").option(
|
|
41880
|
+
"--dark-logo <file-or-url>",
|
|
41881
|
+
"Dark-mode logo: an image file to upload, or an http(s) URL"
|
|
41882
|
+
).option("--clear-dark-logo", "Remove the dark-mode logo").option(
|
|
41883
|
+
"--logos-from-homepage",
|
|
41884
|
+
"Fetch light and dark logos from the homepage (after saving --homepage, if given)"
|
|
41885
|
+
).option("-o, --output <format>", "Output format: table, json, tsv (default: table)", "table").addHelpText(
|
|
41886
|
+
"after",
|
|
41887
|
+
`
|
|
41888
|
+
Requires an organization admin signed in with \`tracecli auth login\`; API keys
|
|
41889
|
+
are rejected. Logo files upload straight to storage (.png, .jpg, .jpeg, .gif,
|
|
41890
|
+
.svg, .webp; 2 MB max). Saving a file or URL for a slot replaces whatever that
|
|
41891
|
+
slot held before.
|
|
41892
|
+
|
|
41893
|
+
Examples:
|
|
41894
|
+
tracecli org update --homepage https://acme.com
|
|
41895
|
+
tracecli org update --homepage https://acme.com --logos-from-homepage
|
|
41896
|
+
tracecli org update --logo ./logo.svg --dark-logo ./logo-dark.svg
|
|
41897
|
+
tracecli org update --logo https://cdn.acme.com/logo.png
|
|
41898
|
+
tracecli org update --clear-dark-logo
|
|
41899
|
+
tracecli org update --homepage https://acme.com --output json`
|
|
41900
|
+
).action(async (options) => {
|
|
41901
|
+
await runUpdate2(options);
|
|
41902
|
+
});
|
|
41903
|
+
}
|
|
41904
|
+
|
|
41591
41905
|
// src/auth/commands.ts
|
|
41592
41906
|
function getClientId() {
|
|
41593
41907
|
const clientId = "client_01KEZ01W5Q3107GTBY51WQ67HT";
|
|
@@ -42360,10 +42674,13 @@ Examples:
|
|
|
42360
42674
|
});
|
|
42361
42675
|
}
|
|
42362
42676
|
function createOrgCommand() {
|
|
42363
|
-
const org = new Command("org").description(
|
|
42677
|
+
const org = new Command("org").description(
|
|
42678
|
+
"Manage active organization context and organization settings"
|
|
42679
|
+
);
|
|
42364
42680
|
org.addCommand(createOrgListCommand());
|
|
42365
42681
|
org.addCommand(createOrgCurrentCommand());
|
|
42366
42682
|
org.addCommand(createOrgSwitchCommand());
|
|
42683
|
+
org.addCommand(createOrgUpdateCommand());
|
|
42367
42684
|
return org;
|
|
42368
42685
|
}
|
|
42369
42686
|
function createAuthCommand() {
|
|
@@ -43428,7 +43745,7 @@ function createInboxCommand() {
|
|
|
43428
43745
|
}
|
|
43429
43746
|
|
|
43430
43747
|
// src/secret-input.ts
|
|
43431
|
-
import { readFileSync as
|
|
43748
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
43432
43749
|
var SecretInputError = class extends Error {
|
|
43433
43750
|
};
|
|
43434
43751
|
function secretSourceCount(opts) {
|
|
@@ -43463,7 +43780,7 @@ async function resolveSecret(label, flagBase, opts, readStdin = defaultReadStdin
|
|
|
43463
43780
|
if (opts.file !== void 0) {
|
|
43464
43781
|
let contents;
|
|
43465
43782
|
try {
|
|
43466
|
-
contents =
|
|
43783
|
+
contents = readFileSync3(opts.file, "utf8");
|
|
43467
43784
|
} catch {
|
|
43468
43785
|
throw new SecretInputError(`Could not read ${label} file: ${opts.file}`);
|
|
43469
43786
|
}
|
|
@@ -44049,7 +44366,7 @@ async function runGet2(credentialId, options) {
|
|
|
44049
44366
|
}
|
|
44050
44367
|
outputCredential(result.body, output);
|
|
44051
44368
|
}
|
|
44052
|
-
async function
|
|
44369
|
+
async function runUpdate3(credentialId, options) {
|
|
44053
44370
|
const output = parseOutput(options.output);
|
|
44054
44371
|
requireEnvId(options.env);
|
|
44055
44372
|
requireCredId(credentialId);
|
|
@@ -44301,7 +44618,7 @@ Examples:
|
|
|
44301
44618
|
tracecli credential update <id> --env <envId> --clear-auth-config`
|
|
44302
44619
|
);
|
|
44303
44620
|
return command.action(async (credentialId, options) => {
|
|
44304
|
-
await
|
|
44621
|
+
await runUpdate3(credentialId, options);
|
|
44305
44622
|
});
|
|
44306
44623
|
}
|
|
44307
44624
|
function createDeleteCommand3() {
|
|
@@ -44790,7 +45107,7 @@ async function runGet3(environmentId, options) {
|
|
|
44790
45107
|
}
|
|
44791
45108
|
outputEnvironment(result.body, output);
|
|
44792
45109
|
}
|
|
44793
|
-
async function
|
|
45110
|
+
async function runUpdate4(environmentId, options) {
|
|
44794
45111
|
const output = parseOutput(options.output);
|
|
44795
45112
|
requireAppId(options.app);
|
|
44796
45113
|
requireEnvId2(environmentId);
|
|
@@ -44901,7 +45218,7 @@ Examples:
|
|
|
44901
45218
|
tracecli environment update <envId> --app <appId> --additional-url https://api.acme.com
|
|
44902
45219
|
tracecli environment update <envId> --app <appId> --production`
|
|
44903
45220
|
).action(async (environmentId, options) => {
|
|
44904
|
-
await
|
|
45221
|
+
await runUpdate4(environmentId, options);
|
|
44905
45222
|
});
|
|
44906
45223
|
}
|
|
44907
45224
|
function createDeleteCommand4() {
|
|
@@ -44920,7 +45237,7 @@ function createEnvironmentCommand() {
|
|
|
44920
45237
|
}
|
|
44921
45238
|
|
|
44922
45239
|
// src/metadata.ts
|
|
44923
|
-
import { readFileSync as
|
|
45240
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
44924
45241
|
import { dirname as dirname2, join as join5 } from "path";
|
|
44925
45242
|
import { fileURLToPath } from "url";
|
|
44926
45243
|
var DEFAULT_PACKAGE_NAME = "securewithtrace";
|
|
@@ -44929,7 +45246,7 @@ function readPackageMetadata() {
|
|
|
44929
45246
|
const currentFilePath = fileURLToPath(import.meta.url);
|
|
44930
45247
|
const packageJsonPath = join5(dirname2(currentFilePath), "..", "package.json");
|
|
44931
45248
|
try {
|
|
44932
|
-
const rawPackageJson =
|
|
45249
|
+
const rawPackageJson = readFileSync4(packageJsonPath, "utf-8");
|
|
44933
45250
|
const parsed = JSON.parse(rawPackageJson);
|
|
44934
45251
|
if (typeof parsed !== "object" || parsed === null) {
|
|
44935
45252
|
return { name: DEFAULT_PACKAGE_NAME, version: DEFAULT_VERSION };
|
|
@@ -44947,10 +45264,10 @@ var CLI_PACKAGE_NAME = packageMetadata.name;
|
|
|
44947
45264
|
var CLI_VERSION = packageMetadata.version;
|
|
44948
45265
|
|
|
44949
45266
|
// src/pentest/commands.ts
|
|
44950
|
-
import { readFileSync as
|
|
45267
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
44951
45268
|
|
|
44952
45269
|
// src/pentest/create-finding.ts
|
|
44953
|
-
import { readFileSync as
|
|
45270
|
+
import { readFileSync as readFileSync5, statSync as statSync2 } from "fs";
|
|
44954
45271
|
|
|
44955
45272
|
// src/pentest/resolve.ts
|
|
44956
45273
|
var UUID_PATTERN2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
@@ -45029,14 +45346,14 @@ function parseCreateFindingJsonFile(raw, sourceLabel) {
|
|
|
45029
45346
|
}
|
|
45030
45347
|
function readUtf8File(path, flag) {
|
|
45031
45348
|
try {
|
|
45032
|
-
const stats =
|
|
45349
|
+
const stats = statSync2(path);
|
|
45033
45350
|
if (!stats.isFile()) {
|
|
45034
45351
|
return { error: `${flag} '${sanitizeDisplay(path)}' is not a regular file.` };
|
|
45035
45352
|
}
|
|
45036
45353
|
} catch {
|
|
45037
45354
|
return { error: `Cannot read ${flag} '${sanitizeDisplay(path)}'.` };
|
|
45038
45355
|
}
|
|
45039
|
-
const text =
|
|
45356
|
+
const text = readFileSync5(path, "utf8");
|
|
45040
45357
|
if (text.trim().length === 0) {
|
|
45041
45358
|
return { error: `${flag} '${sanitizeDisplay(path)}' is empty.` };
|
|
45042
45359
|
}
|
|
@@ -45262,8 +45579,8 @@ function createCreateFindingCommand() {
|
|
|
45262
45579
|
}
|
|
45263
45580
|
|
|
45264
45581
|
// src/pentest/evidence.ts
|
|
45265
|
-
import { readFileSync as
|
|
45266
|
-
import { basename } from "path";
|
|
45582
|
+
import { readFileSync as readFileSync6, statSync as statSync3 } from "fs";
|
|
45583
|
+
import { basename as basename2 } from "path";
|
|
45267
45584
|
var SECTION_LIST = ValidationEvidenceSectionValues.join(", ");
|
|
45268
45585
|
var KIND_LIST = PentestExploitArtifactKindValues.join(", ");
|
|
45269
45586
|
function isSection(value) {
|
|
@@ -45272,7 +45589,7 @@ function isSection(value) {
|
|
|
45272
45589
|
function isKind(value) {
|
|
45273
45590
|
return PentestExploitArtifactKindValues.includes(value);
|
|
45274
45591
|
}
|
|
45275
|
-
function
|
|
45592
|
+
function formatBytes2(bytes) {
|
|
45276
45593
|
if (bytes < 1024) return `${bytes} B`;
|
|
45277
45594
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
|
|
45278
45595
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
@@ -45325,7 +45642,7 @@ async function runAttachEvidence(options) {
|
|
|
45325
45642
|
}
|
|
45326
45643
|
let sizeBytes;
|
|
45327
45644
|
try {
|
|
45328
|
-
const stats =
|
|
45645
|
+
const stats = statSync3(options.file);
|
|
45329
45646
|
if (!stats.isFile()) {
|
|
45330
45647
|
console.error(colorError(`--file '${sanitizeDisplay(options.file)}' is not a regular file.`));
|
|
45331
45648
|
process.exit(1);
|
|
@@ -45342,7 +45659,7 @@ async function runAttachEvidence(options) {
|
|
|
45342
45659
|
if (sizeBytes > MAX_STAFF_EVIDENCE_UPLOAD_BYTES) {
|
|
45343
45660
|
console.error(
|
|
45344
45661
|
colorError(
|
|
45345
|
-
`File is ${
|
|
45662
|
+
`File is ${formatBytes2(sizeBytes)}; the limit for one exhibit is ${formatBytes2(MAX_STAFF_EVIDENCE_UPLOAD_BYTES)}.`
|
|
45346
45663
|
)
|
|
45347
45664
|
);
|
|
45348
45665
|
process.exit(1);
|
|
@@ -45351,7 +45668,7 @@ async function runAttachEvidence(options) {
|
|
|
45351
45668
|
const pentestId = await resolvePentestId(organizationId, options.pentest);
|
|
45352
45669
|
const pentestFindingId = await resolveFindingId(organizationId, pentestId, options.finding);
|
|
45353
45670
|
const params = { organizationId, pentestId, pentestFindingId };
|
|
45354
|
-
const filename =
|
|
45671
|
+
const filename = basename2(options.file);
|
|
45355
45672
|
const slot = await authenticatedRequest(
|
|
45356
45673
|
(client) => client.pentestFinding.createFindingEvidenceUpload({
|
|
45357
45674
|
params,
|
|
@@ -45368,7 +45685,7 @@ async function runAttachEvidence(options) {
|
|
|
45368
45685
|
);
|
|
45369
45686
|
process.exit(1);
|
|
45370
45687
|
}
|
|
45371
|
-
const body =
|
|
45688
|
+
const body = readFileSync6(options.file);
|
|
45372
45689
|
const put = await fetch(slot.body.uploadUrl, { method: "PUT", body: new Uint8Array(body) });
|
|
45373
45690
|
if (!put.ok) {
|
|
45374
45691
|
console.error(
|
|
@@ -45408,7 +45725,7 @@ async function runAttachEvidence(options) {
|
|
|
45408
45725
|
const placement = attached.body.section ? ` \xB7 ${attached.body.section}` : "";
|
|
45409
45726
|
console.log(
|
|
45410
45727
|
colorSuccess(
|
|
45411
|
-
`Attached ${attached.body.kind} \xB7 ${
|
|
45728
|
+
`Attached ${attached.body.kind} \xB7 ${formatBytes2(attached.body.sizeBytes)} \xB7 ${visibility}${placement}`
|
|
45412
45729
|
)
|
|
45413
45730
|
);
|
|
45414
45731
|
console.log(` artifact ${attached.body.artifactId}`);
|
|
@@ -46246,7 +46563,7 @@ function outputPentestUpdate(pt, output) {
|
|
|
46246
46563
|
}
|
|
46247
46564
|
printPentestUpdateDetail(pt);
|
|
46248
46565
|
}
|
|
46249
|
-
async function
|
|
46566
|
+
async function runUpdate5(pentestIdentifier, options) {
|
|
46250
46567
|
const output = parseOutput(options.output);
|
|
46251
46568
|
const description = resolveClearableOption(
|
|
46252
46569
|
options.description,
|
|
@@ -46316,7 +46633,7 @@ Examples:
|
|
|
46316
46633
|
tracecli pentest update PT-3 --clear-concerns --clear-priorities
|
|
46317
46634
|
tracecli pentest update PT-3 --title "Acme Q3" -o json`
|
|
46318
46635
|
).action(async (pentestId, options) => {
|
|
46319
|
-
await
|
|
46636
|
+
await runUpdate5(pentestId, options);
|
|
46320
46637
|
});
|
|
46321
46638
|
}
|
|
46322
46639
|
function failQuestionnaire(status, body, action, notFoundLabel) {
|
|
@@ -46507,7 +46824,7 @@ async function runQuestionnaire(pentestIdentifier, options) {
|
|
|
46507
46824
|
if (filePath !== void 0) {
|
|
46508
46825
|
let raw;
|
|
46509
46826
|
try {
|
|
46510
|
-
raw =
|
|
46827
|
+
raw = readFileSync7(filePath, "utf8");
|
|
46511
46828
|
} catch {
|
|
46512
46829
|
console.error(colorError(`Could not read answers file: ${filePath}`));
|
|
46513
46830
|
process.exit(1);
|
|
@@ -46591,7 +46908,7 @@ function createPentestCommand() {
|
|
|
46591
46908
|
}
|
|
46592
46909
|
|
|
46593
46910
|
// src/update/commands.ts
|
|
46594
|
-
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as
|
|
46911
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "fs";
|
|
46595
46912
|
import { dirname as dirname3, join as join6 } from "path";
|
|
46596
46913
|
import { createInterface as createInterface4 } from "readline/promises";
|
|
46597
46914
|
var UPDATE_CACHE_FILE = "update-check.json";
|
|
@@ -46609,7 +46926,7 @@ function readUpdateCheckCache() {
|
|
|
46609
46926
|
return null;
|
|
46610
46927
|
}
|
|
46611
46928
|
try {
|
|
46612
|
-
const rawCache =
|
|
46929
|
+
const rawCache = readFileSync8(cachePath, "utf-8");
|
|
46613
46930
|
const parsed = JSON.parse(rawCache);
|
|
46614
46931
|
if (typeof parsed !== "object" || parsed === null) {
|
|
46615
46932
|
return null;
|
|
@@ -46973,88 +47290,1043 @@ Examples:
|
|
|
46973
47290
|
});
|
|
46974
47291
|
}
|
|
46975
47292
|
|
|
46976
|
-
// src/
|
|
46977
|
-
|
|
46978
|
-
|
|
46979
|
-
|
|
46980
|
-
|
|
46981
|
-
|
|
46982
|
-
|
|
46983
|
-
|
|
46984
|
-
|
|
46985
|
-
|
|
46986
|
-
|
|
46987
|
-
|
|
46988
|
-
|
|
46989
|
-
|
|
46990
|
-
|
|
46991
|
-
|
|
46992
|
-
|
|
47293
|
+
// src/upload/prepare.ts
|
|
47294
|
+
import { createHash } from "crypto";
|
|
47295
|
+
import { createReadStream, statSync as statSync4 } from "fs";
|
|
47296
|
+
import { open } from "fs/promises";
|
|
47297
|
+
import { basename as basename3 } from "path";
|
|
47298
|
+
var CodeUploadPrepareError = class extends Error {
|
|
47299
|
+
constructor(message) {
|
|
47300
|
+
super(message);
|
|
47301
|
+
this.name = "CodeUploadPrepareError";
|
|
47302
|
+
}
|
|
47303
|
+
};
|
|
47304
|
+
var KIND_BY_INPUT = {
|
|
47305
|
+
source: "SOURCE_ARCHIVE",
|
|
47306
|
+
code: "SOURCE_ARCHIVE",
|
|
47307
|
+
"source-archive": "SOURCE_ARCHIVE",
|
|
47308
|
+
source_archive: "SOURCE_ARCHIVE",
|
|
47309
|
+
SOURCE_ARCHIVE: "SOURCE_ARCHIVE",
|
|
47310
|
+
apk: "ANDROID_APK",
|
|
47311
|
+
android: "ANDROID_APK",
|
|
47312
|
+
"android-apk": "ANDROID_APK",
|
|
47313
|
+
android_apk: "ANDROID_APK",
|
|
47314
|
+
ANDROID_APK: "ANDROID_APK",
|
|
47315
|
+
ipa: "IOS_IPA",
|
|
47316
|
+
ios: "IOS_IPA",
|
|
47317
|
+
"ios-ipa": "IOS_IPA",
|
|
47318
|
+
ios_ipa: "IOS_IPA",
|
|
47319
|
+
IOS_IPA: "IOS_IPA"
|
|
47320
|
+
};
|
|
47321
|
+
function normalizeArtifactKind(input) {
|
|
47322
|
+
const trimmed = input.trim();
|
|
47323
|
+
return KIND_BY_INPUT[trimmed] ?? KIND_BY_INPUT[trimmed.toLowerCase()] ?? null;
|
|
47324
|
+
}
|
|
47325
|
+
function inferArtifactKindFromFilename(filename) {
|
|
47326
|
+
const lower = filename.toLowerCase();
|
|
47327
|
+
if (lower.endsWith(".zip")) return "SOURCE_ARCHIVE";
|
|
47328
|
+
if (lower.endsWith(".apk")) return "ANDROID_APK";
|
|
47329
|
+
if (lower.endsWith(".ipa")) return "IOS_IPA";
|
|
47330
|
+
return null;
|
|
47331
|
+
}
|
|
47332
|
+
function requiredExtensionForKind(kind) {
|
|
47333
|
+
if (kind === "ANDROID_APK") return ".apk";
|
|
47334
|
+
if (kind === "IOS_IPA") return ".ipa";
|
|
47335
|
+
return ".zip";
|
|
47336
|
+
}
|
|
47337
|
+
function defaultDisplayName(originalFilename) {
|
|
47338
|
+
const base = originalFilename.replace(/\.(zip|apk|ipa)$/i, "").trim();
|
|
47339
|
+
return base.slice(0, 200) || "source";
|
|
47340
|
+
}
|
|
47341
|
+
function formatArchiveBytes(bytes) {
|
|
47342
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
47343
|
+
if (bytes < 1024 * 1024) {
|
|
47344
|
+
const kb = bytes / 1024;
|
|
47345
|
+
return `${kb.toFixed(Number.isInteger(kb) ? 0 : 1)} KB`;
|
|
46993
47346
|
}
|
|
46994
|
-
|
|
46995
|
-
|
|
46996
|
-
|
|
46997
|
-
if (value.length === 0) {
|
|
46998
|
-
return { error: `Filter value cannot be empty in expression '${expression}'.` };
|
|
47347
|
+
if (bytes < 1024 * 1024 * 1024) {
|
|
47348
|
+
const mb = bytes / (1024 * 1024);
|
|
47349
|
+
return `${mb.toFixed(Number.isInteger(mb) ? 0 : 1)} MB`;
|
|
46999
47350
|
}
|
|
47000
|
-
|
|
47001
|
-
|
|
47002
|
-
|
|
47003
|
-
|
|
47351
|
+
const gb = bytes / (1024 * 1024 * 1024);
|
|
47352
|
+
return `${gb.toFixed(Number.isInteger(gb) ? 0 : 1)} GB`;
|
|
47353
|
+
}
|
|
47354
|
+
function archiveTooLargeMessage() {
|
|
47355
|
+
return `The file must be ${formatArchiveBytes(CODE_UPLOAD_MAX_BYTES)} or smaller.`;
|
|
47356
|
+
}
|
|
47357
|
+
async function hashFileSha256(filePath) {
|
|
47358
|
+
const hash2 = createHash("sha256");
|
|
47359
|
+
const stream = createReadStream(filePath);
|
|
47360
|
+
for await (const chunk of stream) {
|
|
47361
|
+
hash2.update(chunk);
|
|
47004
47362
|
}
|
|
47005
|
-
return
|
|
47363
|
+
return hash2.digest("hex");
|
|
47006
47364
|
}
|
|
47007
|
-
async function
|
|
47008
|
-
const
|
|
47009
|
-
|
|
47010
|
-
|
|
47011
|
-
|
|
47012
|
-
|
|
47013
|
-
if (result.status !== 200) {
|
|
47014
|
-
console.error(`Failed to list repositories (status ${result.status}).`);
|
|
47015
|
-
process.exit(1);
|
|
47365
|
+
async function readFileSlice(filePath, start, endExclusive) {
|
|
47366
|
+
const length = endExclusive - start;
|
|
47367
|
+
if (!Number.isSafeInteger(start) || !Number.isSafeInteger(length) || start < 0 || length < 1) {
|
|
47368
|
+
throw new Error(
|
|
47369
|
+
`Invalid file slice: start=${String(start)} endExclusive=${String(endExclusive)}.`
|
|
47370
|
+
);
|
|
47016
47371
|
}
|
|
47017
|
-
const
|
|
47018
|
-
const
|
|
47019
|
-
|
|
47020
|
-
const
|
|
47021
|
-
|
|
47022
|
-
|
|
47023
|
-
|
|
47024
|
-
|
|
47025
|
-
return false;
|
|
47026
|
-
});
|
|
47027
|
-
if (match) {
|
|
47028
|
-
return match.repositoryId;
|
|
47372
|
+
const buffer = Buffer.alloc(length);
|
|
47373
|
+
const handle = await open(filePath, "r");
|
|
47374
|
+
try {
|
|
47375
|
+
const { bytesRead } = await handle.read(buffer, 0, length, start);
|
|
47376
|
+
if (bytesRead !== length) {
|
|
47377
|
+
throw new Error(
|
|
47378
|
+
`Expected to read ${String(length)} bytes at offset ${String(start)}, got ${String(bytesRead)}.`
|
|
47379
|
+
);
|
|
47029
47380
|
}
|
|
47381
|
+
return buffer;
|
|
47382
|
+
} finally {
|
|
47383
|
+
await handle.close();
|
|
47030
47384
|
}
|
|
47031
|
-
|
|
47032
|
-
|
|
47033
|
-
|
|
47034
|
-
|
|
47035
|
-
|
|
47036
|
-
if (byName.length === 1 && byName[0]) {
|
|
47037
|
-
return byName[0].repositoryId;
|
|
47038
|
-
}
|
|
47039
|
-
if (byName.length > 1) {
|
|
47040
|
-
console.error(
|
|
47041
|
-
`Ambiguous repository name '${repoIdentifier}'. Multiple matches found:
|
|
47042
|
-
${byName.map((r2) => ` - ${r2.repositoryUrl}`).join("\n")}
|
|
47043
|
-
Please use the full owner/repo format.`
|
|
47385
|
+
}
|
|
47386
|
+
function assertPreparedMatchesUpload(upload, prepared) {
|
|
47387
|
+
if (upload.sizeBytes !== prepared.sizeBytes || upload.archiveSha256 !== prepared.archiveSha256) {
|
|
47388
|
+
throw new CodeUploadPrepareError(
|
|
47389
|
+
"The file does not match the interrupted upload (size or SHA-256 differs)."
|
|
47044
47390
|
);
|
|
47045
|
-
process.exit(1);
|
|
47046
47391
|
}
|
|
47047
|
-
console.error(
|
|
47048
|
-
`Repository '${repoIdentifier}' not found.
|
|
47049
|
-
Run \`tracecli repo list\` to see available repositories.`
|
|
47050
|
-
);
|
|
47051
|
-
process.exit(1);
|
|
47052
47392
|
}
|
|
47053
|
-
function
|
|
47054
|
-
|
|
47055
|
-
|
|
47056
|
-
|
|
47057
|
-
|
|
47393
|
+
async function prepareUploadFile(filePath, options = {}) {
|
|
47394
|
+
let sizeBytes;
|
|
47395
|
+
try {
|
|
47396
|
+
const stats = statSync4(filePath);
|
|
47397
|
+
if (!stats.isFile()) {
|
|
47398
|
+
throw new CodeUploadPrepareError("Path is not a regular file. Pass a .zip, .apk, or .ipa.");
|
|
47399
|
+
}
|
|
47400
|
+
sizeBytes = stats.size;
|
|
47401
|
+
} catch (error48) {
|
|
47402
|
+
if (error48 instanceof CodeUploadPrepareError) throw error48;
|
|
47403
|
+
throw new CodeUploadPrepareError("Cannot read that file.");
|
|
47404
|
+
}
|
|
47405
|
+
if (!Number.isSafeInteger(sizeBytes) || sizeBytes < 1) {
|
|
47406
|
+
throw new CodeUploadPrepareError("The file is empty.");
|
|
47407
|
+
}
|
|
47408
|
+
if (sizeBytes > CODE_UPLOAD_MAX_BYTES) {
|
|
47409
|
+
throw new CodeUploadPrepareError(archiveTooLargeMessage());
|
|
47410
|
+
}
|
|
47411
|
+
const originalFilename = basename3(filePath);
|
|
47412
|
+
const inferred = inferArtifactKindFromFilename(originalFilename);
|
|
47413
|
+
const artifactKind = options.artifactKind ?? inferred;
|
|
47414
|
+
if (artifactKind == null) {
|
|
47415
|
+
throw new CodeUploadPrepareError("Pass a .zip source archive, an .apk, or an .ipa.");
|
|
47416
|
+
}
|
|
47417
|
+
const expected = requiredExtensionForKind(artifactKind);
|
|
47418
|
+
if (!originalFilename.toLowerCase().endsWith(expected)) {
|
|
47419
|
+
const label = isApplicationBuildArtifactKind(artifactKind) ? artifactKind === "ANDROID_APK" ? "Android APK" : "iOS package" : "source archive";
|
|
47420
|
+
throw new CodeUploadPrepareError(`A ${label} must end in ${expected}.`);
|
|
47421
|
+
}
|
|
47422
|
+
const displayName = (options.displayName ?? defaultDisplayName(originalFilename)).trim();
|
|
47423
|
+
if (displayName.length < 1 || displayName.length > 200) {
|
|
47424
|
+
throw new CodeUploadPrepareError("Display name must be 1 to 200 characters.");
|
|
47425
|
+
}
|
|
47426
|
+
return {
|
|
47427
|
+
path: filePath,
|
|
47428
|
+
displayName,
|
|
47429
|
+
originalFilename,
|
|
47430
|
+
sizeBytes,
|
|
47431
|
+
archiveSha256: await hashFileSha256(filePath),
|
|
47432
|
+
artifactKind
|
|
47433
|
+
};
|
|
47434
|
+
}
|
|
47435
|
+
|
|
47436
|
+
// src/upload/multipart.ts
|
|
47437
|
+
var MAX_CONCURRENT_UPLOADS = 3;
|
|
47438
|
+
var MAX_PART_ATTEMPTS = 3;
|
|
47439
|
+
var MAX_URL_REFRESHES = 12;
|
|
47440
|
+
async function uploadCodeArchiveParts(filePath, totalBytes, partSizeBytes, parts, options = {}) {
|
|
47441
|
+
assertUploadPlan(totalBytes, partSizeBytes, parts);
|
|
47442
|
+
const fetchPart = options.fetch ?? globalThis.fetch;
|
|
47443
|
+
const wait = options.wait ?? defaultWait;
|
|
47444
|
+
let uploadedBytes = options.initialUploadedBytes ?? 0;
|
|
47445
|
+
if (!Number.isSafeInteger(uploadedBytes) || uploadedBytes < 0 || uploadedBytes > totalBytes) {
|
|
47446
|
+
throw new Error("Invalid multipart progress: uploaded byte count is outside the archive.");
|
|
47447
|
+
}
|
|
47448
|
+
options.onProgress?.(progress(uploadedBytes, totalBytes));
|
|
47449
|
+
if (parts.length === 0) return;
|
|
47450
|
+
const urls = new Map(parts.map((part) => [part.partNumber, part.uploadUrl]));
|
|
47451
|
+
const refreshUrls = createUrlRefresher(urls, options.refreshParts);
|
|
47452
|
+
let cursor3 = 0;
|
|
47453
|
+
const workerCount = Math.min(MAX_CONCURRENT_UPLOADS, parts.length);
|
|
47454
|
+
await Promise.all(
|
|
47455
|
+
Array.from({ length: workerCount }, async () => {
|
|
47456
|
+
while (cursor3 < parts.length) {
|
|
47457
|
+
const part = parts[cursor3];
|
|
47458
|
+
cursor3 += 1;
|
|
47459
|
+
if (!part) return;
|
|
47460
|
+
const start = (part.partNumber - 1) * partSizeBytes;
|
|
47461
|
+
const end = Math.min(start + partSizeBytes, totalBytes);
|
|
47462
|
+
const body = await readFileSlice(filePath, start, end);
|
|
47463
|
+
await uploadPartWithRetry(part.partNumber, urls, body, fetchPart, wait, refreshUrls);
|
|
47464
|
+
uploadedBytes += body.byteLength;
|
|
47465
|
+
options.onProgress?.(progress(uploadedBytes, totalBytes));
|
|
47466
|
+
}
|
|
47467
|
+
})
|
|
47468
|
+
);
|
|
47469
|
+
}
|
|
47470
|
+
function createUrlRefresher(urls, refreshParts) {
|
|
47471
|
+
if (!refreshParts) return async () => false;
|
|
47472
|
+
let inflight = null;
|
|
47473
|
+
let rounds = 0;
|
|
47474
|
+
return async () => {
|
|
47475
|
+
if (rounds >= MAX_URL_REFRESHES) return false;
|
|
47476
|
+
if (!inflight) {
|
|
47477
|
+
rounds += 1;
|
|
47478
|
+
inflight = (async () => {
|
|
47479
|
+
const fresh = await refreshParts();
|
|
47480
|
+
for (const part of fresh) {
|
|
47481
|
+
assertPresignedUploadUrl(part.uploadUrl);
|
|
47482
|
+
urls.set(part.partNumber, part.uploadUrl);
|
|
47483
|
+
}
|
|
47484
|
+
})().finally(() => {
|
|
47485
|
+
inflight = null;
|
|
47486
|
+
});
|
|
47487
|
+
}
|
|
47488
|
+
await inflight;
|
|
47489
|
+
return true;
|
|
47490
|
+
};
|
|
47491
|
+
}
|
|
47492
|
+
function assertUploadPlan(totalBytes, partSizeBytes, parts) {
|
|
47493
|
+
if (!Number.isSafeInteger(totalBytes) || totalBytes < 1) {
|
|
47494
|
+
throw new Error("Invalid multipart upload: archive size must be a positive integer.");
|
|
47495
|
+
}
|
|
47496
|
+
if (!Number.isSafeInteger(partSizeBytes) || partSizeBytes < 1) {
|
|
47497
|
+
throw new Error("Invalid multipart upload: part size must be a positive integer.");
|
|
47498
|
+
}
|
|
47499
|
+
const totalParts = Math.ceil(totalBytes / partSizeBytes);
|
|
47500
|
+
const seen = /* @__PURE__ */ new Set();
|
|
47501
|
+
for (const part of parts) {
|
|
47502
|
+
if (!Number.isSafeInteger(part.partNumber) || part.partNumber < 1 || part.partNumber > totalParts || seen.has(part.partNumber)) {
|
|
47503
|
+
throw new Error("Invalid multipart upload: part numbers must be unique and in range.");
|
|
47504
|
+
}
|
|
47505
|
+
assertPresignedUploadUrl(part.uploadUrl);
|
|
47506
|
+
seen.add(part.partNumber);
|
|
47507
|
+
}
|
|
47508
|
+
}
|
|
47509
|
+
async function uploadPartWithRetry(partNumber, urls, body, fetchPart, wait, refreshUrls) {
|
|
47510
|
+
let lastStatus = null;
|
|
47511
|
+
for (let attempt = 1; attempt <= MAX_PART_ATTEMPTS; attempt += 1) {
|
|
47512
|
+
const uploadUrl = urls.get(partNumber);
|
|
47513
|
+
if (!uploadUrl) {
|
|
47514
|
+
throw new Error(`Source upload part ${partNumber.toString()} has no upload URL.`);
|
|
47515
|
+
}
|
|
47516
|
+
try {
|
|
47517
|
+
const response = await fetchPart(uploadUrl, {
|
|
47518
|
+
method: "PUT",
|
|
47519
|
+
body: new Uint8Array(body),
|
|
47520
|
+
redirect: "error"
|
|
47521
|
+
});
|
|
47522
|
+
if (response.ok) return;
|
|
47523
|
+
lastStatus = response.status;
|
|
47524
|
+
if (response.status === 403 && await refreshUrls()) {
|
|
47525
|
+
attempt -= 1;
|
|
47526
|
+
continue;
|
|
47527
|
+
}
|
|
47528
|
+
if (response.status >= 400 && response.status < 500 && response.status !== 408) break;
|
|
47529
|
+
} catch {
|
|
47530
|
+
lastStatus = null;
|
|
47531
|
+
}
|
|
47532
|
+
if (attempt < MAX_PART_ATTEMPTS) {
|
|
47533
|
+
await wait(250 * 2 ** (attempt - 1));
|
|
47534
|
+
}
|
|
47535
|
+
}
|
|
47536
|
+
const statusDetail = lastStatus === null ? "network error" : `status ${lastStatus.toString()}`;
|
|
47537
|
+
throw new Error(
|
|
47538
|
+
`Source upload part ${partNumber.toString()} failed after retries (${statusDetail}).`
|
|
47539
|
+
);
|
|
47540
|
+
}
|
|
47541
|
+
function assertPresignedUploadUrl(value) {
|
|
47542
|
+
let url2;
|
|
47543
|
+
try {
|
|
47544
|
+
url2 = new URL(value);
|
|
47545
|
+
} catch {
|
|
47546
|
+
throw new Error("Invalid multipart upload: the API returned an invalid part URL.");
|
|
47547
|
+
}
|
|
47548
|
+
const localHttp = url2.protocol === "http:" && (url2.hostname === "localhost" || url2.hostname === "127.0.0.1" || url2.hostname === "host.docker.internal");
|
|
47549
|
+
if (url2.protocol !== "https:" && !localHttp) {
|
|
47550
|
+
throw new Error("Invalid multipart upload: part URLs must use HTTPS.");
|
|
47551
|
+
}
|
|
47552
|
+
}
|
|
47553
|
+
function progress(uploadedBytes, totalBytes) {
|
|
47554
|
+
return {
|
|
47555
|
+
uploadedBytes,
|
|
47556
|
+
totalBytes,
|
|
47557
|
+
fraction: totalBytes === 0 ? 0 : Math.min(1, uploadedBytes / totalBytes)
|
|
47558
|
+
};
|
|
47559
|
+
}
|
|
47560
|
+
function defaultWait(milliseconds) {
|
|
47561
|
+
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
47562
|
+
}
|
|
47563
|
+
|
|
47564
|
+
// src/upload/commands.ts
|
|
47565
|
+
var TERMINAL_STATUSES = /* @__PURE__ */ new Set(["READY", "REJECTED", "FAILED", "DELETING"]);
|
|
47566
|
+
var DEFAULT_RUNTIME = {
|
|
47567
|
+
wait: (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)),
|
|
47568
|
+
pollIntervalMs: 3e3,
|
|
47569
|
+
fetch: (input, init) => globalThis.fetch(input, init)
|
|
47570
|
+
};
|
|
47571
|
+
function resolveRuntime(runtime) {
|
|
47572
|
+
return {
|
|
47573
|
+
wait: runtime.wait ?? DEFAULT_RUNTIME.wait,
|
|
47574
|
+
pollIntervalMs: runtime.pollIntervalMs ?? DEFAULT_RUNTIME.pollIntervalMs,
|
|
47575
|
+
fetch: runtime.fetch ?? DEFAULT_RUNTIME.fetch
|
|
47576
|
+
};
|
|
47577
|
+
}
|
|
47578
|
+
function isTerminalCodeUploadStatus(status) {
|
|
47579
|
+
return TERMINAL_STATUSES.has(status);
|
|
47580
|
+
}
|
|
47581
|
+
function requireUuid(value, label) {
|
|
47582
|
+
if (!isUuid(value)) {
|
|
47583
|
+
console.error(colorError(`Invalid ${label} '${sanitizeDisplay(value)}'. Expected a UUID.`));
|
|
47584
|
+
process.exit(1);
|
|
47585
|
+
}
|
|
47586
|
+
return value;
|
|
47587
|
+
}
|
|
47588
|
+
function parseKindOption(value) {
|
|
47589
|
+
if (value === void 0) return void 0;
|
|
47590
|
+
const kind = normalizeArtifactKind(value);
|
|
47591
|
+
if (kind == null) {
|
|
47592
|
+
console.error(
|
|
47593
|
+
colorError(`Invalid --kind '${sanitizeDisplay(value)}'. Use source (code ZIP), apk, or ipa.`)
|
|
47594
|
+
);
|
|
47595
|
+
process.exit(1);
|
|
47596
|
+
}
|
|
47597
|
+
return kind;
|
|
47598
|
+
}
|
|
47599
|
+
function failPrepare(error48) {
|
|
47600
|
+
const message = error48 instanceof CodeUploadPrepareError ? error48.message : error48 instanceof Error ? error48.message : "Failed to prepare the file.";
|
|
47601
|
+
console.error(colorError(message));
|
|
47602
|
+
process.exit(1);
|
|
47603
|
+
}
|
|
47604
|
+
function failWriteOp5(action, status, body) {
|
|
47605
|
+
const { message, errorCode } = readErrorFields(body);
|
|
47606
|
+
if (status === UNAUTHORIZED_STATUS2) {
|
|
47607
|
+
exitAuthFailed();
|
|
47608
|
+
}
|
|
47609
|
+
if (status === 403) {
|
|
47610
|
+
console.error(
|
|
47611
|
+
colorError(
|
|
47612
|
+
`Only an organization admin (or Trace staff) on a pentest-enabled organization can ${action} source uploads.`
|
|
47613
|
+
)
|
|
47614
|
+
);
|
|
47615
|
+
process.exit(1);
|
|
47616
|
+
}
|
|
47617
|
+
if (status === 404) {
|
|
47618
|
+
console.error(colorError(message ?? "No code upload with that id."));
|
|
47619
|
+
process.exit(1);
|
|
47620
|
+
}
|
|
47621
|
+
if (status === 400) {
|
|
47622
|
+
console.error(colorError(message ?? "Invalid request (status 400)."));
|
|
47623
|
+
process.exit(1);
|
|
47624
|
+
}
|
|
47625
|
+
if (status === 409) {
|
|
47626
|
+
if (errorCode === "CODE_UPLOAD_NOT_READY") {
|
|
47627
|
+
console.error(
|
|
47628
|
+
colorError(
|
|
47629
|
+
message ?? "The upload must be ready and pass malware scanning before it can be linked. Run `tracecli upload wait <id>`."
|
|
47630
|
+
)
|
|
47631
|
+
);
|
|
47632
|
+
} else if (errorCode === "CODE_UPLOAD_NOT_SOURCE") {
|
|
47633
|
+
console.error(
|
|
47634
|
+
colorError(message ?? "APK and IPA uploads cannot be linked as source repositories.")
|
|
47635
|
+
);
|
|
47636
|
+
} else {
|
|
47637
|
+
console.error(colorError(message ?? "Conflict (status 409)."));
|
|
47638
|
+
}
|
|
47639
|
+
process.exit(1);
|
|
47640
|
+
}
|
|
47641
|
+
if (status === 503) {
|
|
47642
|
+
console.error(
|
|
47643
|
+
colorError(message ?? "Source archive uploads are not configured on this server.")
|
|
47644
|
+
);
|
|
47645
|
+
process.exit(1);
|
|
47646
|
+
}
|
|
47647
|
+
console.error(
|
|
47648
|
+
colorError(`Failed to ${action} upload (status ${status})${message ? `: ${message}` : "."}`)
|
|
47649
|
+
);
|
|
47650
|
+
process.exit(1);
|
|
47651
|
+
}
|
|
47652
|
+
function failReadOp5(action, status, body) {
|
|
47653
|
+
const { message } = readErrorFields(body);
|
|
47654
|
+
if (status === UNAUTHORIZED_STATUS2) {
|
|
47655
|
+
exitAuthFailed();
|
|
47656
|
+
}
|
|
47657
|
+
if (status === 403) {
|
|
47658
|
+
console.error(
|
|
47659
|
+
colorError(
|
|
47660
|
+
"Only an organization admin (or Trace staff) on a pentest-enabled organization can view source uploads."
|
|
47661
|
+
)
|
|
47662
|
+
);
|
|
47663
|
+
process.exit(1);
|
|
47664
|
+
}
|
|
47665
|
+
if (status === 404) {
|
|
47666
|
+
console.error(colorError(message ?? "No code upload with that id."));
|
|
47667
|
+
process.exit(1);
|
|
47668
|
+
}
|
|
47669
|
+
if (status === 503) {
|
|
47670
|
+
console.error(
|
|
47671
|
+
colorError(message ?? "Source archive uploads are not configured on this server.")
|
|
47672
|
+
);
|
|
47673
|
+
process.exit(1);
|
|
47674
|
+
}
|
|
47675
|
+
console.error(
|
|
47676
|
+
colorError(`Failed to ${action} (status ${status})${message ? `: ${message}` : "."}`)
|
|
47677
|
+
);
|
|
47678
|
+
process.exit(1);
|
|
47679
|
+
}
|
|
47680
|
+
function resumeHint(codeUploadId, filePath) {
|
|
47681
|
+
return `Resume with: tracecli upload resume ${codeUploadId} ${filePath}`;
|
|
47682
|
+
}
|
|
47683
|
+
function buildUploadsTable(uploads) {
|
|
47684
|
+
const cells = uploads.map((upload) => ({
|
|
47685
|
+
name: sanitizeDisplay(upload.displayName),
|
|
47686
|
+
kind: formatStatus(upload.artifactKind),
|
|
47687
|
+
status: formatStatus(upload.status),
|
|
47688
|
+
size: formatArchiveBytes(upload.sizeBytes),
|
|
47689
|
+
id: upload.codeUploadId
|
|
47690
|
+
}));
|
|
47691
|
+
const nameWidth = Math.max(4, ...cells.map((c59) => c59.name.length));
|
|
47692
|
+
const kindWidth = Math.max(4, ...cells.map((c59) => c59.kind.length));
|
|
47693
|
+
const statusWidth = Math.max(6, ...cells.map((c59) => c59.status.length));
|
|
47694
|
+
const sizeWidth = Math.max(4, ...cells.map((c59) => c59.size.length));
|
|
47695
|
+
const header = `${"NAME".padEnd(nameWidth)} ${"KIND".padEnd(kindWidth)} ${"STATUS".padEnd(statusWidth)} ${"SIZE".padEnd(sizeWidth)} ID`;
|
|
47696
|
+
const rows = cells.map(
|
|
47697
|
+
(c59) => `${c59.name.padEnd(nameWidth)} ${c59.kind.padEnd(kindWidth)} ${c59.status.padEnd(statusWidth)} ${c59.size.padEnd(sizeWidth)} ${c59.id}`
|
|
47698
|
+
);
|
|
47699
|
+
return { header, rows };
|
|
47700
|
+
}
|
|
47701
|
+
function renderUploadDetail(upload) {
|
|
47702
|
+
const pad = 18;
|
|
47703
|
+
const linked = upload.linkedApplications.length === 0 ? "\u2014" : upload.linkedApplications.map((app) => `${sanitizeDisplay(app.name)} (${app.applicationId})`).join(", ");
|
|
47704
|
+
return [
|
|
47705
|
+
`${"Name:".padEnd(pad)} ${sanitizeDisplay(upload.displayName)}`,
|
|
47706
|
+
`${"Filename:".padEnd(pad)} ${sanitizeDisplay(upload.originalFilename)}`,
|
|
47707
|
+
`${"Kind:".padEnd(pad)} ${formatStatus(upload.artifactKind)}`,
|
|
47708
|
+
`${"Status:".padEnd(pad)} ${formatStatus(upload.status)}`,
|
|
47709
|
+
`${"Size:".padEnd(pad)} ${formatArchiveBytes(upload.sizeBytes)}`,
|
|
47710
|
+
`${"SHA-256:".padEnd(pad)} ${upload.archiveSha256}`,
|
|
47711
|
+
`${"Upload ID:".padEnd(pad)} ${upload.codeUploadId}`,
|
|
47712
|
+
`${"Repository ID:".padEnd(pad)} ${upload.repositoryId}`,
|
|
47713
|
+
`${"Malware:".padEnd(pad)} ${upload.malwareScanResult ? formatStatus(upload.malwareScanResult) : "\u2014"}`,
|
|
47714
|
+
`${"Rejection:".padEnd(pad)} ${upload.rejectionReason ? formatStatus(upload.rejectionReason) : "\u2014"}`,
|
|
47715
|
+
`${"Linked apps:".padEnd(pad)} ${linked}`,
|
|
47716
|
+
`${"Created:".padEnd(pad)} ${formatDate(upload.createdAt)}`,
|
|
47717
|
+
`${"Ready:".padEnd(pad)} ${formatDate(upload.readyAt)}`
|
|
47718
|
+
].join("\n");
|
|
47719
|
+
}
|
|
47720
|
+
function printUploadsTsv(uploads) {
|
|
47721
|
+
console.log(
|
|
47722
|
+
[
|
|
47723
|
+
"codeUploadId",
|
|
47724
|
+
"repositoryId",
|
|
47725
|
+
"displayName",
|
|
47726
|
+
"originalFilename",
|
|
47727
|
+
"artifactKind",
|
|
47728
|
+
"status",
|
|
47729
|
+
"sizeBytes",
|
|
47730
|
+
"archiveSha256",
|
|
47731
|
+
"malwareScanResult",
|
|
47732
|
+
"rejectionReason",
|
|
47733
|
+
"applicationId",
|
|
47734
|
+
"createdAt",
|
|
47735
|
+
"readyAt"
|
|
47736
|
+
].join(" ")
|
|
47737
|
+
);
|
|
47738
|
+
for (const upload of uploads) {
|
|
47739
|
+
console.log(
|
|
47740
|
+
[
|
|
47741
|
+
tsvSafe(upload.codeUploadId),
|
|
47742
|
+
tsvSafe(upload.repositoryId),
|
|
47743
|
+
tsvSafe(sanitizeDisplay(upload.displayName)),
|
|
47744
|
+
tsvSafe(sanitizeDisplay(upload.originalFilename)),
|
|
47745
|
+
tsvSafe(upload.artifactKind),
|
|
47746
|
+
tsvSafe(upload.status),
|
|
47747
|
+
String(upload.sizeBytes),
|
|
47748
|
+
tsvSafe(upload.archiveSha256),
|
|
47749
|
+
tsvSafe(upload.malwareScanResult ?? ""),
|
|
47750
|
+
tsvSafe(upload.rejectionReason ?? ""),
|
|
47751
|
+
tsvSafe(upload.applicationId ?? ""),
|
|
47752
|
+
tsvSafe(toIso(upload.createdAt)),
|
|
47753
|
+
tsvSafe(upload.readyAt ? toIso(upload.readyAt) : "")
|
|
47754
|
+
].join(" ")
|
|
47755
|
+
);
|
|
47756
|
+
}
|
|
47757
|
+
}
|
|
47758
|
+
function outputUpload(upload, output) {
|
|
47759
|
+
if (output === "json") {
|
|
47760
|
+
console.log(JSON.stringify(upload, null, 2));
|
|
47761
|
+
return;
|
|
47762
|
+
}
|
|
47763
|
+
if (output === "tsv") {
|
|
47764
|
+
printUploadsTsv([upload]);
|
|
47765
|
+
return;
|
|
47766
|
+
}
|
|
47767
|
+
console.log(renderUploadDetail(upload));
|
|
47768
|
+
}
|
|
47769
|
+
function writeProgress(progress2) {
|
|
47770
|
+
if (!process.stderr.isTTY) return;
|
|
47771
|
+
const pct = Math.floor(progress2.fraction * 100);
|
|
47772
|
+
process.stderr.write(
|
|
47773
|
+
`\rUploading ${pct}% (${formatArchiveBytes(progress2.uploadedBytes)} / ${formatArchiveBytes(progress2.totalBytes)})`
|
|
47774
|
+
);
|
|
47775
|
+
}
|
|
47776
|
+
function finishProgress() {
|
|
47777
|
+
if (process.stderr.isTTY) process.stderr.write("\n");
|
|
47778
|
+
}
|
|
47779
|
+
async function listUploads(organizationId, query = {}) {
|
|
47780
|
+
const result = await authenticatedRequest(
|
|
47781
|
+
(client) => client.codeUpload.listCodeUploads({
|
|
47782
|
+
params: { organizationId },
|
|
47783
|
+
query
|
|
47784
|
+
})
|
|
47785
|
+
);
|
|
47786
|
+
if (result.status !== SUCCESS_STATUS) {
|
|
47787
|
+
failReadOp5("list uploads", result.status, result.body);
|
|
47788
|
+
}
|
|
47789
|
+
return result.body.uploads;
|
|
47790
|
+
}
|
|
47791
|
+
async function requireUpload(organizationId, codeUploadId, query = {}) {
|
|
47792
|
+
const uploads = await listUploads(organizationId, query);
|
|
47793
|
+
const match = uploads.find((upload) => upload.codeUploadId === codeUploadId);
|
|
47794
|
+
if (!match) {
|
|
47795
|
+
console.error(colorError(`No code upload '${codeUploadId}'.`));
|
|
47796
|
+
process.exit(1);
|
|
47797
|
+
}
|
|
47798
|
+
return match;
|
|
47799
|
+
}
|
|
47800
|
+
async function refreshRemainingParts(organizationId, codeUploadId) {
|
|
47801
|
+
const result = await authenticatedRequest(
|
|
47802
|
+
(client) => client.codeUpload.resumeCodeUpload({
|
|
47803
|
+
params: { organizationId, codeUploadId },
|
|
47804
|
+
body: {}
|
|
47805
|
+
})
|
|
47806
|
+
);
|
|
47807
|
+
if (result.status !== SUCCESS_STATUS) {
|
|
47808
|
+
failWriteOp5("resume", result.status, result.body);
|
|
47809
|
+
}
|
|
47810
|
+
return result.body.multipart.parts;
|
|
47811
|
+
}
|
|
47812
|
+
async function completeUpload(organizationId, codeUploadId) {
|
|
47813
|
+
const result = await authenticatedRequest(
|
|
47814
|
+
(client) => client.codeUpload.completeCodeUpload({
|
|
47815
|
+
params: { organizationId, codeUploadId },
|
|
47816
|
+
body: {}
|
|
47817
|
+
})
|
|
47818
|
+
);
|
|
47819
|
+
if (result.status !== ACCEPTED_STATUS) {
|
|
47820
|
+
failWriteOp5("complete", result.status, result.body);
|
|
47821
|
+
}
|
|
47822
|
+
return result.body;
|
|
47823
|
+
}
|
|
47824
|
+
async function waitForTerminal(organizationId, codeUploadId, runtime) {
|
|
47825
|
+
for (; ; ) {
|
|
47826
|
+
const upload = await requireUpload(organizationId, codeUploadId);
|
|
47827
|
+
if (isTerminalCodeUploadStatus(upload.status)) {
|
|
47828
|
+
return upload;
|
|
47829
|
+
}
|
|
47830
|
+
if (process.stderr.isTTY) {
|
|
47831
|
+
process.stderr.write(`\rWaiting for malware scan (${formatStatus(upload.status)})...`);
|
|
47832
|
+
}
|
|
47833
|
+
await runtime.wait(runtime.pollIntervalMs);
|
|
47834
|
+
}
|
|
47835
|
+
}
|
|
47836
|
+
function printWaitOutcome(upload, output) {
|
|
47837
|
+
if (output !== "table") {
|
|
47838
|
+
outputUpload(upload, output);
|
|
47839
|
+
if (upload.status !== "READY") process.exit(1);
|
|
47840
|
+
return;
|
|
47841
|
+
}
|
|
47842
|
+
if (upload.status === "READY") {
|
|
47843
|
+
console.log(colorSuccess(`Upload ${upload.codeUploadId} is ready.`));
|
|
47844
|
+
outputUpload(upload, output);
|
|
47845
|
+
if (upload.artifactKind === "SOURCE_ARCHIVE") {
|
|
47846
|
+
console.log(
|
|
47847
|
+
`Link it with: tracecli upload link --app <applicationId> --upload ${upload.codeUploadId}`
|
|
47848
|
+
);
|
|
47849
|
+
}
|
|
47850
|
+
return;
|
|
47851
|
+
}
|
|
47852
|
+
const reason = upload.rejectionReason ? ` (${formatStatus(upload.rejectionReason)})` : "";
|
|
47853
|
+
console.error(
|
|
47854
|
+
colorError(`Upload ${upload.codeUploadId} ended as ${formatStatus(upload.status)}${reason}.`)
|
|
47855
|
+
);
|
|
47856
|
+
outputUpload(upload, output);
|
|
47857
|
+
process.exit(1);
|
|
47858
|
+
}
|
|
47859
|
+
async function runTransfer(filePath, options, runtime) {
|
|
47860
|
+
const output = parseOutput(options.output);
|
|
47861
|
+
let prepared;
|
|
47862
|
+
try {
|
|
47863
|
+
prepared = await prepareUploadFile(filePath, {
|
|
47864
|
+
displayName: options.name,
|
|
47865
|
+
artifactKind: options.artifactKind
|
|
47866
|
+
});
|
|
47867
|
+
} catch (error48) {
|
|
47868
|
+
failPrepare(error48);
|
|
47869
|
+
}
|
|
47870
|
+
const organizationId = await resolveOrganizationId();
|
|
47871
|
+
const initialized = await authenticatedRequest(
|
|
47872
|
+
(client) => client.codeUpload.initializeCodeUpload({
|
|
47873
|
+
params: { organizationId },
|
|
47874
|
+
body: {
|
|
47875
|
+
displayName: prepared.displayName,
|
|
47876
|
+
originalFilename: prepared.originalFilename,
|
|
47877
|
+
sizeBytes: prepared.sizeBytes,
|
|
47878
|
+
archiveSha256: prepared.archiveSha256,
|
|
47879
|
+
artifactKind: prepared.artifactKind,
|
|
47880
|
+
...options.applicationId !== void 0 ? { applicationId: options.applicationId } : {}
|
|
47881
|
+
}
|
|
47882
|
+
})
|
|
47883
|
+
);
|
|
47884
|
+
if (initialized.status !== CREATED_STATUS) {
|
|
47885
|
+
failWriteOp5("create", initialized.status, initialized.body);
|
|
47886
|
+
}
|
|
47887
|
+
const codeUploadId = initialized.body.upload.codeUploadId;
|
|
47888
|
+
if (output === "table") {
|
|
47889
|
+
console.error(
|
|
47890
|
+
`Initialized upload ${codeUploadId} (${formatArchiveBytes(prepared.sizeBytes)}).`
|
|
47891
|
+
);
|
|
47892
|
+
}
|
|
47893
|
+
try {
|
|
47894
|
+
await uploadCodeArchiveParts(
|
|
47895
|
+
prepared.path,
|
|
47896
|
+
prepared.sizeBytes,
|
|
47897
|
+
initialized.body.multipart.partSizeBytes,
|
|
47898
|
+
initialized.body.multipart.parts,
|
|
47899
|
+
{
|
|
47900
|
+
fetch: runtime.fetch,
|
|
47901
|
+
wait: runtime.wait,
|
|
47902
|
+
onProgress: writeProgress,
|
|
47903
|
+
refreshParts: () => refreshRemainingParts(organizationId, codeUploadId)
|
|
47904
|
+
}
|
|
47905
|
+
);
|
|
47906
|
+
finishProgress();
|
|
47907
|
+
} catch (error48) {
|
|
47908
|
+
finishProgress();
|
|
47909
|
+
const detail = error48 instanceof Error ? error48.message : "Upload to storage failed.";
|
|
47910
|
+
console.error(colorError(detail));
|
|
47911
|
+
console.error(resumeHint(codeUploadId, filePath));
|
|
47912
|
+
process.exit(1);
|
|
47913
|
+
}
|
|
47914
|
+
const completed = await completeUpload(organizationId, codeUploadId);
|
|
47915
|
+
const upload = options.wait ? await waitForTerminal(organizationId, codeUploadId, runtime) : completed;
|
|
47916
|
+
if (options.wait) {
|
|
47917
|
+
if (process.stderr.isTTY) process.stderr.write("\n");
|
|
47918
|
+
printWaitOutcome(upload, output);
|
|
47919
|
+
return;
|
|
47920
|
+
}
|
|
47921
|
+
if (output === "table") {
|
|
47922
|
+
console.log(colorSuccess(`Uploaded ${sanitizeDisplay(upload.displayName)}.`));
|
|
47923
|
+
console.log(
|
|
47924
|
+
`Malware scanning is in progress. Check with: tracecli upload wait ${codeUploadId}`
|
|
47925
|
+
);
|
|
47926
|
+
}
|
|
47927
|
+
outputUpload(upload, output);
|
|
47928
|
+
}
|
|
47929
|
+
async function runCreate5(filePath, options, runtime) {
|
|
47930
|
+
const inferred = inferArtifactKindFromFilename(filePath);
|
|
47931
|
+
if (inferred != null && isApplicationBuildArtifactKind(inferred)) {
|
|
47932
|
+
console.error(
|
|
47933
|
+
colorError(
|
|
47934
|
+
"That file is a mobile build, not a source archive. Attach it to an existing application with:"
|
|
47935
|
+
)
|
|
47936
|
+
);
|
|
47937
|
+
console.error(" tracecli upload build <file> --app <applicationId>");
|
|
47938
|
+
process.exit(1);
|
|
47939
|
+
}
|
|
47940
|
+
await runTransfer(
|
|
47941
|
+
filePath,
|
|
47942
|
+
{
|
|
47943
|
+
...options.name !== void 0 ? { name: options.name } : {},
|
|
47944
|
+
...options.wait !== void 0 ? { wait: options.wait } : {},
|
|
47945
|
+
output: options.output,
|
|
47946
|
+
artifactKind: "SOURCE_ARCHIVE"
|
|
47947
|
+
},
|
|
47948
|
+
runtime
|
|
47949
|
+
);
|
|
47950
|
+
}
|
|
47951
|
+
async function runBuild(filePath, options, runtime) {
|
|
47952
|
+
const applicationId = requireUuid(options.app, "--app");
|
|
47953
|
+
const inferred = inferArtifactKindFromFilename(filePath);
|
|
47954
|
+
if (inferred == null || !isApplicationBuildArtifactKind(inferred)) {
|
|
47955
|
+
console.error(
|
|
47956
|
+
colorError(
|
|
47957
|
+
"Build uploads are an APK or IPA attached to an existing application. For a source ZIP, use `tracecli upload create <file>`."
|
|
47958
|
+
)
|
|
47959
|
+
);
|
|
47960
|
+
process.exit(1);
|
|
47961
|
+
}
|
|
47962
|
+
await runTransfer(
|
|
47963
|
+
filePath,
|
|
47964
|
+
{
|
|
47965
|
+
...options.name !== void 0 ? { name: options.name } : {},
|
|
47966
|
+
...options.wait !== void 0 ? { wait: options.wait } : {},
|
|
47967
|
+
output: options.output,
|
|
47968
|
+
applicationId,
|
|
47969
|
+
artifactKind: inferred
|
|
47970
|
+
},
|
|
47971
|
+
runtime
|
|
47972
|
+
);
|
|
47973
|
+
}
|
|
47974
|
+
async function runResume(codeUploadIdArg, filePath, options, runtime) {
|
|
47975
|
+
const output = parseOutput(options.output);
|
|
47976
|
+
const codeUploadId = requireUuid(codeUploadIdArg, "code upload id");
|
|
47977
|
+
let prepared;
|
|
47978
|
+
try {
|
|
47979
|
+
prepared = await prepareUploadFile(filePath);
|
|
47980
|
+
} catch (error48) {
|
|
47981
|
+
failPrepare(error48);
|
|
47982
|
+
}
|
|
47983
|
+
const organizationId = await resolveOrganizationId();
|
|
47984
|
+
const resumed = await authenticatedRequest(
|
|
47985
|
+
(client) => client.codeUpload.resumeCodeUpload({
|
|
47986
|
+
params: { organizationId, codeUploadId },
|
|
47987
|
+
body: {}
|
|
47988
|
+
})
|
|
47989
|
+
);
|
|
47990
|
+
if (resumed.status !== SUCCESS_STATUS) {
|
|
47991
|
+
failWriteOp5("resume", resumed.status, resumed.body);
|
|
47992
|
+
}
|
|
47993
|
+
try {
|
|
47994
|
+
assertPreparedMatchesUpload(resumed.body.upload, prepared);
|
|
47995
|
+
} catch (error48) {
|
|
47996
|
+
failPrepare(error48);
|
|
47997
|
+
}
|
|
47998
|
+
const uploadedBytes = resumed.body.multipart.uploadedParts.reduce(
|
|
47999
|
+
(sum, part) => sum + part.sizeBytes,
|
|
48000
|
+
0
|
|
48001
|
+
);
|
|
48002
|
+
if (output === "table") {
|
|
48003
|
+
console.error(
|
|
48004
|
+
`Resuming upload ${codeUploadId} (${formatArchiveBytes(uploadedBytes)} already stored).`
|
|
48005
|
+
);
|
|
48006
|
+
}
|
|
48007
|
+
try {
|
|
48008
|
+
await uploadCodeArchiveParts(
|
|
48009
|
+
prepared.path,
|
|
48010
|
+
prepared.sizeBytes,
|
|
48011
|
+
resumed.body.multipart.partSizeBytes,
|
|
48012
|
+
resumed.body.multipart.parts,
|
|
48013
|
+
{
|
|
48014
|
+
initialUploadedBytes: uploadedBytes,
|
|
48015
|
+
fetch: runtime.fetch,
|
|
48016
|
+
wait: runtime.wait,
|
|
48017
|
+
onProgress: writeProgress,
|
|
48018
|
+
refreshParts: () => refreshRemainingParts(organizationId, codeUploadId)
|
|
48019
|
+
}
|
|
48020
|
+
);
|
|
48021
|
+
finishProgress();
|
|
48022
|
+
} catch (error48) {
|
|
48023
|
+
finishProgress();
|
|
48024
|
+
const detail = error48 instanceof Error ? error48.message : "Upload to storage failed.";
|
|
48025
|
+
console.error(colorError(detail));
|
|
48026
|
+
console.error(resumeHint(codeUploadId, filePath));
|
|
48027
|
+
process.exit(1);
|
|
48028
|
+
}
|
|
48029
|
+
const completed = await completeUpload(organizationId, codeUploadId);
|
|
48030
|
+
const upload = options.wait ? await waitForTerminal(organizationId, codeUploadId, runtime) : completed;
|
|
48031
|
+
if (options.wait) {
|
|
48032
|
+
if (process.stderr.isTTY) process.stderr.write("\n");
|
|
48033
|
+
printWaitOutcome(upload, output);
|
|
48034
|
+
return;
|
|
48035
|
+
}
|
|
48036
|
+
if (output === "table") {
|
|
48037
|
+
console.log(colorSuccess(`Uploaded ${sanitizeDisplay(upload.displayName)}.`));
|
|
48038
|
+
console.log(
|
|
48039
|
+
`Malware scanning is in progress. Check with: tracecli upload wait ${codeUploadId}`
|
|
48040
|
+
);
|
|
48041
|
+
}
|
|
48042
|
+
outputUpload(upload, output);
|
|
48043
|
+
}
|
|
48044
|
+
async function runList5(options) {
|
|
48045
|
+
const output = parseOutput(options.output);
|
|
48046
|
+
if (options.web) {
|
|
48047
|
+
const slug = await resolveOrganizationSlug();
|
|
48048
|
+
const url2 = `${getAppBaseUrl()}/dashboard/${slug}/settings/integrations/sources`;
|
|
48049
|
+
const opened = await openBrowser(url2);
|
|
48050
|
+
if (!opened) console.log(url2);
|
|
48051
|
+
return;
|
|
48052
|
+
}
|
|
48053
|
+
const kind = parseKindOption(options.kind);
|
|
48054
|
+
const applicationId = options.app !== void 0 ? requireUuid(options.app, "--app") : void 0;
|
|
48055
|
+
const organizationId = await resolveOrganizationId();
|
|
48056
|
+
const uploads = await listUploads(organizationId, {
|
|
48057
|
+
...kind !== void 0 ? { artifactKind: kind } : {},
|
|
48058
|
+
...applicationId !== void 0 ? { applicationId } : {}
|
|
48059
|
+
});
|
|
48060
|
+
if (output === "json") {
|
|
48061
|
+
console.log(JSON.stringify({ uploads }, null, 2));
|
|
48062
|
+
return;
|
|
48063
|
+
}
|
|
48064
|
+
if (output === "tsv") {
|
|
48065
|
+
printUploadsTsv(uploads);
|
|
48066
|
+
return;
|
|
48067
|
+
}
|
|
48068
|
+
if (uploads.length === 0) {
|
|
48069
|
+
console.log("No source uploads found.");
|
|
48070
|
+
return;
|
|
48071
|
+
}
|
|
48072
|
+
const { header, rows } = buildUploadsTable(uploads);
|
|
48073
|
+
await runInteractivePager({
|
|
48074
|
+
items: uploads,
|
|
48075
|
+
header,
|
|
48076
|
+
renderItem: (_upload, i2) => [rows[i2] ?? ""],
|
|
48077
|
+
disabled: options.pager === false
|
|
48078
|
+
});
|
|
48079
|
+
}
|
|
48080
|
+
async function runGet4(codeUploadIdArg, options) {
|
|
48081
|
+
const output = parseOutput(options.output);
|
|
48082
|
+
const codeUploadId = requireUuid(codeUploadIdArg, "code upload id");
|
|
48083
|
+
const organizationId = await resolveOrganizationId();
|
|
48084
|
+
const upload = await requireUpload(organizationId, codeUploadId);
|
|
48085
|
+
outputUpload(upload, output);
|
|
48086
|
+
}
|
|
48087
|
+
async function runWait(codeUploadIdArg, options, runtime) {
|
|
48088
|
+
const output = parseOutput(options.output);
|
|
48089
|
+
const codeUploadId = requireUuid(codeUploadIdArg, "code upload id");
|
|
48090
|
+
const organizationId = await resolveOrganizationId();
|
|
48091
|
+
const upload = await waitForTerminal(organizationId, codeUploadId, runtime);
|
|
48092
|
+
if (process.stderr.isTTY) process.stderr.write("\n");
|
|
48093
|
+
printWaitOutcome(upload, output);
|
|
48094
|
+
}
|
|
48095
|
+
async function runDelete5(codeUploadIdArg, options) {
|
|
48096
|
+
const codeUploadId = requireUuid(codeUploadIdArg, "code upload id");
|
|
48097
|
+
const confirmed = await confirmDeletion(`upload ${codeUploadId}`, options.yes);
|
|
48098
|
+
if (!confirmed) {
|
|
48099
|
+
console.log("Aborted.");
|
|
48100
|
+
return;
|
|
48101
|
+
}
|
|
48102
|
+
const organizationId = await resolveOrganizationId();
|
|
48103
|
+
const result = await authenticatedRequest(
|
|
48104
|
+
(client) => client.codeUpload.deleteCodeUpload({
|
|
48105
|
+
params: { organizationId, codeUploadId }
|
|
48106
|
+
})
|
|
48107
|
+
);
|
|
48108
|
+
if (result.status !== NO_CONTENT_STATUS) {
|
|
48109
|
+
failWriteOp5("delete", result.status, result.body);
|
|
48110
|
+
}
|
|
48111
|
+
console.log(colorSuccess(`Deleted upload ${codeUploadId}`));
|
|
48112
|
+
}
|
|
48113
|
+
async function runLink(options) {
|
|
48114
|
+
const output = parseOutput(options.output);
|
|
48115
|
+
const applicationId = requireUuid(options.app, "--app");
|
|
48116
|
+
const codeUploadId = requireUuid(options.upload, "--upload");
|
|
48117
|
+
const organizationId = await resolveOrganizationId();
|
|
48118
|
+
const upload = await requireUpload(organizationId, codeUploadId);
|
|
48119
|
+
if (isApplicationBuildArtifactKind(upload.artifactKind)) {
|
|
48120
|
+
console.error(colorError("APK and IPA uploads cannot be linked as source repositories."));
|
|
48121
|
+
process.exit(1);
|
|
48122
|
+
}
|
|
48123
|
+
if (upload.status !== "READY" || upload.malwareScanResult !== "NO_THREATS_FOUND") {
|
|
48124
|
+
console.error(
|
|
48125
|
+
colorError(
|
|
48126
|
+
`Upload is ${formatStatus(upload.status)}; it must be ready and pass malware scanning before it can be linked.`
|
|
48127
|
+
)
|
|
48128
|
+
);
|
|
48129
|
+
process.exit(1);
|
|
48130
|
+
}
|
|
48131
|
+
const result = await authenticatedRequest(
|
|
48132
|
+
(client) => client.application.createApplicationRepository({
|
|
48133
|
+
params: { organizationId, applicationId },
|
|
48134
|
+
body: {
|
|
48135
|
+
repositoryId: upload.repositoryId,
|
|
48136
|
+
...options.path !== void 0 ? { repositoryPath: options.path } : {}
|
|
48137
|
+
}
|
|
48138
|
+
})
|
|
48139
|
+
);
|
|
48140
|
+
if (result.status !== CREATED_STATUS) {
|
|
48141
|
+
failWriteOp5("link", result.status, result.body);
|
|
48142
|
+
}
|
|
48143
|
+
if (output === "json") {
|
|
48144
|
+
console.log(JSON.stringify(result.body, null, 2));
|
|
48145
|
+
return;
|
|
48146
|
+
}
|
|
48147
|
+
if (output === "tsv") {
|
|
48148
|
+
console.log(["applicationId", "repositoryId", "codeUploadId"].join(" "));
|
|
48149
|
+
console.log([applicationId, upload.repositoryId, codeUploadId].join(" "));
|
|
48150
|
+
return;
|
|
48151
|
+
}
|
|
48152
|
+
console.log(
|
|
48153
|
+
colorSuccess(`Linked ${sanitizeDisplay(upload.displayName)} to application ${applicationId}.`)
|
|
48154
|
+
);
|
|
48155
|
+
}
|
|
48156
|
+
function createCreateCommand5(runtime) {
|
|
48157
|
+
return new Command("create").alias("source").description("Upload a source ZIP as a code archive (not a mobile build)").argument("<file>", "Path to a .zip source archive").option("--name <displayName>", "Display name (defaults to the filename without extension)").option("--wait", "Wait until malware scanning finishes").option("-o, --output <format>", "Output format: table, json, tsv (default: table)", "table").addHelpText(
|
|
48158
|
+
"after",
|
|
48159
|
+
`
|
|
48160
|
+
This is a code upload: a source ZIP that becomes an uploaded repository after
|
|
48161
|
+
it is ready. Link it with \`upload link\`. APK and IPA files belong on
|
|
48162
|
+
\`upload build --app\`, which attaches a build to an existing application.
|
|
48163
|
+
|
|
48164
|
+
Requires a human login (\`tracecli auth login\`) on a pentest-enabled organization.
|
|
48165
|
+
API keys are rejected.
|
|
48166
|
+
|
|
48167
|
+
Examples:
|
|
48168
|
+
tracecli upload create ./checkout.zip
|
|
48169
|
+
tracecli upload create ./checkout.zip --name "Checkout" --wait`
|
|
48170
|
+
).action(async (file2, options) => {
|
|
48171
|
+
await runCreate5(file2, options, runtime);
|
|
48172
|
+
});
|
|
48173
|
+
}
|
|
48174
|
+
function createBuildCommand(runtime) {
|
|
48175
|
+
return new Command("build").description("Upload an APK or IPA and attach it to an existing application").argument("<file>", "Path to an .apk or .ipa").requiredOption("--app <applicationId>", "Application that owns this build").option("--name <displayName>", "Display name (defaults to the filename without extension)").option("--wait", "Wait until malware scanning finishes").option("-o, --output <format>", "Output format: table, json, tsv (default: table)", "table").addHelpText(
|
|
48176
|
+
"after",
|
|
48177
|
+
`
|
|
48178
|
+
Build uploads are not source archives. They attach to --app immediately and
|
|
48179
|
+
cannot be linked as a repository. For a source ZIP, use \`upload create\`.
|
|
48180
|
+
|
|
48181
|
+
Examples:
|
|
48182
|
+
tracecli upload build ./app-release.apk --app <applicationId>
|
|
48183
|
+
tracecli upload build ./app.ipa --app <applicationId> --name "iOS build"`
|
|
48184
|
+
).action(async (file2, options) => {
|
|
48185
|
+
await runBuild(file2, options, runtime);
|
|
48186
|
+
});
|
|
48187
|
+
}
|
|
48188
|
+
function createListCommand7() {
|
|
48189
|
+
return new Command("list").description("List source and build uploads in the active organization").option("--kind <kind>", "Filter: source (code ZIP), apk, or ipa").option("--app <applicationId>", "Filter build uploads for one application").option("-o, --output <format>", "Output format: table, json, tsv (default: table)", "table").option("--no-pager", "Disable the interactive pager for table output").option("-w, --web", "Open the source uploads page in a browser").action(async (options) => {
|
|
48190
|
+
await runList5(options);
|
|
48191
|
+
});
|
|
48192
|
+
}
|
|
48193
|
+
function createGetCommand4() {
|
|
48194
|
+
return new Command("get").description("Show a single upload").argument("<codeUploadId>", "Upload UUID").option("-o, --output <format>", "Output format: table, json, tsv (default: table)", "table").action(async (codeUploadId, options) => {
|
|
48195
|
+
await runGet4(codeUploadId, options);
|
|
48196
|
+
});
|
|
48197
|
+
}
|
|
48198
|
+
function createResumeCommand(runtime) {
|
|
48199
|
+
return new Command("resume").description("Resume an interrupted upload with the same file").argument("<codeUploadId>", "Upload UUID from the interrupted create").argument("<file>", "The same .zip, .apk, or .ipa file").option("--wait", "Wait until malware scanning finishes").option("-o, --output <format>", "Output format: table, json, tsv (default: table)", "table").addHelpText(
|
|
48200
|
+
"after",
|
|
48201
|
+
`
|
|
48202
|
+
The file must match the original size and SHA-256. Trace skips parts that
|
|
48203
|
+
already landed in storage.
|
|
48204
|
+
|
|
48205
|
+
Examples:
|
|
48206
|
+
tracecli upload resume <codeUploadId> ./checkout.zip`
|
|
48207
|
+
).action(async (codeUploadId, file2, options) => {
|
|
48208
|
+
await runResume(codeUploadId, file2, options, runtime);
|
|
48209
|
+
});
|
|
48210
|
+
}
|
|
48211
|
+
function createWaitCommand(runtime) {
|
|
48212
|
+
return new Command("wait").description("Wait until an upload finishes malware scanning and validation").argument("<codeUploadId>", "Upload UUID").option("-o, --output <format>", "Output format: table, json, tsv (default: table)", "table").action(async (codeUploadId, options) => {
|
|
48213
|
+
await runWait(codeUploadId, options, runtime);
|
|
48214
|
+
});
|
|
48215
|
+
}
|
|
48216
|
+
function createDeleteCommand5() {
|
|
48217
|
+
return new Command("delete").description("Delete an uploaded archive and its stored bytes").argument("<codeUploadId>", "Upload UUID").option("-y, --yes", "Skip the confirmation prompt (required in non-interactive shells)").action(async (codeUploadId, options) => {
|
|
48218
|
+
await runDelete5(codeUploadId, options);
|
|
48219
|
+
});
|
|
48220
|
+
}
|
|
48221
|
+
function createLinkCommand() {
|
|
48222
|
+
return new Command("link").description("Link a ready source upload to an application").requiredOption("--app <applicationId>", "Application UUID").requiredOption("--upload <codeUploadId>", "Upload UUID (must be a ready source ZIP)").option("--path <repositoryPath>", "Path within the archive").option("-o, --output <format>", "Output format: table, json, tsv (default: table)", "table").addHelpText(
|
|
48223
|
+
"after",
|
|
48224
|
+
`
|
|
48225
|
+
Only a READY source archive that passed malware scanning can be linked.
|
|
48226
|
+
APK and IPA builds attach to an application with \`upload build --app\`.
|
|
48227
|
+
|
|
48228
|
+
Examples:
|
|
48229
|
+
tracecli upload link --app <applicationId> --upload <codeUploadId>`
|
|
48230
|
+
).action(async (options) => {
|
|
48231
|
+
await runLink(options);
|
|
48232
|
+
});
|
|
48233
|
+
}
|
|
48234
|
+
function createUploadCommand(runtime = {}) {
|
|
48235
|
+
const resolved = resolveRuntime(runtime);
|
|
48236
|
+
const upload = new Command("upload").alias("code-upload").description("Upload source archives (and separately, mobile builds) for pentest");
|
|
48237
|
+
upload.addCommand(createCreateCommand5(resolved));
|
|
48238
|
+
upload.addCommand(createBuildCommand(resolved));
|
|
48239
|
+
upload.addCommand(createListCommand7());
|
|
48240
|
+
upload.addCommand(createGetCommand4());
|
|
48241
|
+
upload.addCommand(createResumeCommand(resolved));
|
|
48242
|
+
upload.addCommand(createWaitCommand(resolved));
|
|
48243
|
+
upload.addCommand(createDeleteCommand5());
|
|
48244
|
+
upload.addCommand(createLinkCommand());
|
|
48245
|
+
return upload;
|
|
48246
|
+
}
|
|
48247
|
+
|
|
48248
|
+
// src/vulnerabilities/commands.ts
|
|
48249
|
+
var DEFAULT_LIMIT3 = 30;
|
|
48250
|
+
var MAX_LIMIT3 = 100;
|
|
48251
|
+
var VULNERABILITY_FILTER_FIELDS = [
|
|
48252
|
+
"repositoryId",
|
|
48253
|
+
"severity",
|
|
48254
|
+
"vulnerabilityType",
|
|
48255
|
+
"status",
|
|
48256
|
+
"cweId",
|
|
48257
|
+
"scanMode"
|
|
48258
|
+
];
|
|
48259
|
+
function parseVulnerabilityFilter(expression) {
|
|
48260
|
+
const match = expression.match(/^([^!=]+)(!=|=)(.+)$/);
|
|
48261
|
+
if (!match?.[1] || !match[2] || !match[3]) {
|
|
48262
|
+
return {
|
|
48263
|
+
error: `Invalid filter expression '${expression}'. Expected format: field=value or field!=value (e.g. cweId=CWE-89).`
|
|
48264
|
+
};
|
|
48265
|
+
}
|
|
48266
|
+
const field = match[1].trim();
|
|
48267
|
+
const op = match[2];
|
|
48268
|
+
const value = match[3].trim();
|
|
48269
|
+
if (value.length === 0) {
|
|
48270
|
+
return { error: `Filter value cannot be empty in expression '${expression}'.` };
|
|
48271
|
+
}
|
|
48272
|
+
if (!VULNERABILITY_FILTER_FIELDS.includes(field)) {
|
|
48273
|
+
return {
|
|
48274
|
+
error: `Unknown filter field '${field}'. Supported fields: ${VULNERABILITY_FILTER_FIELDS.join(", ")}.`
|
|
48275
|
+
};
|
|
48276
|
+
}
|
|
48277
|
+
return { field, operator: op === "!=" ? "neq" : "eq", value };
|
|
48278
|
+
}
|
|
48279
|
+
async function resolveRepositoryIdAuthenticated(organizationId, repoIdentifier) {
|
|
48280
|
+
const result = await authenticatedRequest(
|
|
48281
|
+
(client) => client.repository.listRepositories({
|
|
48282
|
+
params: { organizationId }
|
|
48283
|
+
})
|
|
48284
|
+
);
|
|
48285
|
+
if (result.status !== 200) {
|
|
48286
|
+
console.error(`Failed to list repositories (status ${result.status}).`);
|
|
48287
|
+
process.exit(1);
|
|
48288
|
+
}
|
|
48289
|
+
const repos = result.body;
|
|
48290
|
+
const isBareNameOnly = !repoIdentifier.includes("/") && !repoIdentifier.startsWith("http");
|
|
48291
|
+
if (!isBareNameOnly) {
|
|
48292
|
+
const match = repos.find((r2) => {
|
|
48293
|
+
const url2 = r2.repositoryUrl;
|
|
48294
|
+
if (url2.endsWith(`/${repoIdentifier}`)) return true;
|
|
48295
|
+
if (url2.endsWith(`/${repoIdentifier}.git`)) return true;
|
|
48296
|
+
if (url2 === repoIdentifier) return true;
|
|
48297
|
+
return false;
|
|
48298
|
+
});
|
|
48299
|
+
if (match) {
|
|
48300
|
+
return match.repositoryId;
|
|
48301
|
+
}
|
|
48302
|
+
}
|
|
48303
|
+
const byName = repos.filter((r2) => {
|
|
48304
|
+
const segments = r2.repositoryUrl.replace(/\.git$/, "").split("/");
|
|
48305
|
+
const name = segments[segments.length - 1];
|
|
48306
|
+
return name === repoIdentifier;
|
|
48307
|
+
});
|
|
48308
|
+
if (byName.length === 1 && byName[0]) {
|
|
48309
|
+
return byName[0].repositoryId;
|
|
48310
|
+
}
|
|
48311
|
+
if (byName.length > 1) {
|
|
48312
|
+
console.error(
|
|
48313
|
+
`Ambiguous repository name '${repoIdentifier}'. Multiple matches found:
|
|
48314
|
+
${byName.map((r2) => ` - ${r2.repositoryUrl}`).join("\n")}
|
|
48315
|
+
Please use the full owner/repo format.`
|
|
48316
|
+
);
|
|
48317
|
+
process.exit(1);
|
|
48318
|
+
}
|
|
48319
|
+
console.error(
|
|
48320
|
+
`Repository '${repoIdentifier}' not found.
|
|
48321
|
+
Run \`tracecli repo list\` to see available repositories.`
|
|
48322
|
+
);
|
|
48323
|
+
process.exit(1);
|
|
48324
|
+
}
|
|
48325
|
+
function formatSeverity(severity) {
|
|
48326
|
+
switch (severity) {
|
|
48327
|
+
case "CRITICAL":
|
|
48328
|
+
return "CRIT";
|
|
48329
|
+
case "HIGH":
|
|
47058
48330
|
return "HIGH";
|
|
47059
48331
|
case "MEDIUM":
|
|
47060
48332
|
return "MED";
|
|
@@ -47149,7 +48421,7 @@ function printVulnerabilityDetailTsv(vulnerability) {
|
|
|
47149
48421
|
].map(sanitizeTsvField);
|
|
47150
48422
|
console.log(cols.join(" "));
|
|
47151
48423
|
}
|
|
47152
|
-
function
|
|
48424
|
+
function createGetCommand5() {
|
|
47153
48425
|
return new Command("get").description("Get a vulnerability by display ID or UUID").argument("<id>", "Vulnerability display ID (e.g. TRACE-123 or 123) or UUID").option("-o, --output <format>", "Output format: table, json, tsv (default: table)", "table").option("-w, --web", "Open the vulnerability detail page in a browser").addHelpText(
|
|
47154
48426
|
"after",
|
|
47155
48427
|
`
|
|
@@ -47208,7 +48480,7 @@ Run \`tracecli vulnerabilities list\` to see available vulnerabilities.`
|
|
|
47208
48480
|
printVulnerabilityDetail(result.body);
|
|
47209
48481
|
});
|
|
47210
48482
|
}
|
|
47211
|
-
function
|
|
48483
|
+
function createListCommand8() {
|
|
47212
48484
|
return new Command("list").description("List vulnerabilities for your organization").option("--repo <repo>", "Filter by repository name or owner/repo").option(
|
|
47213
48485
|
"--severity <severity>",
|
|
47214
48486
|
"Filter by severity (CRITICAL, HIGH, MEDIUM, LOW)",
|
|
@@ -47341,8 +48613,8 @@ Examples:
|
|
|
47341
48613
|
}
|
|
47342
48614
|
function createVulnerabilitiesCommand() {
|
|
47343
48615
|
const vulnerabilities = new Command("vulnerabilities").description("Manage vulnerabilities");
|
|
47344
|
-
vulnerabilities.addCommand(
|
|
47345
|
-
vulnerabilities.addCommand(
|
|
48616
|
+
vulnerabilities.addCommand(createGetCommand5());
|
|
48617
|
+
vulnerabilities.addCommand(createListCommand8());
|
|
47346
48618
|
return vulnerabilities;
|
|
47347
48619
|
}
|
|
47348
48620
|
|
|
@@ -47365,6 +48637,7 @@ program2.addCommand(createInboxCommand());
|
|
|
47365
48637
|
program2.addCommand(createRepoCommand());
|
|
47366
48638
|
program2.addCommand(createVulnerabilitiesCommand());
|
|
47367
48639
|
program2.addCommand(createCodeSizeCommand());
|
|
48640
|
+
program2.addCommand(createUploadCommand());
|
|
47368
48641
|
program2.addCommand(createPentestCommand());
|
|
47369
48642
|
program2.addCommand(createCompletionCommand(program2));
|
|
47370
48643
|
program2.addCommand(createUpgradeCommand());
|