securewithtrace 0.38.0 → 0.39.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 +965 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
`tracecli` is the official CLI for Trace.
|
|
12
12
|
|
|
13
|
-
Use it to authenticate, browse and filter connected repositories, clone repositories, and inspect vulnerabilities directly from the command line.
|
|
13
|
+
Use it to authenticate, browse and filter connected repositories, upload source archives, clone repositories, and inspect vulnerabilities directly from the command line.
|
|
14
14
|
|
|
15
15
|
## Install
|
|
16
16
|
|
|
@@ -56,6 +56,7 @@ tracecli vulnerabilities list --severity HIGH
|
|
|
56
56
|
- **Find repositories by dependency** with optional version filtering (`repo list --dependency`)
|
|
57
57
|
- **Clone repositories** with secure token handling (`repo clone`)
|
|
58
58
|
- **Provision and read Trace-hosted email inboxes** for pentest test accounts (`inbox create/list/emails/email/delete`)
|
|
59
|
+
- **Upload source ZIPs** for pentest when the app is not on GitHub, or attach an APK/IPA to an existing app (`upload create`, `upload build --app`)
|
|
59
60
|
- **Filter vulnerabilities** by repo, severity, and status (`vulnerabilities list`)
|
|
60
61
|
- **Export results as JSON or TSV** for automation (`vulnerabilities list --output json`)
|
|
61
62
|
- **Upgrade easily** with built-in update checks and `upgrade` command (`upgrade`, `self-update`)
|
|
@@ -125,6 +126,44 @@ tracecli repo clone securewithtrace/trace
|
|
|
125
126
|
tracecli repo clone securewithtrace/trace --branch main --depth 1
|
|
126
127
|
```
|
|
127
128
|
|
|
129
|
+
### Source uploads
|
|
130
|
+
|
|
131
|
+
`upload create` is a source ZIP (a code upload). `upload build --app` attaches an APK or IPA to an existing application. Requires `tracecli auth login` on a pentest-enabled organization; API keys are rejected.
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
# Upload a source archive and wait until it is ready
|
|
135
|
+
tracecli upload create ./checkout.zip --name "Checkout" --wait
|
|
136
|
+
|
|
137
|
+
# List uploads
|
|
138
|
+
tracecli upload list
|
|
139
|
+
|
|
140
|
+
# Resume an interrupted transfer with the same file
|
|
141
|
+
tracecli upload resume <codeUploadId> ./checkout.zip
|
|
142
|
+
|
|
143
|
+
# Link a ready source ZIP to an application
|
|
144
|
+
tracecli upload link --app <applicationId> --upload <codeUploadId>
|
|
145
|
+
|
|
146
|
+
# Attach an Android build to an existing application
|
|
147
|
+
tracecli upload build ./app-release.apk --app <applicationId>
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Flags for `upload create`:
|
|
151
|
+
|
|
152
|
+
| Flag | Description |
|
|
153
|
+
|------|-------------|
|
|
154
|
+
| `--name <displayName>` | Display name (defaults to the filename without extension) |
|
|
155
|
+
| `--wait` | Wait until malware scanning finishes |
|
|
156
|
+
| `--output <format>` | `table` (default), `json`, `tsv` |
|
|
157
|
+
|
|
158
|
+
Flags for `upload build`:
|
|
159
|
+
|
|
160
|
+
| Flag | Description |
|
|
161
|
+
|------|-------------|
|
|
162
|
+
| `--app <applicationId>` | Application that owns this build (required) |
|
|
163
|
+
| `--name <displayName>` | Display name (defaults to the filename without extension) |
|
|
164
|
+
| `--wait` | Wait until malware scanning finishes |
|
|
165
|
+
| `--output <format>` | `table` (default), `json`, `tsv` |
|
|
166
|
+
|
|
128
167
|
### Vulnerabilities
|
|
129
168
|
|
|
130
169
|
```bash
|
package/dist/index.js
CHANGED
|
@@ -37929,18 +37929,18 @@ var proto = Object.defineProperties(() => {
|
|
|
37929
37929
|
}
|
|
37930
37930
|
}
|
|
37931
37931
|
});
|
|
37932
|
-
var createStyler = (
|
|
37932
|
+
var createStyler = (open2, close, parent) => {
|
|
37933
37933
|
let openAll;
|
|
37934
37934
|
let closeAll;
|
|
37935
37935
|
if (parent === void 0) {
|
|
37936
|
-
openAll =
|
|
37936
|
+
openAll = open2;
|
|
37937
37937
|
closeAll = close;
|
|
37938
37938
|
} else {
|
|
37939
|
-
openAll = parent.openAll +
|
|
37939
|
+
openAll = parent.openAll + open2;
|
|
37940
37940
|
closeAll = close + parent.closeAll;
|
|
37941
37941
|
}
|
|
37942
37942
|
return {
|
|
37943
|
-
open,
|
|
37943
|
+
open: open2,
|
|
37944
37944
|
close,
|
|
37945
37945
|
openAll,
|
|
37946
37946
|
closeAll,
|
|
@@ -39659,6 +39659,7 @@ function sanitizeDisplay(value) {
|
|
|
39659
39659
|
}
|
|
39660
39660
|
var SUCCESS_STATUS = 200;
|
|
39661
39661
|
var CREATED_STATUS = 201;
|
|
39662
|
+
var ACCEPTED_STATUS = 202;
|
|
39662
39663
|
var NO_CONTENT_STATUS = 204;
|
|
39663
39664
|
var UNAUTHORIZED_STATUS2 = 401;
|
|
39664
39665
|
function parseOutput(value) {
|
|
@@ -46973,6 +46974,961 @@ Examples:
|
|
|
46973
46974
|
});
|
|
46974
46975
|
}
|
|
46975
46976
|
|
|
46977
|
+
// src/upload/prepare.ts
|
|
46978
|
+
import { createHash } from "crypto";
|
|
46979
|
+
import { createReadStream, statSync as statSync3 } from "fs";
|
|
46980
|
+
import { open } from "fs/promises";
|
|
46981
|
+
import { basename as basename2 } from "path";
|
|
46982
|
+
var CodeUploadPrepareError = class extends Error {
|
|
46983
|
+
constructor(message) {
|
|
46984
|
+
super(message);
|
|
46985
|
+
this.name = "CodeUploadPrepareError";
|
|
46986
|
+
}
|
|
46987
|
+
};
|
|
46988
|
+
var KIND_BY_INPUT = {
|
|
46989
|
+
source: "SOURCE_ARCHIVE",
|
|
46990
|
+
code: "SOURCE_ARCHIVE",
|
|
46991
|
+
"source-archive": "SOURCE_ARCHIVE",
|
|
46992
|
+
source_archive: "SOURCE_ARCHIVE",
|
|
46993
|
+
SOURCE_ARCHIVE: "SOURCE_ARCHIVE",
|
|
46994
|
+
apk: "ANDROID_APK",
|
|
46995
|
+
android: "ANDROID_APK",
|
|
46996
|
+
"android-apk": "ANDROID_APK",
|
|
46997
|
+
android_apk: "ANDROID_APK",
|
|
46998
|
+
ANDROID_APK: "ANDROID_APK",
|
|
46999
|
+
ipa: "IOS_IPA",
|
|
47000
|
+
ios: "IOS_IPA",
|
|
47001
|
+
"ios-ipa": "IOS_IPA",
|
|
47002
|
+
ios_ipa: "IOS_IPA",
|
|
47003
|
+
IOS_IPA: "IOS_IPA"
|
|
47004
|
+
};
|
|
47005
|
+
function normalizeArtifactKind(input) {
|
|
47006
|
+
const trimmed = input.trim();
|
|
47007
|
+
return KIND_BY_INPUT[trimmed] ?? KIND_BY_INPUT[trimmed.toLowerCase()] ?? null;
|
|
47008
|
+
}
|
|
47009
|
+
function inferArtifactKindFromFilename(filename) {
|
|
47010
|
+
const lower = filename.toLowerCase();
|
|
47011
|
+
if (lower.endsWith(".zip")) return "SOURCE_ARCHIVE";
|
|
47012
|
+
if (lower.endsWith(".apk")) return "ANDROID_APK";
|
|
47013
|
+
if (lower.endsWith(".ipa")) return "IOS_IPA";
|
|
47014
|
+
return null;
|
|
47015
|
+
}
|
|
47016
|
+
function requiredExtensionForKind(kind) {
|
|
47017
|
+
if (kind === "ANDROID_APK") return ".apk";
|
|
47018
|
+
if (kind === "IOS_IPA") return ".ipa";
|
|
47019
|
+
return ".zip";
|
|
47020
|
+
}
|
|
47021
|
+
function defaultDisplayName(originalFilename) {
|
|
47022
|
+
const base = originalFilename.replace(/\.(zip|apk|ipa)$/i, "").trim();
|
|
47023
|
+
return base.slice(0, 200) || "source";
|
|
47024
|
+
}
|
|
47025
|
+
function formatArchiveBytes(bytes) {
|
|
47026
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
47027
|
+
if (bytes < 1024 * 1024) {
|
|
47028
|
+
const kb = bytes / 1024;
|
|
47029
|
+
return `${kb.toFixed(Number.isInteger(kb) ? 0 : 1)} KB`;
|
|
47030
|
+
}
|
|
47031
|
+
if (bytes < 1024 * 1024 * 1024) {
|
|
47032
|
+
const mb = bytes / (1024 * 1024);
|
|
47033
|
+
return `${mb.toFixed(Number.isInteger(mb) ? 0 : 1)} MB`;
|
|
47034
|
+
}
|
|
47035
|
+
const gb = bytes / (1024 * 1024 * 1024);
|
|
47036
|
+
return `${gb.toFixed(Number.isInteger(gb) ? 0 : 1)} GB`;
|
|
47037
|
+
}
|
|
47038
|
+
function archiveTooLargeMessage() {
|
|
47039
|
+
return `The file must be ${formatArchiveBytes(CODE_UPLOAD_MAX_BYTES)} or smaller.`;
|
|
47040
|
+
}
|
|
47041
|
+
async function hashFileSha256(filePath) {
|
|
47042
|
+
const hash2 = createHash("sha256");
|
|
47043
|
+
const stream = createReadStream(filePath);
|
|
47044
|
+
for await (const chunk of stream) {
|
|
47045
|
+
hash2.update(chunk);
|
|
47046
|
+
}
|
|
47047
|
+
return hash2.digest("hex");
|
|
47048
|
+
}
|
|
47049
|
+
async function readFileSlice(filePath, start, endExclusive) {
|
|
47050
|
+
const length = endExclusive - start;
|
|
47051
|
+
if (!Number.isSafeInteger(start) || !Number.isSafeInteger(length) || start < 0 || length < 1) {
|
|
47052
|
+
throw new Error(
|
|
47053
|
+
`Invalid file slice: start=${String(start)} endExclusive=${String(endExclusive)}.`
|
|
47054
|
+
);
|
|
47055
|
+
}
|
|
47056
|
+
const buffer = Buffer.alloc(length);
|
|
47057
|
+
const handle = await open(filePath, "r");
|
|
47058
|
+
try {
|
|
47059
|
+
const { bytesRead } = await handle.read(buffer, 0, length, start);
|
|
47060
|
+
if (bytesRead !== length) {
|
|
47061
|
+
throw new Error(
|
|
47062
|
+
`Expected to read ${String(length)} bytes at offset ${String(start)}, got ${String(bytesRead)}.`
|
|
47063
|
+
);
|
|
47064
|
+
}
|
|
47065
|
+
return buffer;
|
|
47066
|
+
} finally {
|
|
47067
|
+
await handle.close();
|
|
47068
|
+
}
|
|
47069
|
+
}
|
|
47070
|
+
function assertPreparedMatchesUpload(upload, prepared) {
|
|
47071
|
+
if (upload.sizeBytes !== prepared.sizeBytes || upload.archiveSha256 !== prepared.archiveSha256) {
|
|
47072
|
+
throw new CodeUploadPrepareError(
|
|
47073
|
+
"The file does not match the interrupted upload (size or SHA-256 differs)."
|
|
47074
|
+
);
|
|
47075
|
+
}
|
|
47076
|
+
}
|
|
47077
|
+
async function prepareUploadFile(filePath, options = {}) {
|
|
47078
|
+
let sizeBytes;
|
|
47079
|
+
try {
|
|
47080
|
+
const stats = statSync3(filePath);
|
|
47081
|
+
if (!stats.isFile()) {
|
|
47082
|
+
throw new CodeUploadPrepareError("Path is not a regular file. Pass a .zip, .apk, or .ipa.");
|
|
47083
|
+
}
|
|
47084
|
+
sizeBytes = stats.size;
|
|
47085
|
+
} catch (error48) {
|
|
47086
|
+
if (error48 instanceof CodeUploadPrepareError) throw error48;
|
|
47087
|
+
throw new CodeUploadPrepareError("Cannot read that file.");
|
|
47088
|
+
}
|
|
47089
|
+
if (!Number.isSafeInteger(sizeBytes) || sizeBytes < 1) {
|
|
47090
|
+
throw new CodeUploadPrepareError("The file is empty.");
|
|
47091
|
+
}
|
|
47092
|
+
if (sizeBytes > CODE_UPLOAD_MAX_BYTES) {
|
|
47093
|
+
throw new CodeUploadPrepareError(archiveTooLargeMessage());
|
|
47094
|
+
}
|
|
47095
|
+
const originalFilename = basename2(filePath);
|
|
47096
|
+
const inferred = inferArtifactKindFromFilename(originalFilename);
|
|
47097
|
+
const artifactKind = options.artifactKind ?? inferred;
|
|
47098
|
+
if (artifactKind == null) {
|
|
47099
|
+
throw new CodeUploadPrepareError("Pass a .zip source archive, an .apk, or an .ipa.");
|
|
47100
|
+
}
|
|
47101
|
+
const expected = requiredExtensionForKind(artifactKind);
|
|
47102
|
+
if (!originalFilename.toLowerCase().endsWith(expected)) {
|
|
47103
|
+
const label = isApplicationBuildArtifactKind(artifactKind) ? artifactKind === "ANDROID_APK" ? "Android APK" : "iOS package" : "source archive";
|
|
47104
|
+
throw new CodeUploadPrepareError(`A ${label} must end in ${expected}.`);
|
|
47105
|
+
}
|
|
47106
|
+
const displayName = (options.displayName ?? defaultDisplayName(originalFilename)).trim();
|
|
47107
|
+
if (displayName.length < 1 || displayName.length > 200) {
|
|
47108
|
+
throw new CodeUploadPrepareError("Display name must be 1 to 200 characters.");
|
|
47109
|
+
}
|
|
47110
|
+
return {
|
|
47111
|
+
path: filePath,
|
|
47112
|
+
displayName,
|
|
47113
|
+
originalFilename,
|
|
47114
|
+
sizeBytes,
|
|
47115
|
+
archiveSha256: await hashFileSha256(filePath),
|
|
47116
|
+
artifactKind
|
|
47117
|
+
};
|
|
47118
|
+
}
|
|
47119
|
+
|
|
47120
|
+
// src/upload/multipart.ts
|
|
47121
|
+
var MAX_CONCURRENT_UPLOADS = 3;
|
|
47122
|
+
var MAX_PART_ATTEMPTS = 3;
|
|
47123
|
+
var MAX_URL_REFRESHES = 12;
|
|
47124
|
+
async function uploadCodeArchiveParts(filePath, totalBytes, partSizeBytes, parts, options = {}) {
|
|
47125
|
+
assertUploadPlan(totalBytes, partSizeBytes, parts);
|
|
47126
|
+
const fetchPart = options.fetch ?? globalThis.fetch;
|
|
47127
|
+
const wait = options.wait ?? defaultWait;
|
|
47128
|
+
let uploadedBytes = options.initialUploadedBytes ?? 0;
|
|
47129
|
+
if (!Number.isSafeInteger(uploadedBytes) || uploadedBytes < 0 || uploadedBytes > totalBytes) {
|
|
47130
|
+
throw new Error("Invalid multipart progress: uploaded byte count is outside the archive.");
|
|
47131
|
+
}
|
|
47132
|
+
options.onProgress?.(progress(uploadedBytes, totalBytes));
|
|
47133
|
+
if (parts.length === 0) return;
|
|
47134
|
+
const urls = new Map(parts.map((part) => [part.partNumber, part.uploadUrl]));
|
|
47135
|
+
const refreshUrls = createUrlRefresher(urls, options.refreshParts);
|
|
47136
|
+
let cursor3 = 0;
|
|
47137
|
+
const workerCount = Math.min(MAX_CONCURRENT_UPLOADS, parts.length);
|
|
47138
|
+
await Promise.all(
|
|
47139
|
+
Array.from({ length: workerCount }, async () => {
|
|
47140
|
+
while (cursor3 < parts.length) {
|
|
47141
|
+
const part = parts[cursor3];
|
|
47142
|
+
cursor3 += 1;
|
|
47143
|
+
if (!part) return;
|
|
47144
|
+
const start = (part.partNumber - 1) * partSizeBytes;
|
|
47145
|
+
const end = Math.min(start + partSizeBytes, totalBytes);
|
|
47146
|
+
const body = await readFileSlice(filePath, start, end);
|
|
47147
|
+
await uploadPartWithRetry(part.partNumber, urls, body, fetchPart, wait, refreshUrls);
|
|
47148
|
+
uploadedBytes += body.byteLength;
|
|
47149
|
+
options.onProgress?.(progress(uploadedBytes, totalBytes));
|
|
47150
|
+
}
|
|
47151
|
+
})
|
|
47152
|
+
);
|
|
47153
|
+
}
|
|
47154
|
+
function createUrlRefresher(urls, refreshParts) {
|
|
47155
|
+
if (!refreshParts) return async () => false;
|
|
47156
|
+
let inflight = null;
|
|
47157
|
+
let rounds = 0;
|
|
47158
|
+
return async () => {
|
|
47159
|
+
if (rounds >= MAX_URL_REFRESHES) return false;
|
|
47160
|
+
if (!inflight) {
|
|
47161
|
+
rounds += 1;
|
|
47162
|
+
inflight = (async () => {
|
|
47163
|
+
const fresh = await refreshParts();
|
|
47164
|
+
for (const part of fresh) {
|
|
47165
|
+
assertPresignedUploadUrl(part.uploadUrl);
|
|
47166
|
+
urls.set(part.partNumber, part.uploadUrl);
|
|
47167
|
+
}
|
|
47168
|
+
})().finally(() => {
|
|
47169
|
+
inflight = null;
|
|
47170
|
+
});
|
|
47171
|
+
}
|
|
47172
|
+
await inflight;
|
|
47173
|
+
return true;
|
|
47174
|
+
};
|
|
47175
|
+
}
|
|
47176
|
+
function assertUploadPlan(totalBytes, partSizeBytes, parts) {
|
|
47177
|
+
if (!Number.isSafeInteger(totalBytes) || totalBytes < 1) {
|
|
47178
|
+
throw new Error("Invalid multipart upload: archive size must be a positive integer.");
|
|
47179
|
+
}
|
|
47180
|
+
if (!Number.isSafeInteger(partSizeBytes) || partSizeBytes < 1) {
|
|
47181
|
+
throw new Error("Invalid multipart upload: part size must be a positive integer.");
|
|
47182
|
+
}
|
|
47183
|
+
const totalParts = Math.ceil(totalBytes / partSizeBytes);
|
|
47184
|
+
const seen = /* @__PURE__ */ new Set();
|
|
47185
|
+
for (const part of parts) {
|
|
47186
|
+
if (!Number.isSafeInteger(part.partNumber) || part.partNumber < 1 || part.partNumber > totalParts || seen.has(part.partNumber)) {
|
|
47187
|
+
throw new Error("Invalid multipart upload: part numbers must be unique and in range.");
|
|
47188
|
+
}
|
|
47189
|
+
assertPresignedUploadUrl(part.uploadUrl);
|
|
47190
|
+
seen.add(part.partNumber);
|
|
47191
|
+
}
|
|
47192
|
+
}
|
|
47193
|
+
async function uploadPartWithRetry(partNumber, urls, body, fetchPart, wait, refreshUrls) {
|
|
47194
|
+
let lastStatus = null;
|
|
47195
|
+
for (let attempt = 1; attempt <= MAX_PART_ATTEMPTS; attempt += 1) {
|
|
47196
|
+
const uploadUrl = urls.get(partNumber);
|
|
47197
|
+
if (!uploadUrl) {
|
|
47198
|
+
throw new Error(`Source upload part ${partNumber.toString()} has no upload URL.`);
|
|
47199
|
+
}
|
|
47200
|
+
try {
|
|
47201
|
+
const response = await fetchPart(uploadUrl, {
|
|
47202
|
+
method: "PUT",
|
|
47203
|
+
body: new Uint8Array(body),
|
|
47204
|
+
redirect: "error"
|
|
47205
|
+
});
|
|
47206
|
+
if (response.ok) return;
|
|
47207
|
+
lastStatus = response.status;
|
|
47208
|
+
if (response.status === 403 && await refreshUrls()) {
|
|
47209
|
+
attempt -= 1;
|
|
47210
|
+
continue;
|
|
47211
|
+
}
|
|
47212
|
+
if (response.status >= 400 && response.status < 500 && response.status !== 408) break;
|
|
47213
|
+
} catch {
|
|
47214
|
+
lastStatus = null;
|
|
47215
|
+
}
|
|
47216
|
+
if (attempt < MAX_PART_ATTEMPTS) {
|
|
47217
|
+
await wait(250 * 2 ** (attempt - 1));
|
|
47218
|
+
}
|
|
47219
|
+
}
|
|
47220
|
+
const statusDetail = lastStatus === null ? "network error" : `status ${lastStatus.toString()}`;
|
|
47221
|
+
throw new Error(
|
|
47222
|
+
`Source upload part ${partNumber.toString()} failed after retries (${statusDetail}).`
|
|
47223
|
+
);
|
|
47224
|
+
}
|
|
47225
|
+
function assertPresignedUploadUrl(value) {
|
|
47226
|
+
let url2;
|
|
47227
|
+
try {
|
|
47228
|
+
url2 = new URL(value);
|
|
47229
|
+
} catch {
|
|
47230
|
+
throw new Error("Invalid multipart upload: the API returned an invalid part URL.");
|
|
47231
|
+
}
|
|
47232
|
+
const localHttp = url2.protocol === "http:" && (url2.hostname === "localhost" || url2.hostname === "127.0.0.1" || url2.hostname === "host.docker.internal");
|
|
47233
|
+
if (url2.protocol !== "https:" && !localHttp) {
|
|
47234
|
+
throw new Error("Invalid multipart upload: part URLs must use HTTPS.");
|
|
47235
|
+
}
|
|
47236
|
+
}
|
|
47237
|
+
function progress(uploadedBytes, totalBytes) {
|
|
47238
|
+
return {
|
|
47239
|
+
uploadedBytes,
|
|
47240
|
+
totalBytes,
|
|
47241
|
+
fraction: totalBytes === 0 ? 0 : Math.min(1, uploadedBytes / totalBytes)
|
|
47242
|
+
};
|
|
47243
|
+
}
|
|
47244
|
+
function defaultWait(milliseconds) {
|
|
47245
|
+
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
47246
|
+
}
|
|
47247
|
+
|
|
47248
|
+
// src/upload/commands.ts
|
|
47249
|
+
var TERMINAL_STATUSES = /* @__PURE__ */ new Set(["READY", "REJECTED", "FAILED", "DELETING"]);
|
|
47250
|
+
var DEFAULT_RUNTIME = {
|
|
47251
|
+
wait: (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)),
|
|
47252
|
+
pollIntervalMs: 3e3,
|
|
47253
|
+
fetch: (input, init) => globalThis.fetch(input, init)
|
|
47254
|
+
};
|
|
47255
|
+
function resolveRuntime(runtime) {
|
|
47256
|
+
return {
|
|
47257
|
+
wait: runtime.wait ?? DEFAULT_RUNTIME.wait,
|
|
47258
|
+
pollIntervalMs: runtime.pollIntervalMs ?? DEFAULT_RUNTIME.pollIntervalMs,
|
|
47259
|
+
fetch: runtime.fetch ?? DEFAULT_RUNTIME.fetch
|
|
47260
|
+
};
|
|
47261
|
+
}
|
|
47262
|
+
function isTerminalCodeUploadStatus(status) {
|
|
47263
|
+
return TERMINAL_STATUSES.has(status);
|
|
47264
|
+
}
|
|
47265
|
+
function requireUuid(value, label) {
|
|
47266
|
+
if (!isUuid(value)) {
|
|
47267
|
+
console.error(colorError(`Invalid ${label} '${sanitizeDisplay(value)}'. Expected a UUID.`));
|
|
47268
|
+
process.exit(1);
|
|
47269
|
+
}
|
|
47270
|
+
return value;
|
|
47271
|
+
}
|
|
47272
|
+
function parseKindOption(value) {
|
|
47273
|
+
if (value === void 0) return void 0;
|
|
47274
|
+
const kind = normalizeArtifactKind(value);
|
|
47275
|
+
if (kind == null) {
|
|
47276
|
+
console.error(
|
|
47277
|
+
colorError(`Invalid --kind '${sanitizeDisplay(value)}'. Use source (code ZIP), apk, or ipa.`)
|
|
47278
|
+
);
|
|
47279
|
+
process.exit(1);
|
|
47280
|
+
}
|
|
47281
|
+
return kind;
|
|
47282
|
+
}
|
|
47283
|
+
function failPrepare(error48) {
|
|
47284
|
+
const message = error48 instanceof CodeUploadPrepareError ? error48.message : error48 instanceof Error ? error48.message : "Failed to prepare the file.";
|
|
47285
|
+
console.error(colorError(message));
|
|
47286
|
+
process.exit(1);
|
|
47287
|
+
}
|
|
47288
|
+
function failWriteOp5(action, status, body) {
|
|
47289
|
+
const { message, errorCode } = readErrorFields(body);
|
|
47290
|
+
if (status === UNAUTHORIZED_STATUS2) {
|
|
47291
|
+
exitAuthFailed();
|
|
47292
|
+
}
|
|
47293
|
+
if (status === 403) {
|
|
47294
|
+
console.error(
|
|
47295
|
+
colorError(
|
|
47296
|
+
`Only an organization admin (or Trace staff) on a pentest-enabled organization can ${action} source uploads.`
|
|
47297
|
+
)
|
|
47298
|
+
);
|
|
47299
|
+
process.exit(1);
|
|
47300
|
+
}
|
|
47301
|
+
if (status === 404) {
|
|
47302
|
+
console.error(colorError(message ?? "No code upload with that id."));
|
|
47303
|
+
process.exit(1);
|
|
47304
|
+
}
|
|
47305
|
+
if (status === 400) {
|
|
47306
|
+
console.error(colorError(message ?? "Invalid request (status 400)."));
|
|
47307
|
+
process.exit(1);
|
|
47308
|
+
}
|
|
47309
|
+
if (status === 409) {
|
|
47310
|
+
if (errorCode === "CODE_UPLOAD_NOT_READY") {
|
|
47311
|
+
console.error(
|
|
47312
|
+
colorError(
|
|
47313
|
+
message ?? "The upload must be ready and pass malware scanning before it can be linked. Run `tracecli upload wait <id>`."
|
|
47314
|
+
)
|
|
47315
|
+
);
|
|
47316
|
+
} else if (errorCode === "CODE_UPLOAD_NOT_SOURCE") {
|
|
47317
|
+
console.error(
|
|
47318
|
+
colorError(message ?? "APK and IPA uploads cannot be linked as source repositories.")
|
|
47319
|
+
);
|
|
47320
|
+
} else {
|
|
47321
|
+
console.error(colorError(message ?? "Conflict (status 409)."));
|
|
47322
|
+
}
|
|
47323
|
+
process.exit(1);
|
|
47324
|
+
}
|
|
47325
|
+
if (status === 503) {
|
|
47326
|
+
console.error(
|
|
47327
|
+
colorError(message ?? "Source archive uploads are not configured on this server.")
|
|
47328
|
+
);
|
|
47329
|
+
process.exit(1);
|
|
47330
|
+
}
|
|
47331
|
+
console.error(
|
|
47332
|
+
colorError(`Failed to ${action} upload (status ${status})${message ? `: ${message}` : "."}`)
|
|
47333
|
+
);
|
|
47334
|
+
process.exit(1);
|
|
47335
|
+
}
|
|
47336
|
+
function failReadOp5(action, status, body) {
|
|
47337
|
+
const { message } = readErrorFields(body);
|
|
47338
|
+
if (status === UNAUTHORIZED_STATUS2) {
|
|
47339
|
+
exitAuthFailed();
|
|
47340
|
+
}
|
|
47341
|
+
if (status === 403) {
|
|
47342
|
+
console.error(
|
|
47343
|
+
colorError(
|
|
47344
|
+
"Only an organization admin (or Trace staff) on a pentest-enabled organization can view source uploads."
|
|
47345
|
+
)
|
|
47346
|
+
);
|
|
47347
|
+
process.exit(1);
|
|
47348
|
+
}
|
|
47349
|
+
if (status === 404) {
|
|
47350
|
+
console.error(colorError(message ?? "No code upload with that id."));
|
|
47351
|
+
process.exit(1);
|
|
47352
|
+
}
|
|
47353
|
+
if (status === 503) {
|
|
47354
|
+
console.error(
|
|
47355
|
+
colorError(message ?? "Source archive uploads are not configured on this server.")
|
|
47356
|
+
);
|
|
47357
|
+
process.exit(1);
|
|
47358
|
+
}
|
|
47359
|
+
console.error(
|
|
47360
|
+
colorError(`Failed to ${action} (status ${status})${message ? `: ${message}` : "."}`)
|
|
47361
|
+
);
|
|
47362
|
+
process.exit(1);
|
|
47363
|
+
}
|
|
47364
|
+
function resumeHint(codeUploadId, filePath) {
|
|
47365
|
+
return `Resume with: tracecli upload resume ${codeUploadId} ${filePath}`;
|
|
47366
|
+
}
|
|
47367
|
+
function buildUploadsTable(uploads) {
|
|
47368
|
+
const cells = uploads.map((upload) => ({
|
|
47369
|
+
name: sanitizeDisplay(upload.displayName),
|
|
47370
|
+
kind: formatStatus(upload.artifactKind),
|
|
47371
|
+
status: formatStatus(upload.status),
|
|
47372
|
+
size: formatArchiveBytes(upload.sizeBytes),
|
|
47373
|
+
id: upload.codeUploadId
|
|
47374
|
+
}));
|
|
47375
|
+
const nameWidth = Math.max(4, ...cells.map((c59) => c59.name.length));
|
|
47376
|
+
const kindWidth = Math.max(4, ...cells.map((c59) => c59.kind.length));
|
|
47377
|
+
const statusWidth = Math.max(6, ...cells.map((c59) => c59.status.length));
|
|
47378
|
+
const sizeWidth = Math.max(4, ...cells.map((c59) => c59.size.length));
|
|
47379
|
+
const header = `${"NAME".padEnd(nameWidth)} ${"KIND".padEnd(kindWidth)} ${"STATUS".padEnd(statusWidth)} ${"SIZE".padEnd(sizeWidth)} ID`;
|
|
47380
|
+
const rows = cells.map(
|
|
47381
|
+
(c59) => `${c59.name.padEnd(nameWidth)} ${c59.kind.padEnd(kindWidth)} ${c59.status.padEnd(statusWidth)} ${c59.size.padEnd(sizeWidth)} ${c59.id}`
|
|
47382
|
+
);
|
|
47383
|
+
return { header, rows };
|
|
47384
|
+
}
|
|
47385
|
+
function renderUploadDetail(upload) {
|
|
47386
|
+
const pad = 18;
|
|
47387
|
+
const linked = upload.linkedApplications.length === 0 ? "\u2014" : upload.linkedApplications.map((app) => `${sanitizeDisplay(app.name)} (${app.applicationId})`).join(", ");
|
|
47388
|
+
return [
|
|
47389
|
+
`${"Name:".padEnd(pad)} ${sanitizeDisplay(upload.displayName)}`,
|
|
47390
|
+
`${"Filename:".padEnd(pad)} ${sanitizeDisplay(upload.originalFilename)}`,
|
|
47391
|
+
`${"Kind:".padEnd(pad)} ${formatStatus(upload.artifactKind)}`,
|
|
47392
|
+
`${"Status:".padEnd(pad)} ${formatStatus(upload.status)}`,
|
|
47393
|
+
`${"Size:".padEnd(pad)} ${formatArchiveBytes(upload.sizeBytes)}`,
|
|
47394
|
+
`${"SHA-256:".padEnd(pad)} ${upload.archiveSha256}`,
|
|
47395
|
+
`${"Upload ID:".padEnd(pad)} ${upload.codeUploadId}`,
|
|
47396
|
+
`${"Repository ID:".padEnd(pad)} ${upload.repositoryId}`,
|
|
47397
|
+
`${"Malware:".padEnd(pad)} ${upload.malwareScanResult ? formatStatus(upload.malwareScanResult) : "\u2014"}`,
|
|
47398
|
+
`${"Rejection:".padEnd(pad)} ${upload.rejectionReason ? formatStatus(upload.rejectionReason) : "\u2014"}`,
|
|
47399
|
+
`${"Linked apps:".padEnd(pad)} ${linked}`,
|
|
47400
|
+
`${"Created:".padEnd(pad)} ${formatDate(upload.createdAt)}`,
|
|
47401
|
+
`${"Ready:".padEnd(pad)} ${formatDate(upload.readyAt)}`
|
|
47402
|
+
].join("\n");
|
|
47403
|
+
}
|
|
47404
|
+
function printUploadsTsv(uploads) {
|
|
47405
|
+
console.log(
|
|
47406
|
+
[
|
|
47407
|
+
"codeUploadId",
|
|
47408
|
+
"repositoryId",
|
|
47409
|
+
"displayName",
|
|
47410
|
+
"originalFilename",
|
|
47411
|
+
"artifactKind",
|
|
47412
|
+
"status",
|
|
47413
|
+
"sizeBytes",
|
|
47414
|
+
"archiveSha256",
|
|
47415
|
+
"malwareScanResult",
|
|
47416
|
+
"rejectionReason",
|
|
47417
|
+
"applicationId",
|
|
47418
|
+
"createdAt",
|
|
47419
|
+
"readyAt"
|
|
47420
|
+
].join(" ")
|
|
47421
|
+
);
|
|
47422
|
+
for (const upload of uploads) {
|
|
47423
|
+
console.log(
|
|
47424
|
+
[
|
|
47425
|
+
tsvSafe(upload.codeUploadId),
|
|
47426
|
+
tsvSafe(upload.repositoryId),
|
|
47427
|
+
tsvSafe(sanitizeDisplay(upload.displayName)),
|
|
47428
|
+
tsvSafe(sanitizeDisplay(upload.originalFilename)),
|
|
47429
|
+
tsvSafe(upload.artifactKind),
|
|
47430
|
+
tsvSafe(upload.status),
|
|
47431
|
+
String(upload.sizeBytes),
|
|
47432
|
+
tsvSafe(upload.archiveSha256),
|
|
47433
|
+
tsvSafe(upload.malwareScanResult ?? ""),
|
|
47434
|
+
tsvSafe(upload.rejectionReason ?? ""),
|
|
47435
|
+
tsvSafe(upload.applicationId ?? ""),
|
|
47436
|
+
tsvSafe(toIso(upload.createdAt)),
|
|
47437
|
+
tsvSafe(upload.readyAt ? toIso(upload.readyAt) : "")
|
|
47438
|
+
].join(" ")
|
|
47439
|
+
);
|
|
47440
|
+
}
|
|
47441
|
+
}
|
|
47442
|
+
function outputUpload(upload, output) {
|
|
47443
|
+
if (output === "json") {
|
|
47444
|
+
console.log(JSON.stringify(upload, null, 2));
|
|
47445
|
+
return;
|
|
47446
|
+
}
|
|
47447
|
+
if (output === "tsv") {
|
|
47448
|
+
printUploadsTsv([upload]);
|
|
47449
|
+
return;
|
|
47450
|
+
}
|
|
47451
|
+
console.log(renderUploadDetail(upload));
|
|
47452
|
+
}
|
|
47453
|
+
function writeProgress(progress2) {
|
|
47454
|
+
if (!process.stderr.isTTY) return;
|
|
47455
|
+
const pct = Math.floor(progress2.fraction * 100);
|
|
47456
|
+
process.stderr.write(
|
|
47457
|
+
`\rUploading ${pct}% (${formatArchiveBytes(progress2.uploadedBytes)} / ${formatArchiveBytes(progress2.totalBytes)})`
|
|
47458
|
+
);
|
|
47459
|
+
}
|
|
47460
|
+
function finishProgress() {
|
|
47461
|
+
if (process.stderr.isTTY) process.stderr.write("\n");
|
|
47462
|
+
}
|
|
47463
|
+
async function listUploads(organizationId, query = {}) {
|
|
47464
|
+
const result = await authenticatedRequest(
|
|
47465
|
+
(client) => client.codeUpload.listCodeUploads({
|
|
47466
|
+
params: { organizationId },
|
|
47467
|
+
query
|
|
47468
|
+
})
|
|
47469
|
+
);
|
|
47470
|
+
if (result.status !== SUCCESS_STATUS) {
|
|
47471
|
+
failReadOp5("list uploads", result.status, result.body);
|
|
47472
|
+
}
|
|
47473
|
+
return result.body.uploads;
|
|
47474
|
+
}
|
|
47475
|
+
async function requireUpload(organizationId, codeUploadId, query = {}) {
|
|
47476
|
+
const uploads = await listUploads(organizationId, query);
|
|
47477
|
+
const match = uploads.find((upload) => upload.codeUploadId === codeUploadId);
|
|
47478
|
+
if (!match) {
|
|
47479
|
+
console.error(colorError(`No code upload '${codeUploadId}'.`));
|
|
47480
|
+
process.exit(1);
|
|
47481
|
+
}
|
|
47482
|
+
return match;
|
|
47483
|
+
}
|
|
47484
|
+
async function refreshRemainingParts(organizationId, codeUploadId) {
|
|
47485
|
+
const result = await authenticatedRequest(
|
|
47486
|
+
(client) => client.codeUpload.resumeCodeUpload({
|
|
47487
|
+
params: { organizationId, codeUploadId },
|
|
47488
|
+
body: {}
|
|
47489
|
+
})
|
|
47490
|
+
);
|
|
47491
|
+
if (result.status !== SUCCESS_STATUS) {
|
|
47492
|
+
failWriteOp5("resume", result.status, result.body);
|
|
47493
|
+
}
|
|
47494
|
+
return result.body.multipart.parts;
|
|
47495
|
+
}
|
|
47496
|
+
async function completeUpload(organizationId, codeUploadId) {
|
|
47497
|
+
const result = await authenticatedRequest(
|
|
47498
|
+
(client) => client.codeUpload.completeCodeUpload({
|
|
47499
|
+
params: { organizationId, codeUploadId },
|
|
47500
|
+
body: {}
|
|
47501
|
+
})
|
|
47502
|
+
);
|
|
47503
|
+
if (result.status !== ACCEPTED_STATUS) {
|
|
47504
|
+
failWriteOp5("complete", result.status, result.body);
|
|
47505
|
+
}
|
|
47506
|
+
return result.body;
|
|
47507
|
+
}
|
|
47508
|
+
async function waitForTerminal(organizationId, codeUploadId, runtime) {
|
|
47509
|
+
for (; ; ) {
|
|
47510
|
+
const upload = await requireUpload(organizationId, codeUploadId);
|
|
47511
|
+
if (isTerminalCodeUploadStatus(upload.status)) {
|
|
47512
|
+
return upload;
|
|
47513
|
+
}
|
|
47514
|
+
if (process.stderr.isTTY) {
|
|
47515
|
+
process.stderr.write(`\rWaiting for malware scan (${formatStatus(upload.status)})...`);
|
|
47516
|
+
}
|
|
47517
|
+
await runtime.wait(runtime.pollIntervalMs);
|
|
47518
|
+
}
|
|
47519
|
+
}
|
|
47520
|
+
function printWaitOutcome(upload, output) {
|
|
47521
|
+
if (output !== "table") {
|
|
47522
|
+
outputUpload(upload, output);
|
|
47523
|
+
if (upload.status !== "READY") process.exit(1);
|
|
47524
|
+
return;
|
|
47525
|
+
}
|
|
47526
|
+
if (upload.status === "READY") {
|
|
47527
|
+
console.log(colorSuccess(`Upload ${upload.codeUploadId} is ready.`));
|
|
47528
|
+
outputUpload(upload, output);
|
|
47529
|
+
if (upload.artifactKind === "SOURCE_ARCHIVE") {
|
|
47530
|
+
console.log(
|
|
47531
|
+
`Link it with: tracecli upload link --app <applicationId> --upload ${upload.codeUploadId}`
|
|
47532
|
+
);
|
|
47533
|
+
}
|
|
47534
|
+
return;
|
|
47535
|
+
}
|
|
47536
|
+
const reason = upload.rejectionReason ? ` (${formatStatus(upload.rejectionReason)})` : "";
|
|
47537
|
+
console.error(
|
|
47538
|
+
colorError(`Upload ${upload.codeUploadId} ended as ${formatStatus(upload.status)}${reason}.`)
|
|
47539
|
+
);
|
|
47540
|
+
outputUpload(upload, output);
|
|
47541
|
+
process.exit(1);
|
|
47542
|
+
}
|
|
47543
|
+
async function runTransfer(filePath, options, runtime) {
|
|
47544
|
+
const output = parseOutput(options.output);
|
|
47545
|
+
let prepared;
|
|
47546
|
+
try {
|
|
47547
|
+
prepared = await prepareUploadFile(filePath, {
|
|
47548
|
+
displayName: options.name,
|
|
47549
|
+
artifactKind: options.artifactKind
|
|
47550
|
+
});
|
|
47551
|
+
} catch (error48) {
|
|
47552
|
+
failPrepare(error48);
|
|
47553
|
+
}
|
|
47554
|
+
const organizationId = await resolveOrganizationId();
|
|
47555
|
+
const initialized = await authenticatedRequest(
|
|
47556
|
+
(client) => client.codeUpload.initializeCodeUpload({
|
|
47557
|
+
params: { organizationId },
|
|
47558
|
+
body: {
|
|
47559
|
+
displayName: prepared.displayName,
|
|
47560
|
+
originalFilename: prepared.originalFilename,
|
|
47561
|
+
sizeBytes: prepared.sizeBytes,
|
|
47562
|
+
archiveSha256: prepared.archiveSha256,
|
|
47563
|
+
artifactKind: prepared.artifactKind,
|
|
47564
|
+
...options.applicationId !== void 0 ? { applicationId: options.applicationId } : {}
|
|
47565
|
+
}
|
|
47566
|
+
})
|
|
47567
|
+
);
|
|
47568
|
+
if (initialized.status !== CREATED_STATUS) {
|
|
47569
|
+
failWriteOp5("create", initialized.status, initialized.body);
|
|
47570
|
+
}
|
|
47571
|
+
const codeUploadId = initialized.body.upload.codeUploadId;
|
|
47572
|
+
if (output === "table") {
|
|
47573
|
+
console.error(
|
|
47574
|
+
`Initialized upload ${codeUploadId} (${formatArchiveBytes(prepared.sizeBytes)}).`
|
|
47575
|
+
);
|
|
47576
|
+
}
|
|
47577
|
+
try {
|
|
47578
|
+
await uploadCodeArchiveParts(
|
|
47579
|
+
prepared.path,
|
|
47580
|
+
prepared.sizeBytes,
|
|
47581
|
+
initialized.body.multipart.partSizeBytes,
|
|
47582
|
+
initialized.body.multipart.parts,
|
|
47583
|
+
{
|
|
47584
|
+
fetch: runtime.fetch,
|
|
47585
|
+
wait: runtime.wait,
|
|
47586
|
+
onProgress: writeProgress,
|
|
47587
|
+
refreshParts: () => refreshRemainingParts(organizationId, codeUploadId)
|
|
47588
|
+
}
|
|
47589
|
+
);
|
|
47590
|
+
finishProgress();
|
|
47591
|
+
} catch (error48) {
|
|
47592
|
+
finishProgress();
|
|
47593
|
+
const detail = error48 instanceof Error ? error48.message : "Upload to storage failed.";
|
|
47594
|
+
console.error(colorError(detail));
|
|
47595
|
+
console.error(resumeHint(codeUploadId, filePath));
|
|
47596
|
+
process.exit(1);
|
|
47597
|
+
}
|
|
47598
|
+
const completed = await completeUpload(organizationId, codeUploadId);
|
|
47599
|
+
const upload = options.wait ? await waitForTerminal(organizationId, codeUploadId, runtime) : completed;
|
|
47600
|
+
if (options.wait) {
|
|
47601
|
+
if (process.stderr.isTTY) process.stderr.write("\n");
|
|
47602
|
+
printWaitOutcome(upload, output);
|
|
47603
|
+
return;
|
|
47604
|
+
}
|
|
47605
|
+
if (output === "table") {
|
|
47606
|
+
console.log(colorSuccess(`Uploaded ${sanitizeDisplay(upload.displayName)}.`));
|
|
47607
|
+
console.log(
|
|
47608
|
+
`Malware scanning is in progress. Check with: tracecli upload wait ${codeUploadId}`
|
|
47609
|
+
);
|
|
47610
|
+
}
|
|
47611
|
+
outputUpload(upload, output);
|
|
47612
|
+
}
|
|
47613
|
+
async function runCreate5(filePath, options, runtime) {
|
|
47614
|
+
const inferred = inferArtifactKindFromFilename(filePath);
|
|
47615
|
+
if (inferred != null && isApplicationBuildArtifactKind(inferred)) {
|
|
47616
|
+
console.error(
|
|
47617
|
+
colorError(
|
|
47618
|
+
"That file is a mobile build, not a source archive. Attach it to an existing application with:"
|
|
47619
|
+
)
|
|
47620
|
+
);
|
|
47621
|
+
console.error(" tracecli upload build <file> --app <applicationId>");
|
|
47622
|
+
process.exit(1);
|
|
47623
|
+
}
|
|
47624
|
+
await runTransfer(
|
|
47625
|
+
filePath,
|
|
47626
|
+
{
|
|
47627
|
+
...options.name !== void 0 ? { name: options.name } : {},
|
|
47628
|
+
...options.wait !== void 0 ? { wait: options.wait } : {},
|
|
47629
|
+
output: options.output,
|
|
47630
|
+
artifactKind: "SOURCE_ARCHIVE"
|
|
47631
|
+
},
|
|
47632
|
+
runtime
|
|
47633
|
+
);
|
|
47634
|
+
}
|
|
47635
|
+
async function runBuild(filePath, options, runtime) {
|
|
47636
|
+
const applicationId = requireUuid(options.app, "--app");
|
|
47637
|
+
const inferred = inferArtifactKindFromFilename(filePath);
|
|
47638
|
+
if (inferred == null || !isApplicationBuildArtifactKind(inferred)) {
|
|
47639
|
+
console.error(
|
|
47640
|
+
colorError(
|
|
47641
|
+
"Build uploads are an APK or IPA attached to an existing application. For a source ZIP, use `tracecli upload create <file>`."
|
|
47642
|
+
)
|
|
47643
|
+
);
|
|
47644
|
+
process.exit(1);
|
|
47645
|
+
}
|
|
47646
|
+
await runTransfer(
|
|
47647
|
+
filePath,
|
|
47648
|
+
{
|
|
47649
|
+
...options.name !== void 0 ? { name: options.name } : {},
|
|
47650
|
+
...options.wait !== void 0 ? { wait: options.wait } : {},
|
|
47651
|
+
output: options.output,
|
|
47652
|
+
applicationId,
|
|
47653
|
+
artifactKind: inferred
|
|
47654
|
+
},
|
|
47655
|
+
runtime
|
|
47656
|
+
);
|
|
47657
|
+
}
|
|
47658
|
+
async function runResume(codeUploadIdArg, filePath, options, runtime) {
|
|
47659
|
+
const output = parseOutput(options.output);
|
|
47660
|
+
const codeUploadId = requireUuid(codeUploadIdArg, "code upload id");
|
|
47661
|
+
let prepared;
|
|
47662
|
+
try {
|
|
47663
|
+
prepared = await prepareUploadFile(filePath);
|
|
47664
|
+
} catch (error48) {
|
|
47665
|
+
failPrepare(error48);
|
|
47666
|
+
}
|
|
47667
|
+
const organizationId = await resolveOrganizationId();
|
|
47668
|
+
const resumed = await authenticatedRequest(
|
|
47669
|
+
(client) => client.codeUpload.resumeCodeUpload({
|
|
47670
|
+
params: { organizationId, codeUploadId },
|
|
47671
|
+
body: {}
|
|
47672
|
+
})
|
|
47673
|
+
);
|
|
47674
|
+
if (resumed.status !== SUCCESS_STATUS) {
|
|
47675
|
+
failWriteOp5("resume", resumed.status, resumed.body);
|
|
47676
|
+
}
|
|
47677
|
+
try {
|
|
47678
|
+
assertPreparedMatchesUpload(resumed.body.upload, prepared);
|
|
47679
|
+
} catch (error48) {
|
|
47680
|
+
failPrepare(error48);
|
|
47681
|
+
}
|
|
47682
|
+
const uploadedBytes = resumed.body.multipart.uploadedParts.reduce(
|
|
47683
|
+
(sum, part) => sum + part.sizeBytes,
|
|
47684
|
+
0
|
|
47685
|
+
);
|
|
47686
|
+
if (output === "table") {
|
|
47687
|
+
console.error(
|
|
47688
|
+
`Resuming upload ${codeUploadId} (${formatArchiveBytes(uploadedBytes)} already stored).`
|
|
47689
|
+
);
|
|
47690
|
+
}
|
|
47691
|
+
try {
|
|
47692
|
+
await uploadCodeArchiveParts(
|
|
47693
|
+
prepared.path,
|
|
47694
|
+
prepared.sizeBytes,
|
|
47695
|
+
resumed.body.multipart.partSizeBytes,
|
|
47696
|
+
resumed.body.multipart.parts,
|
|
47697
|
+
{
|
|
47698
|
+
initialUploadedBytes: uploadedBytes,
|
|
47699
|
+
fetch: runtime.fetch,
|
|
47700
|
+
wait: runtime.wait,
|
|
47701
|
+
onProgress: writeProgress,
|
|
47702
|
+
refreshParts: () => refreshRemainingParts(organizationId, codeUploadId)
|
|
47703
|
+
}
|
|
47704
|
+
);
|
|
47705
|
+
finishProgress();
|
|
47706
|
+
} catch (error48) {
|
|
47707
|
+
finishProgress();
|
|
47708
|
+
const detail = error48 instanceof Error ? error48.message : "Upload to storage failed.";
|
|
47709
|
+
console.error(colorError(detail));
|
|
47710
|
+
console.error(resumeHint(codeUploadId, filePath));
|
|
47711
|
+
process.exit(1);
|
|
47712
|
+
}
|
|
47713
|
+
const completed = await completeUpload(organizationId, codeUploadId);
|
|
47714
|
+
const upload = options.wait ? await waitForTerminal(organizationId, codeUploadId, runtime) : completed;
|
|
47715
|
+
if (options.wait) {
|
|
47716
|
+
if (process.stderr.isTTY) process.stderr.write("\n");
|
|
47717
|
+
printWaitOutcome(upload, output);
|
|
47718
|
+
return;
|
|
47719
|
+
}
|
|
47720
|
+
if (output === "table") {
|
|
47721
|
+
console.log(colorSuccess(`Uploaded ${sanitizeDisplay(upload.displayName)}.`));
|
|
47722
|
+
console.log(
|
|
47723
|
+
`Malware scanning is in progress. Check with: tracecli upload wait ${codeUploadId}`
|
|
47724
|
+
);
|
|
47725
|
+
}
|
|
47726
|
+
outputUpload(upload, output);
|
|
47727
|
+
}
|
|
47728
|
+
async function runList5(options) {
|
|
47729
|
+
const output = parseOutput(options.output);
|
|
47730
|
+
if (options.web) {
|
|
47731
|
+
const slug = await resolveOrganizationSlug();
|
|
47732
|
+
const url2 = `${getAppBaseUrl()}/dashboard/${slug}/settings/integrations/sources`;
|
|
47733
|
+
const opened = await openBrowser(url2);
|
|
47734
|
+
if (!opened) console.log(url2);
|
|
47735
|
+
return;
|
|
47736
|
+
}
|
|
47737
|
+
const kind = parseKindOption(options.kind);
|
|
47738
|
+
const applicationId = options.app !== void 0 ? requireUuid(options.app, "--app") : void 0;
|
|
47739
|
+
const organizationId = await resolveOrganizationId();
|
|
47740
|
+
const uploads = await listUploads(organizationId, {
|
|
47741
|
+
...kind !== void 0 ? { artifactKind: kind } : {},
|
|
47742
|
+
...applicationId !== void 0 ? { applicationId } : {}
|
|
47743
|
+
});
|
|
47744
|
+
if (output === "json") {
|
|
47745
|
+
console.log(JSON.stringify({ uploads }, null, 2));
|
|
47746
|
+
return;
|
|
47747
|
+
}
|
|
47748
|
+
if (output === "tsv") {
|
|
47749
|
+
printUploadsTsv(uploads);
|
|
47750
|
+
return;
|
|
47751
|
+
}
|
|
47752
|
+
if (uploads.length === 0) {
|
|
47753
|
+
console.log("No source uploads found.");
|
|
47754
|
+
return;
|
|
47755
|
+
}
|
|
47756
|
+
const { header, rows } = buildUploadsTable(uploads);
|
|
47757
|
+
await runInteractivePager({
|
|
47758
|
+
items: uploads,
|
|
47759
|
+
header,
|
|
47760
|
+
renderItem: (_upload, i2) => [rows[i2] ?? ""],
|
|
47761
|
+
disabled: options.pager === false
|
|
47762
|
+
});
|
|
47763
|
+
}
|
|
47764
|
+
async function runGet4(codeUploadIdArg, options) {
|
|
47765
|
+
const output = parseOutput(options.output);
|
|
47766
|
+
const codeUploadId = requireUuid(codeUploadIdArg, "code upload id");
|
|
47767
|
+
const organizationId = await resolveOrganizationId();
|
|
47768
|
+
const upload = await requireUpload(organizationId, codeUploadId);
|
|
47769
|
+
outputUpload(upload, output);
|
|
47770
|
+
}
|
|
47771
|
+
async function runWait(codeUploadIdArg, options, runtime) {
|
|
47772
|
+
const output = parseOutput(options.output);
|
|
47773
|
+
const codeUploadId = requireUuid(codeUploadIdArg, "code upload id");
|
|
47774
|
+
const organizationId = await resolveOrganizationId();
|
|
47775
|
+
const upload = await waitForTerminal(organizationId, codeUploadId, runtime);
|
|
47776
|
+
if (process.stderr.isTTY) process.stderr.write("\n");
|
|
47777
|
+
printWaitOutcome(upload, output);
|
|
47778
|
+
}
|
|
47779
|
+
async function runDelete5(codeUploadIdArg, options) {
|
|
47780
|
+
const codeUploadId = requireUuid(codeUploadIdArg, "code upload id");
|
|
47781
|
+
const confirmed = await confirmDeletion(`upload ${codeUploadId}`, options.yes);
|
|
47782
|
+
if (!confirmed) {
|
|
47783
|
+
console.log("Aborted.");
|
|
47784
|
+
return;
|
|
47785
|
+
}
|
|
47786
|
+
const organizationId = await resolveOrganizationId();
|
|
47787
|
+
const result = await authenticatedRequest(
|
|
47788
|
+
(client) => client.codeUpload.deleteCodeUpload({
|
|
47789
|
+
params: { organizationId, codeUploadId }
|
|
47790
|
+
})
|
|
47791
|
+
);
|
|
47792
|
+
if (result.status !== NO_CONTENT_STATUS) {
|
|
47793
|
+
failWriteOp5("delete", result.status, result.body);
|
|
47794
|
+
}
|
|
47795
|
+
console.log(colorSuccess(`Deleted upload ${codeUploadId}`));
|
|
47796
|
+
}
|
|
47797
|
+
async function runLink(options) {
|
|
47798
|
+
const output = parseOutput(options.output);
|
|
47799
|
+
const applicationId = requireUuid(options.app, "--app");
|
|
47800
|
+
const codeUploadId = requireUuid(options.upload, "--upload");
|
|
47801
|
+
const organizationId = await resolveOrganizationId();
|
|
47802
|
+
const upload = await requireUpload(organizationId, codeUploadId);
|
|
47803
|
+
if (isApplicationBuildArtifactKind(upload.artifactKind)) {
|
|
47804
|
+
console.error(colorError("APK and IPA uploads cannot be linked as source repositories."));
|
|
47805
|
+
process.exit(1);
|
|
47806
|
+
}
|
|
47807
|
+
if (upload.status !== "READY" || upload.malwareScanResult !== "NO_THREATS_FOUND") {
|
|
47808
|
+
console.error(
|
|
47809
|
+
colorError(
|
|
47810
|
+
`Upload is ${formatStatus(upload.status)}; it must be ready and pass malware scanning before it can be linked.`
|
|
47811
|
+
)
|
|
47812
|
+
);
|
|
47813
|
+
process.exit(1);
|
|
47814
|
+
}
|
|
47815
|
+
const result = await authenticatedRequest(
|
|
47816
|
+
(client) => client.application.createApplicationRepository({
|
|
47817
|
+
params: { organizationId, applicationId },
|
|
47818
|
+
body: {
|
|
47819
|
+
repositoryId: upload.repositoryId,
|
|
47820
|
+
...options.path !== void 0 ? { repositoryPath: options.path } : {}
|
|
47821
|
+
}
|
|
47822
|
+
})
|
|
47823
|
+
);
|
|
47824
|
+
if (result.status !== CREATED_STATUS) {
|
|
47825
|
+
failWriteOp5("link", result.status, result.body);
|
|
47826
|
+
}
|
|
47827
|
+
if (output === "json") {
|
|
47828
|
+
console.log(JSON.stringify(result.body, null, 2));
|
|
47829
|
+
return;
|
|
47830
|
+
}
|
|
47831
|
+
if (output === "tsv") {
|
|
47832
|
+
console.log(["applicationId", "repositoryId", "codeUploadId"].join(" "));
|
|
47833
|
+
console.log([applicationId, upload.repositoryId, codeUploadId].join(" "));
|
|
47834
|
+
return;
|
|
47835
|
+
}
|
|
47836
|
+
console.log(
|
|
47837
|
+
colorSuccess(`Linked ${sanitizeDisplay(upload.displayName)} to application ${applicationId}.`)
|
|
47838
|
+
);
|
|
47839
|
+
}
|
|
47840
|
+
function createCreateCommand5(runtime) {
|
|
47841
|
+
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(
|
|
47842
|
+
"after",
|
|
47843
|
+
`
|
|
47844
|
+
This is a code upload: a source ZIP that becomes an uploaded repository after
|
|
47845
|
+
it is ready. Link it with \`upload link\`. APK and IPA files belong on
|
|
47846
|
+
\`upload build --app\`, which attaches a build to an existing application.
|
|
47847
|
+
|
|
47848
|
+
Requires a human login (\`tracecli auth login\`) on a pentest-enabled organization.
|
|
47849
|
+
API keys are rejected.
|
|
47850
|
+
|
|
47851
|
+
Examples:
|
|
47852
|
+
tracecli upload create ./checkout.zip
|
|
47853
|
+
tracecli upload create ./checkout.zip --name "Checkout" --wait`
|
|
47854
|
+
).action(async (file2, options) => {
|
|
47855
|
+
await runCreate5(file2, options, runtime);
|
|
47856
|
+
});
|
|
47857
|
+
}
|
|
47858
|
+
function createBuildCommand(runtime) {
|
|
47859
|
+
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(
|
|
47860
|
+
"after",
|
|
47861
|
+
`
|
|
47862
|
+
Build uploads are not source archives. They attach to --app immediately and
|
|
47863
|
+
cannot be linked as a repository. For a source ZIP, use \`upload create\`.
|
|
47864
|
+
|
|
47865
|
+
Examples:
|
|
47866
|
+
tracecli upload build ./app-release.apk --app <applicationId>
|
|
47867
|
+
tracecli upload build ./app.ipa --app <applicationId> --name "iOS build"`
|
|
47868
|
+
).action(async (file2, options) => {
|
|
47869
|
+
await runBuild(file2, options, runtime);
|
|
47870
|
+
});
|
|
47871
|
+
}
|
|
47872
|
+
function createListCommand7() {
|
|
47873
|
+
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) => {
|
|
47874
|
+
await runList5(options);
|
|
47875
|
+
});
|
|
47876
|
+
}
|
|
47877
|
+
function createGetCommand4() {
|
|
47878
|
+
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) => {
|
|
47879
|
+
await runGet4(codeUploadId, options);
|
|
47880
|
+
});
|
|
47881
|
+
}
|
|
47882
|
+
function createResumeCommand(runtime) {
|
|
47883
|
+
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(
|
|
47884
|
+
"after",
|
|
47885
|
+
`
|
|
47886
|
+
The file must match the original size and SHA-256. Trace skips parts that
|
|
47887
|
+
already landed in storage.
|
|
47888
|
+
|
|
47889
|
+
Examples:
|
|
47890
|
+
tracecli upload resume <codeUploadId> ./checkout.zip`
|
|
47891
|
+
).action(async (codeUploadId, file2, options) => {
|
|
47892
|
+
await runResume(codeUploadId, file2, options, runtime);
|
|
47893
|
+
});
|
|
47894
|
+
}
|
|
47895
|
+
function createWaitCommand(runtime) {
|
|
47896
|
+
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) => {
|
|
47897
|
+
await runWait(codeUploadId, options, runtime);
|
|
47898
|
+
});
|
|
47899
|
+
}
|
|
47900
|
+
function createDeleteCommand5() {
|
|
47901
|
+
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) => {
|
|
47902
|
+
await runDelete5(codeUploadId, options);
|
|
47903
|
+
});
|
|
47904
|
+
}
|
|
47905
|
+
function createLinkCommand() {
|
|
47906
|
+
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(
|
|
47907
|
+
"after",
|
|
47908
|
+
`
|
|
47909
|
+
Only a READY source archive that passed malware scanning can be linked.
|
|
47910
|
+
APK and IPA builds attach to an application with \`upload build --app\`.
|
|
47911
|
+
|
|
47912
|
+
Examples:
|
|
47913
|
+
tracecli upload link --app <applicationId> --upload <codeUploadId>`
|
|
47914
|
+
).action(async (options) => {
|
|
47915
|
+
await runLink(options);
|
|
47916
|
+
});
|
|
47917
|
+
}
|
|
47918
|
+
function createUploadCommand(runtime = {}) {
|
|
47919
|
+
const resolved = resolveRuntime(runtime);
|
|
47920
|
+
const upload = new Command("upload").alias("code-upload").description("Upload source archives (and separately, mobile builds) for pentest");
|
|
47921
|
+
upload.addCommand(createCreateCommand5(resolved));
|
|
47922
|
+
upload.addCommand(createBuildCommand(resolved));
|
|
47923
|
+
upload.addCommand(createListCommand7());
|
|
47924
|
+
upload.addCommand(createGetCommand4());
|
|
47925
|
+
upload.addCommand(createResumeCommand(resolved));
|
|
47926
|
+
upload.addCommand(createWaitCommand(resolved));
|
|
47927
|
+
upload.addCommand(createDeleteCommand5());
|
|
47928
|
+
upload.addCommand(createLinkCommand());
|
|
47929
|
+
return upload;
|
|
47930
|
+
}
|
|
47931
|
+
|
|
46976
47932
|
// src/vulnerabilities/commands.ts
|
|
46977
47933
|
var DEFAULT_LIMIT3 = 30;
|
|
46978
47934
|
var MAX_LIMIT3 = 100;
|
|
@@ -47149,7 +48105,7 @@ function printVulnerabilityDetailTsv(vulnerability) {
|
|
|
47149
48105
|
].map(sanitizeTsvField);
|
|
47150
48106
|
console.log(cols.join(" "));
|
|
47151
48107
|
}
|
|
47152
|
-
function
|
|
48108
|
+
function createGetCommand5() {
|
|
47153
48109
|
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
48110
|
"after",
|
|
47155
48111
|
`
|
|
@@ -47208,7 +48164,7 @@ Run \`tracecli vulnerabilities list\` to see available vulnerabilities.`
|
|
|
47208
48164
|
printVulnerabilityDetail(result.body);
|
|
47209
48165
|
});
|
|
47210
48166
|
}
|
|
47211
|
-
function
|
|
48167
|
+
function createListCommand8() {
|
|
47212
48168
|
return new Command("list").description("List vulnerabilities for your organization").option("--repo <repo>", "Filter by repository name or owner/repo").option(
|
|
47213
48169
|
"--severity <severity>",
|
|
47214
48170
|
"Filter by severity (CRITICAL, HIGH, MEDIUM, LOW)",
|
|
@@ -47341,8 +48297,8 @@ Examples:
|
|
|
47341
48297
|
}
|
|
47342
48298
|
function createVulnerabilitiesCommand() {
|
|
47343
48299
|
const vulnerabilities = new Command("vulnerabilities").description("Manage vulnerabilities");
|
|
47344
|
-
vulnerabilities.addCommand(
|
|
47345
|
-
vulnerabilities.addCommand(
|
|
48300
|
+
vulnerabilities.addCommand(createGetCommand5());
|
|
48301
|
+
vulnerabilities.addCommand(createListCommand8());
|
|
47346
48302
|
return vulnerabilities;
|
|
47347
48303
|
}
|
|
47348
48304
|
|
|
@@ -47365,6 +48321,7 @@ program2.addCommand(createInboxCommand());
|
|
|
47365
48321
|
program2.addCommand(createRepoCommand());
|
|
47366
48322
|
program2.addCommand(createVulnerabilitiesCommand());
|
|
47367
48323
|
program2.addCommand(createCodeSizeCommand());
|
|
48324
|
+
program2.addCommand(createUploadCommand());
|
|
47368
48325
|
program2.addCommand(createPentestCommand());
|
|
47369
48326
|
program2.addCommand(createCompletionCommand(program2));
|
|
47370
48327
|
program2.addCommand(createUpgradeCommand());
|